feat(chat): 채팅 모듈 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-03-31 16:30:48 +09:00
parent 222520d5e9
commit 47085dc1ca
27 changed files with 464 additions and 128 deletions

View File

@@ -33,7 +33,7 @@ struct CharacterItemView: View {
HStack {
Spacer()
Text("N")
Text(I18n.Chat.Character.newBadge)
.appFont(size: 18, weight: .regular)
.foregroundColor(.white)
.frame(width: 30, height: 30)

View File

@@ -8,7 +8,7 @@
import SwiftUI
struct CharacterSectionView: View {
let title: LocalizedStringResource
let title: String
let items: [Character]
let isShowRank: Bool
var trailingTitle: String? = nil
@@ -52,7 +52,7 @@ struct CharacterSectionView: View {
#Preview {
CharacterSectionView(
title: "신규 캐릭터",
title: I18n.Chat.Character.newSectionTitle,
items: [
Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300", isNew: true),
Character(characterId: 2, name: "데이지", description: "", imageUrl: "https://picsum.photos/300", isNew: false)

View File

@@ -39,7 +39,7 @@ struct CharacterView: View {
//
if !viewModel.popularCharacters.isEmpty {
CharacterSectionView(
title: "인기 캐릭터",
title: I18n.Chat.Character.popularSectionTitle,
items: viewModel.popularCharacters,
isShowRank: true,
onTap: { ch in
@@ -51,7 +51,7 @@ struct CharacterView: View {
//
if !viewModel.newCharacters.isEmpty {
CharacterSectionView(
title: "신규 캐릭터",
title: I18n.Chat.Character.newSectionTitle,
items: viewModel.newCharacters,
isShowRank: false,
trailingTitle: I18n.Common.viewAll,
@@ -67,7 +67,7 @@ struct CharacterView: View {
if !viewModel.recommendCharacters.isEmpty {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("추천 캐릭터")
Text(I18n.Chat.Character.recommendSectionTitle)
.appFont(size: 24, weight: .bold)
.foregroundColor(.white)

View File

@@ -54,7 +54,7 @@ final class CharacterViewModel: ObservableObject {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
}
self.isShowPopup = true
@@ -62,7 +62,7 @@ final class CharacterViewModel: ObservableObject {
self.isLoading = false
} catch {
self.isLoading = false
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
}
}
@@ -93,7 +93,7 @@ final class CharacterViewModel: ObservableObject {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
}
self.isShowPopup = true
@@ -101,11 +101,10 @@ final class CharacterViewModel: ObservableObject {
self.isLoading = false
} catch {
self.isLoading = false
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@@ -34,7 +34,7 @@ struct CharacterDetailView: View {
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: String(localized: "캐릭터 정보")) {
DetailNavigationBar(title: I18n.Chat.Character.detailTitle) {
if presentationMode.wrappedValue.isPresented {
presentationMode.wrappedValue.dismiss()
} else {
@@ -77,7 +77,7 @@ struct CharacterDetailView: View {
if let others = viewModel.characterDetail?.others, !others.isEmpty {
VStack(spacing: 16) {
HStack {
Text("장르의 다른 캐릭터")
Text(I18n.Chat.Character.detailOtherCharactersTitle)
.appFont(size: 26, weight: .bold)
.foregroundColor(.white)
@@ -178,6 +178,13 @@ extension CharacterDetailView {
// MARK: - Profile Section
extension CharacterDetailView {
private func isMaleGender(_ gender: String) -> Bool {
let normalizedGender = gender
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return normalizedGender == "남성" || normalizedGender == "male"
}
private var profileSection: some View {
VStack(alignment: .leading, spacing: 16) {
if viewModel.characterDetail?.mbti != nil ||
@@ -189,7 +196,7 @@ extension CharacterDetailView {
Text(viewModel.characterDetail?.translated?.gender ?? gender)
.appFont(size: 14, weight: .regular)
.foregroundColor(
gender == "남성" ?
isMaleGender(gender) ?
Color.button :
Color.mainRed
)
@@ -201,7 +208,7 @@ extension CharacterDetailView {
RoundedRectangle(cornerRadius: 4)
.stroke(lineWidth: 1)
.foregroundColor(
gender == "남성" ?
isMaleGender(gender) ?
Color.button :
Color.mainRed
)
@@ -209,7 +216,7 @@ extension CharacterDetailView {
}
if let age = viewModel.characterDetail?.age {
Text("\(age)")
Text(I18n.Chat.Character.age(age))
.appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5"))
.padding(.horizontal, 7)
@@ -252,7 +259,7 @@ extension CharacterDetailView {
if let characterType = viewModel.characterDetail?.characterType {
HStack(spacing: 8) {
Text(characterType.rawValue)
Text(characterType == .Clone ? I18n.Chat.Character.typeClone : I18n.Chat.Character.typeCharacter)
.appFont(size: 12, weight: .regular)
.foregroundColor(.white)
.padding(.horizontal, 5)
@@ -282,7 +289,7 @@ extension CharacterDetailView {
private func worldViewSection(backgrounds: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("[세계관 및 작품 소개]")
Text(I18n.Chat.Character.detailWorldViewTitle)
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
@@ -300,7 +307,7 @@ extension CharacterDetailView {
private func originalWorkSection(title: String, link: String) -> some View {
VStack(spacing: 8) {
HStack {
Text("원작")
Text(I18n.Chat.Character.detailOriginalTitle)
.appFont(size: 16, weight: .bold)
.fontWeight(.bold)
.foregroundColor(.white)
@@ -321,7 +328,7 @@ extension CharacterDetailView {
UIApplication.shared.open(url)
}
}) {
Text("원작 보러가기")
Text(I18n.Chat.Character.detailOriginalLinkButton)
.appFont(size: 16, weight: .bold)
.fontWeight(.bold)
.foregroundColor(Color(hex: "3BB9F1"))
@@ -342,7 +349,7 @@ extension CharacterDetailView {
private func personalitySection(personalities: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("[성격 및 특징]")
Text(I18n.Chat.Character.detailPersonalityTitle)
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
@@ -354,24 +361,19 @@ extension CharacterDetailView {
//
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("⚠️ 캐릭터톡 대화 가이드")
Text(I18n.Chat.Character.detailConversationGuideTitle)
.appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "B0BEC5"))
Spacer()
}
Text("""
보이스온의 오픈월드 캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다. 세계관 속 연관 캐릭터가 되어 대화를 하거나 완전히 새로운 인물이 되어 캐릭터와 당신만의 스토리를 만들어 갈 수 있습니다.
""")
Text(I18n.Chat.Character.detailConversationGuideDescription1)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "AEAEB2"))
.multilineTextAlignment(.leading)
Text("""
오픈월드 캐릭터톡은 캐릭터를 정교하게 설계하였지만, 대화가 어색하거나 불완전할 수도 있습니다.
대화 도중 캐릭터의 대화가 이상하거나 새로운 캐릭터로 대화를 나누고 싶다면 대화를 초기화 하고 새롭게 캐릭터와 대화를 나눠보세요.
""")
Text(I18n.Chat.Character.detailConversationGuideDescription2)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "AEAEB2"))
.multilineTextAlignment(.leading)
@@ -393,7 +395,7 @@ extension CharacterDetailView {
// MARK: - Chat Button
extension CharacterDetailView {
private var chatButton: some View {
Text("대화하기")
Text(I18n.Chat.Character.detailChatButton)
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
@@ -450,7 +452,7 @@ struct CharacterExpandableTextView: View {
.foregroundColor(Color(hex: "607D8B"))
.rotationEffect(.degrees(isExpanded ? 180 : 0))
Text(isExpanded ? "간략히" : "더보기")
Text(isExpanded ? I18n.Chat.Character.detailCollapse : I18n.Chat.Character.detailExpand)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "607D8B"))
}

View File

@@ -76,7 +76,7 @@ struct CharacterDetailGalleryView: View {
VStack(spacing: 8) {
// ( % , , )
HStack {
Text("\(viewModel.ownershipPercentage)% 보유중")
Text(I18n.Chat.Character.DetailGallery.ownership(viewModel.ownershipPercentage))
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
@@ -91,7 +91,7 @@ struct CharacterDetailGalleryView: View {
.appFont(size: 16, weight: .regular)
.foregroundColor(.white)
Text("\(viewModel.totalCount)")
Text(I18n.Chat.Character.DetailGallery.totalCount(viewModel.totalCount))
.appFont(size: 16, weight: .regular)
.foregroundColor(.white)
}

View File

@@ -68,7 +68,7 @@ final class NewCharacterListViewModel: ObservableObject {
} else {
self?.isLoading = false
}
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self?.errorMessage = I18n.Common.commonError
self?.isShowPopup = true
}
} receiveValue: { [weak self] response in
@@ -93,7 +93,7 @@ final class NewCharacterListViewModel: ObservableObject {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
}
self.isShowPopup = true
if isLoadMore {
@@ -108,7 +108,7 @@ final class NewCharacterListViewModel: ObservableObject {
} else {
self.isLoading = false
}
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
}
}

View File

@@ -17,18 +17,18 @@ struct NewCharacterListView: View {
Group { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 8) {
// Toolbar
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
DetailNavigationBar(title: I18n.Chat.Character.NewList.title)
VStack(alignment: .leading, spacing: 12) {
// n
HStack(spacing: 0) {
Text("전체")
Text(I18n.Chat.Character.NewList.totalPrefix)
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Text(" \(viewModel.totalCount)")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "ff5c49"))
Text("")
Text(I18n.Chat.Character.NewList.countUnit)
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Spacer()

View File

@@ -14,8 +14,8 @@ struct RecentCharacterSectionView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) {
Text("최근 대화한 캐릭터 ")
HStack(spacing: 4) {
Text(I18n.Chat.Character.recentSectionTitle)
.appFont(size: 20, weight: .bold)
.foregroundColor(.white)