240 lines
9.6 KiB
Swift
240 lines
9.6 KiB
Swift
//
|
|
// AiMessageItemView.swift
|
|
// SodaLive
|
|
//
|
|
// Created by klaus on 9/2/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Kingfisher
|
|
|
|
struct AiMessageBubbleShape: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
let path = UIBezierPath()
|
|
|
|
// 시작점 (왼쪽 상단, 4px 반지름)
|
|
path.move(to: CGPoint(x: 4, y: 0))
|
|
|
|
// 상단 라인 (오른쪽 상단 16px 반지름까지)
|
|
path.addLine(to: CGPoint(x: rect.width - 16, y: 0))
|
|
|
|
// 오른쪽 상단 모서리 (16px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: rect.width - 16, y: 16),
|
|
radius: 16,
|
|
startAngle: -CGFloat.pi / 2,
|
|
endAngle: 0,
|
|
clockwise: true)
|
|
|
|
// 오른쪽 라인 (오른쪽 하단 16px 반지름까지)
|
|
path.addLine(to: CGPoint(x: rect.width, y: rect.height - 16))
|
|
|
|
// 오른쪽 하단 모서리 (16px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: rect.width - 16, y: rect.height - 16),
|
|
radius: 16,
|
|
startAngle: 0,
|
|
endAngle: CGFloat.pi / 2,
|
|
clockwise: true)
|
|
|
|
// 하단 라인 (왼쪽 하단 16px 반지름까지)
|
|
path.addLine(to: CGPoint(x: 16, y: rect.height))
|
|
|
|
// 왼쪽 하단 모서리 (16px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: 16, y: rect.height - 16),
|
|
radius: 16,
|
|
startAngle: CGFloat.pi / 2,
|
|
endAngle: CGFloat.pi,
|
|
clockwise: true)
|
|
|
|
// 왼쪽 라인 (왼쪽 상단 4px 반지름까지)
|
|
path.addLine(to: CGPoint(x: 0, y: 4))
|
|
|
|
// 왼쪽 상단 모서리 (4px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: 4, y: 4),
|
|
radius: 4,
|
|
startAngle: CGFloat.pi,
|
|
endAngle: -CGFloat.pi / 2,
|
|
clockwise: true)
|
|
|
|
path.close()
|
|
return Path(path.cgPath)
|
|
}
|
|
}
|
|
|
|
struct AiMessageItemView: View {
|
|
let message: ServerChatMessage
|
|
let characterName: String
|
|
|
|
let purchaseMessage: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(alignment: .bottom, spacing: 4) {
|
|
// 메시지 영역
|
|
HStack(alignment: .top, spacing: 9) {
|
|
// 프로필 이미지
|
|
KFImage(URL(string: message.profileImageUrl))
|
|
.placeholder {
|
|
Image(systemName: "person.crop.circle")
|
|
.resizable()
|
|
.scaledToFit()
|
|
}
|
|
.cancelOnDisappear(true)
|
|
.resizable()
|
|
.frame(width: 30, height: 30)
|
|
.clipShape(Circle())
|
|
|
|
// 메시지 텍스트 영역
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(spacing: 4) {
|
|
Text(characterName)
|
|
.font(.custom(Font.preRegular.rawValue, size: 12))
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
// 메시지 내용 (텍스트 또는 이미지)
|
|
if message.messageType.lowercased() == "image",
|
|
let imageUrl = message.imageUrl,
|
|
!imageUrl.isEmpty
|
|
{
|
|
// 이미지 메시지
|
|
let maxWidth = (UIScreen.main.bounds.width - 48) * 0.7
|
|
let imageHeight = maxWidth * 5 / 4 // 4:5 비율
|
|
|
|
ZStack {
|
|
KFImage(URL(string: imageUrl))
|
|
.cancelOnDisappear(true)
|
|
.resizable()
|
|
.scaledToFill() // 비율 유지하며 프레임을 채움
|
|
|
|
if let price = message.price, price > 0, !message.hasAccess {
|
|
Color.black.opacity(0.2)
|
|
.frame(width: maxWidth, height: imageHeight)
|
|
.cornerRadius(10)
|
|
|
|
VStack(spacing: 18) {
|
|
HStack(spacing: 4) {
|
|
Image("ic_can")
|
|
.resizable()
|
|
.frame(width: 24, height: 24)
|
|
|
|
Text("\(message.price ?? 5)")
|
|
.font(.custom(Font.preBold.rawValue, size: 16))
|
|
.foregroundColor(Color(hex: "263238"))
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 3)
|
|
.background(Color(hex: "B5E7FA"))
|
|
.cornerRadius(30)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.stroke(lineWidth: 1)
|
|
.foregroundColor(.button)
|
|
}
|
|
|
|
Text("눌러서 잠금해제")
|
|
.font(.custom(Font.preBold.rawValue, size: 18))
|
|
.foregroundColor(.white)
|
|
}
|
|
.frame(width: maxWidth, height: imageHeight)
|
|
}
|
|
}
|
|
.frame(width: maxWidth, height: imageHeight)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
|
.onTapGesture {
|
|
purchaseMessage()
|
|
}
|
|
} else {
|
|
// 텍스트 메시지 버블
|
|
HStack(spacing: 10) {
|
|
styledMessageText(message.message)
|
|
.lineLimit(nil)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 8)
|
|
.background(
|
|
Color.black.opacity(0.1)
|
|
)
|
|
.clipShape(AiMessageBubbleShape())
|
|
}
|
|
}
|
|
}
|
|
|
|
// 시간 표시
|
|
VStack {
|
|
Text(formatTime(from: message.createdAt))
|
|
.font(.custom(Font.preRegular.rawValue, size: 10))
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private func formatTime(from timestamp: Int64) -> String {
|
|
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
|
return date.convertDateFormat(dateFormat: "a h:mm")
|
|
}
|
|
|
|
private func styledMessageText(_ message: String) -> Text {
|
|
var result = Text("")
|
|
let components = message.components(separatedBy: "(")
|
|
|
|
for (index, component) in components.enumerated() {
|
|
if index == 0 {
|
|
// 첫 번째 컴포넌트는 항상 일반 텍스트
|
|
if !component.isEmpty {
|
|
result = result + Text(component)
|
|
.font(.custom(Font.preRegular.rawValue, size: 16))
|
|
.foregroundColor(.white)
|
|
}
|
|
} else {
|
|
// "(" 이후의 텍스트 처리
|
|
if let closeIndex = component.firstIndex(of: ")") {
|
|
let beforeClose = String(component[..<closeIndex])
|
|
let afterClose = String(component[component.index(after: closeIndex)...])
|
|
|
|
// 소괄호 안의 텍스트 (특별 스타일)
|
|
result = result + Text("(\(beforeClose))")
|
|
.font(.system(size: 16, design: .default).italic())
|
|
.foregroundColor(Color(hex: "e2e2e2").opacity(0.49))
|
|
|
|
// 소괄호 뒤의 텍스트 (일반 스타일)
|
|
if !afterClose.isEmpty {
|
|
result = result + Text(afterClose)
|
|
.font(.custom(Font.preRegular.rawValue, size: 16))
|
|
.foregroundColor(.white)
|
|
}
|
|
} else {
|
|
// 닫는 괄호가 없으면 일반 텍스트로 처리
|
|
result = result + Text("(\(component)")
|
|
.font(.custom(Font.preRegular.rawValue, size: 16))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
AiMessageItemView(
|
|
message: ServerChatMessage(
|
|
messageId: 1,
|
|
message: "",
|
|
profileImageUrl: "https://example.com/profile.jpg",
|
|
mine: false,
|
|
createdAt: Date().currentTimeMillis(),
|
|
messageType: "IMAGE",
|
|
imageUrl: "https://picsum.photos/1000",
|
|
price: nil,
|
|
hasAccess: true
|
|
),
|
|
characterName: "보라",
|
|
purchaseMessage: {}
|
|
)
|
|
.padding()
|
|
.background(Color.black)
|
|
}
|