diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 4c32144..9317c4b 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -2400,6 +2400,72 @@ If you block this user, the following features will be restricted. } enum Voice { + enum SavePopup { + static var title: String { + pick(ko: "메시지 보관", en: "Archive message", ja: "メッセージを保管") + } + + static func description(_ canCount: Int) -> String { + pick( + ko: "메시지를 보관하는데\n\(canCount)캔이 필요합니다.\n메시지를 보관하시겠습니까?", + en: "Archiving this message requires\n\(canCount) cans.\nDo you want to archive this message?", + ja: "メッセージを保管するには\n\(canCount)can が必要です。\nこのメッセージを保管しますか?" + ) + } + + static var notice: String { + pick( + ko: "※ 메시지 보관시, 본인이 삭제하기 전까지 영구보관됩니다.", + en: "※ Archived messages are kept permanently until you delete them.", + ja: "※ 保管したメッセージは、ご自身で削除するまで永久に保管されます。" + ) + } + } + + enum Write { + static var title: String { + pick(ko: "음성메시지", en: "Voice message", ja: "音声メッセージ") + } + + static var recipientLabel: String { + pick(ko: "TO.", en: "TO.", ja: "宛先") + } + + static var sendButton: String { + pick(ko: "메시지 보내기", en: "Send message", ja: "メッセージを送信") + } + + static var selectRecipient: String { + pick(ko: "받는 사람을 선택해 주세요.", en: "Select a recipient.", ja: "受信者を選択してください。") + } + + static var sendSuccess: String { + pick(ko: "메시지 전송이 완료되었습니다.", en: "Your message has been sent.", ja: "メッセージの送信が完了しました。") + } + + static var sendFailed: String { + pick( + ko: "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", + en: "Could not send the voice message.\nPlease try again.\nIf the issue persists, contact customer support.", + ja: "音声メッセージを送信できませんでした。\nもう一度お試しください。\n問題が続く場合はカスタマーサポートにお問い合わせください。" + ) + } + + static var reRecord: String { + pick(ko: "다시 녹음", en: "Record again", ja: "再録音") + } + } + + enum Toast { + static var saveFailed: String { + pick( + ko: "메시지를 저장하지 못했습니다\n잠시 후 다시 시도해 주세요.", + en: "Could not save the message.\nPlease try again later.", + ja: "メッセージを保存できませんでした。\nしばらくしてからもう一度お試しください。" + ) + } + } + enum Sound { static var permissionDenied: String { pick( diff --git a/SodaLive/Sources/Message/Voice/VoiceMessageView.swift b/SodaLive/Sources/Message/Voice/VoiceMessageView.swift index ddfc7bb..7ea96b4 100644 --- a/SodaLive/Sources/Message/Voice/VoiceMessageView.swift +++ b/SodaLive/Sources/Message/Voice/VoiceMessageView.swift @@ -37,7 +37,7 @@ struct VoiceMessageView: View { viewModel.selectedMessageId = item.messageId soundManager.stopAudio() if item.isKept { - viewModel.errorMessage = "이미 보관된 메시지 입니다" + viewModel.errorMessage = I18n.Message.Text.Detail.alreadyKept viewModel.isShowPopup = true return } else { @@ -76,7 +76,7 @@ struct VoiceMessageView: View { .resizable() .frame(width: 60, height: 60) - Text("메시지가 없습니다.\n친구들과 소통해보세요!") + Text(I18n.Message.Text.emptyState) .multilineTextAlignment(.center) .appFont(size: 10.7, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) @@ -114,18 +114,18 @@ struct VoiceMessageView: View { .ignoresSafeArea() VStack(spacing: 0) { - Text("메시지 보관") + Text(I18n.Message.Voice.SavePopup.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "bbbbbb")) .padding(.top, 40) - Text("메시지를 보관하는데\n\(viewModel.saveMessagePrice)캔이 필요합니다.\n메시지를 보관하시겠습니까?") + Text(I18n.Message.Voice.SavePopup.description(viewModel.saveMessagePrice)) .appFont(size: 15, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .padding(.top, 13.3) .multilineTextAlignment(.center) - Text("※ 메시지 보관시, 본인이 삭제하기 전까지 영구보관됩니다.") + Text(I18n.Message.Voice.SavePopup.notice) .appFont(size: 12, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .padding(.top, 13.3) @@ -133,7 +133,7 @@ struct VoiceMessageView: View { .multilineTextAlignment(.center) HStack(spacing: 13.3) { - Text("취소") + Text(I18n.Common.cancel) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "3bb9f1")) .padding(.vertical, 16) @@ -151,7 +151,7 @@ struct VoiceMessageView: View { viewModel.isShowSavePopup = false } - Text("확인") + Text(I18n.Common.confirm) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift b/SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift index 4f83d6c..fd991c4 100644 --- a/SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift +++ b/SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift @@ -32,7 +32,7 @@ final class VoiceMessageViewModel: ObservableObject { @Published var recipientNickname: String = "" @Published var recipientId = 0 - @Published var sendText = "메시지 보내기" + @Published var sendText = I18n.Message.Voice.Write.sendButton @Published var selectedMessageId = -1 @Published var openPlayerItemIndex = -1 @@ -73,7 +73,7 @@ final class VoiceMessageViewModel: ObservableObject { func deleteMessage() { if selectedMessageId <= 0 { - errorMessage = "메시지를 삭제하지 못했습니다\n잠시 후 다시 시도해 주세요." + errorMessage = I18n.Message.Text.Detail.deleteFailed isShowPopup = true return } @@ -97,7 +97,7 @@ final class VoiceMessageViewModel: ObservableObject { let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { - self.errorMessage = "삭제되었습니다." + self.errorMessage = I18n.Message.Text.Detail.deleteSuccess self.isShowPopup = true self.refresh() } else { @@ -105,13 +105,13 @@ final class VoiceMessageViewModel: ObservableObject { self.errorMessage = message self.isShowPopup = true } else { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed } self.isShowPopup = true } } catch { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed self.isShowPopup = true } } @@ -120,7 +120,7 @@ final class VoiceMessageViewModel: ObservableObject { func keepVoiceMessage() { if selectedMessageId <= 0 { - errorMessage = "메시지를 저장하지 못했습니다\n잠시 후 다시 시도해 주세요." + errorMessage = I18n.Message.Voice.Toast.saveFailed isShowPopup = true return } @@ -143,7 +143,7 @@ final class VoiceMessageViewModel: ObservableObject { let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { - self.errorMessage = "보관되었습니다." + self.errorMessage = I18n.Message.Text.Detail.keepSuccess self.isShowPopup = true self.refresh() } else { @@ -151,13 +151,13 @@ final class VoiceMessageViewModel: ObservableObject { self.errorMessage = message self.isShowPopup = true } else { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed } self.isShowPopup = true } } catch { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed self.isShowPopup = true } } @@ -166,7 +166,7 @@ final class VoiceMessageViewModel: ObservableObject { func keepTextMessage() { if selectedMessageId <= 0 { - errorMessage = "메시지를 저장하지 못했습니다\n잠시 후 다시 시도해 주세요." + errorMessage = I18n.Message.Voice.Toast.saveFailed isShowPopup = true return } @@ -189,7 +189,7 @@ final class VoiceMessageViewModel: ObservableObject { let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { - self.errorMessage = "보관되었습니다." + self.errorMessage = I18n.Message.Text.Detail.keepSuccess self.isShowPopup = true self.refresh() } else { @@ -197,13 +197,13 @@ final class VoiceMessageViewModel: ObservableObject { self.errorMessage = message self.isShowPopup = true } else { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed } self.isShowPopup = true } } catch { - self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요." + self.errorMessage = I18n.Message.Text.Detail.keepFailed self.isShowPopup = true } } @@ -212,7 +212,7 @@ final class VoiceMessageViewModel: ObservableObject { func write(soundData: Data, onSuccess: @escaping () -> Void) { if recipientId <= 0 { - errorMessage = "받는 사람을 선택해 주세요." + errorMessage = I18n.Message.Voice.Write.selectRecipient isShowPopup = true return } @@ -254,7 +254,7 @@ final class VoiceMessageViewModel: ObservableObject { if decoded.success { onSuccess() - self.errorMessage = "메시지 전송이 완료되었습니다." + self.errorMessage = I18n.Message.Voice.Write.sendSuccess self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 1) { AppState.shared.back() @@ -263,19 +263,19 @@ final class VoiceMessageViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Message.Voice.Write.sendFailed } self.isShowPopup = true } } catch { - self.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Message.Voice.Write.sendFailed self.isShowPopup = true } } .store(in: &subscription) } else { - self.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Message.Voice.Write.sendFailed self.isShowPopup = true self.isLoading = false } @@ -317,13 +317,13 @@ final class VoiceMessageViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } @@ -365,13 +365,13 @@ final class VoiceMessageViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } @@ -413,13 +413,13 @@ final class VoiceMessageViewModel: ObservableObject { if let message = decoded.message { self.errorMessage = message } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } diff --git a/SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift b/SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift index 4fc0c33..f690370 100644 --- a/SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift +++ b/SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift @@ -35,7 +35,7 @@ struct VoiceMessageWriteView: View { Spacer() VStack(spacing: 0) { HStack(spacing: 0) { - Text("음성메시지") + Text(I18n.Message.Voice.Write.title) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) @@ -57,14 +57,14 @@ struct VoiceMessageWriteView: View { .frame(width: 46.7, height: 46.7) VStack(alignment: .leading, spacing: 10) { - Text("TO.") + Text(I18n.Message.Voice.Write.recipientLabel) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) Text( viewModel.recipientNickname.count > 0 ? viewModel.recipientNickname : - I18n.TextMessage.recipientPlaceholder + I18n.Message.Text.Write.recipientLabel ) .appFont(size: 16.7, weight: viewModel.recipientNickname.count > 0 ? .bold : .light) .foregroundColor( @@ -111,7 +111,7 @@ struct VoiceMessageWriteView: View { .padding(.vertical, 52.3) .onTapGesture { if viewModel.recipientId <= 0 { - viewModel.errorMessage = "받는 사람을 선택해 주세요." + viewModel.errorMessage = I18n.Message.Voice.Write.selectRecipient viewModel.isShowPopup = true } else { progress = 0 @@ -129,7 +129,7 @@ struct VoiceMessageWriteView: View { HStack(spacing: 0) { Spacer() - Text("삭제") + Text(I18n.Common.delete) .appFont(size: 15.3, weight: .medium) .foregroundColor(Color(hex: "bbbbbb").opacity(0)) @@ -151,7 +151,7 @@ struct VoiceMessageWriteView: View { Spacer() - Text("삭제") + Text(I18n.Common.delete) .appFont(size: 15.3, weight: .medium) .foregroundColor(Color(hex: "bbbbbb")) .onTapGesture { @@ -165,7 +165,7 @@ struct VoiceMessageWriteView: View { .padding(.top, 90) HStack(spacing: 13.3) { - Text("다시 녹음") + Text(I18n.Message.Voice.Write.reRecord) .appFont(size: 18.3, weight: .bold) .foregroundColor(Color(hex: "9970ff")) .frame(width: (proxy.size.width - 40) / 3, height: 50) @@ -195,7 +195,7 @@ struct VoiceMessageWriteView: View { onRefresh() } } catch { - viewModel.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + viewModel.errorMessage = I18n.Message.Voice.Write.sendFailed viewModel.isShowPopup = true } } diff --git a/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index 235d6b1..0513965 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -405,9 +405,9 @@ - [x] `SodaLive/Sources/Message/Voice/VoiceMessageItemView.swift` #### Group 2 (11-13) -- [ ] `SodaLive/Sources/Message/Voice/VoiceMessageView.swift` -- [ ] `SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift` -- [ ] `SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift` +- [x] `SodaLive/Sources/Message/Voice/VoiceMessageView.swift` +- [x] `SodaLive/Sources/Message/Voice/VoiceMessageViewModel.swift` +- [x] `SodaLive/Sources/Message/Voice/Write/VoiceMessageWriteView.swift` ### MyPage (41) #### Group 1 (1-10) @@ -775,3 +775,30 @@ - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). - LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Kingfisher`, `MessageRepository` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료. + +### 14차 구현 (Message 모듈 Group 2, 3개 파일 처리, 2026-03-31) +- 무엇/왜/어떻게: + - 무엇: `변경 대상 파일 전체 목록`의 `Message` Group 2(3개 파일)에서 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환하고 체크박스를 완료 처리. + - 왜: Voice 메시지 목록/보관 팝업/작성 화면/ViewModel 토스트 문구가 하드코딩 상태라 Message 모듈의 i18n 접근이 Group 1과 불일치했기 때문. + - 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search`/`rg` 직접 점검으로 대상 문자열을 확정하고, `I18n.swift`의 `I18n.Message.Voice` 네임스페이스를 확장한 뒤 호출부를 치환. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_4384335d`, `bg_fe76ec47`) + - `task(subagent_type="librarian", ...)` x2 (`bg_da2d810f`, `bg_2416c47e`) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Message/Voice)` + - `grep("I18n\\.Message\\.", include=*.swift, path=SodaLive/Sources/Message)` + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Message/Voice])` + - `bash: rg -n ...` (`command not found` 확인) + - `lsp_diagnostics(filePath=변경 파일 4개)` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` +- 결과: + - `I18n.swift`에 `I18n.Message.Voice.SavePopup`, `I18n.Message.Voice.Write`, `I18n.Message.Voice.Toast` 키셋 추가. + - 치환 완료 파일: `VoiceMessageView.swift`, `VoiceMessageViewModel.swift`, `VoiceMessageWriteView.swift`. + - Voice 보관 팝업(제목/본문/안내/버튼), 작성 화면(타이틀/수신자 라벨/다시 녹음/삭제), ViewModel 토스트/성공·실패 문구를 `I18n.*` 참조로 교체. + - 대상 3개 파일 재탐지 결과 한글 하드코딩 리터럴 0건 확인. + - `Message` Group 2 체크박스 3개 `- [x]` 완료 반영. + - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). + - LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Moya`, `I18n`, `LoadingView` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.