feat(chat-room) 채팅방

- 텍스트 메시지 UI 적용
This commit is contained in:
Yu Sung
2025-09-03 23:14:26 +09:00
parent a42edbe99c
commit 1ec22717cb
4 changed files with 530 additions and 4 deletions

View File

@@ -6,13 +6,176 @@
//
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
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
HStack(alignment: .bottom, spacing: 4) {
//
HStack(alignment: .top, spacing: 9) {
//
KFImage(URL(string: message.profileImageUrl))
.placeholder {
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
}
.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)
}
//
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()
AiMessageItemView(
message: ServerChatMessage(
messageId: 1,
message: "(언제부턴가) 너랑 노는게 제일 재밌고\n너랑 이야기 하는게 제일 신나더라,\n앞으로도 그럴 것 같아❤️",
profileImageUrl: "https://example.com/profile.jpg",
mine: false,
createdAt: Date().currentTimeMillis(),
messageType: "text",
imageUrl: nil,
price: nil,
hasAccess: true
),
characterName: "보라"
)
.padding()
.background(Color.black)
}

View File

@@ -7,12 +7,152 @@
import SwiftUI
struct UserMessageBubbleShape: Shape {
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// ( , 16px )
path.move(to: CGPoint(x: 16, y: 0))
// ( 4px )
path.addLine(to: CGPoint(x: rect.width - 4, y: 0))
// (4px )
path.addArc(withCenter: CGPoint(x: rect.width - 4, y: 4),
radius: 4,
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)
// ( 16px )
path.addLine(to: CGPoint(x: 0, y: 16))
// (16px )
path.addArc(withCenter: CGPoint(x: 16, y: 16),
radius: 16,
startAngle: CGFloat.pi,
endAngle: -CGFloat.pi / 2,
clockwise: true)
path.close()
return Path(path.cgPath)
}
}
struct UserMessageItemView: View {
let message: ServerChatMessage
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
HStack(alignment: .bottom, spacing: 4) {
Spacer()
//
VStack {
Text(formatTime(from: message.createdAt))
.font(.custom(Font.preRegular.rawValue, size: 10))
.foregroundColor(.white)
}
//
HStack(spacing: 9) {
VStack(alignment: .trailing, spacing: 4) {
//
HStack(spacing: 10) {
styledMessageText(message.message)
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(Color.button)
.clipShape(UserMessageBubbleShape())
}
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
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: "333333"))
// ( )
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 {
UserMessageItemView()
UserMessageItemView(
message: ServerChatMessage(
messageId: 1,
message: "(만약에) 멈춰 인프피",
profileImageUrl: "",
mine: true,
createdAt: Date().currentTimeMillis(),
messageType: "text",
imageUrl: nil,
price: nil,
hasAccess: true
)
)
.padding()
.background(Color.black)
}