// // 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[..