From 4c170e0f979edb2f7f5320c300aba972643230a7 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 31 Mar 2026 19:46:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=EC=84=A4=EC=A0=95=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=EB=A5=BC=20I18n=20=ED=82=A4=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/I18n/I18n.swift | 245 ++++++++++++++++++ .../Content/ContentSettingsView.swift | 10 +- .../Settings/Event/EventDetailView.swift | 4 +- .../Settings/Event/EventListView.swift | 2 +- .../Settings/Event/EventListViewModel.swift | 4 +- .../Language/Models/LanguageOption.swift | 8 +- .../Language/Views/LanguageSettingsView.swift | 4 +- .../Settings/Notice/NoticeDetailView.swift | 2 +- .../Settings/Notice/NoticeListView.swift | 2 +- .../Settings/Notice/NoticeListViewModel.swift | 4 +- .../NotificationSettingsDialog.swift | 8 +- .../NotificationSettingsView.swift | 26 +- .../NotificationSettingsViewModel.swift | 4 +- SodaLive/Sources/Settings/SettingsView.swift | 44 ++-- .../Settings/SignOut/SignOutView.swift | 14 +- .../Settings/Terms/TermsViewModel.swift | 8 +- docs/20260331_하드코딩텍스트_I18n통일계획.md | 59 +++-- 17 files changed, 354 insertions(+), 94 deletions(-) diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index f1eb64b..19289b7 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -1108,6 +1108,96 @@ enum I18n { // 설정 > 공통 및 회원탈퇴(SignOut) enum Settings { + static var title: String { + pick(ko: "설정", en: "Settings", ja: "設定") + } + + static var notificationSettings: String { + pick(ko: "알림 설정", en: "Notification settings", ja: "通知設定") + } + + static var languageSettings: String { + pick(ko: "언어 설정", en: "Language settings", ja: "言語設定") + } + + static var contentViewSettings: String { + pick(ko: "콘텐츠 보기 설정", en: "Content view settings", ja: "コンテンツ表示設定") + } + + static var termsOfService: String { + pick(ko: "이용약관", en: "Terms of service", ja: "利用規約") + } + + static var privacyPolicy: String { + pick(ko: "개인정보처리방침", en: "Privacy policy", ja: "プライバシーポリシー") + } + + static var appVersionInfo: String { + pick(ko: "앱 버전 정보", en: "App version info", ja: "アプリバージョン情報") + } + + static var logout: String { + pick(ko: "로그아웃", en: "Log out", ja: "ログアウト") + } + + static var logoutAllDevices: String { + pick(ko: "모든 기기에서 로그아웃", en: "Log out from all devices", ja: "すべての端末でログアウト") + } + + static var signOut: String { + pick(ko: "회원탈퇴", en: "Delete account", ja: "退会") + } + + static var companyInfo: String { + pick( + ko: """ +- 회사명 : 주식회사 소다라이브 + +- 대표자 : 이재형 + +- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호 + +- 사업자등록번호 : 870-81-03220 + +- 통신판매업신고 : 제2024-성남분당B-1012호 + +- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00) + +- 대표 이메일 : sodalive.official@gmail.com +""", + en: """ +- Company: SodaLive Co., Ltd. + +- CEO: Jaehyung Lee + +- Address: 5F 563A, 10, Hwangsaeul-ro335beon-gil, Bundang-gu, Seongnam-si, Gyeonggi-do + +- Business Registration No.: 870-81-03220 + +- Mail-order business report No.: 2024-SeongnamBundangB-1012 + +- Customer center: +82-2-2055-1477 (Hours 10:00~19:00) + +- Email: sodalive.official@gmail.com +""", + ja: """ +- 会社名:株式会社SodaLive + +- 代表者:イ・ジェヒョン + +- 住所:京畿道 城南市 盆唐区 黄새울路335番ギル10、5階563A号 + +- 事業者登録番号:870-81-03220 + +- 通信販売業届出:第2024-城南盆唐B-1012号 + +- カスタマーセンター:02.2055.1477(利用時間 10:00~19:00) + +- 代表メール:sodalive.official@gmail.com +""" + ) + } + // 알림 다이얼로그 타이틀 static var alertTitle: String { pick(ko: "알림", en: "Notice", ja: "お知らせ") } @@ -1152,7 +1242,162 @@ enum I18n { ja: "すべてのデバイスからログアウトしますか?" ) } + + enum Content { + static var sensitiveContentTitle: String { + pick(ko: "민감한 콘텐츠 보기", en: "Show sensitive content", ja: "センシティブなコンテンツ表示") + } + + static var all: String { + pick(ko: "전체", en: "All", ja: "全体") + } + + static var maleOriented: String { + pick(ko: "남성향", en: "Male-oriented", ja: "男性向け") + } + + static var femaleOriented: String { + pick(ko: "여성향", en: "Female-oriented", ja: "女性向け") + } + } + + enum Event { + static var title: String { + pick(ko: "이벤트", en: "Events", ja: "イベント") + } + + static var detailTitle: String { + pick(ko: "이벤트 상세", en: "Event details", ja: "イベント詳細") + } + + static var participateButton: String { + pick(ko: "이벤트 참여하기", en: "Participate in event", ja: "イベントに参加する") + } + } + + enum Language { + static var systemDefault: String { + pick(ko: "시스템 기본", en: "System default", ja: "システム設定") + } + + static var korean: String { + pick(ko: "한국어", en: "한국어", ja: "한국어") + } + + static var english: String { + pick(ko: "English", en: "English", ja: "English") + } + + static var japanese: String { + pick(ko: "日本語", en: "日本語", ja: "日本語") + } + + static var apply: String { + pick(ko: "적용", en: "Apply", ja: "適用") + } + } + + enum Notice { + static var title: String { + pick(ko: "공지사항", en: "Notices", ja: "お知らせ") + } + + static var detailTitle: String { + pick(ko: "공지사항 상세", en: "Notice details", ja: "お知らせ詳細") + } + } + + enum Notification { + static var title: String { + pick(ko: "알림 설정", en: "Notification settings", ja: "通知設定") + } + + static var receiveSettingsTitle: String { + pick(ko: "알림 수신 설정", en: "Notification receive settings", ja: "通知受信設定") + } + + static var serviceNotifications: String { + pick(ko: "서비스 알림", en: "Service notifications", ja: "サービス通知") + } + + static var live: String { + pick(ko: "라이브 알림", en: "Live notifications", ja: "ライブ通知") + } + + static var contentUpload: String { + pick(ko: "콘텐츠 업로드 알림", en: "Content upload notifications", ja: "コンテンツアップロード通知") + } + + static var message: String { + pick(ko: "메시지 알림", en: "Message notifications", ja: "メッセージ通知") + } + + static var confirm: String { + pick(ko: "확인", en: "Confirm", ja: "確認") + } + + static var followingChannels: String { + pick(ko: "팔로잉 채널", en: "Following channels", ja: "フォロー中のチャンネル") + } + + static var totalPrefix: String { + pick(ko: "총", en: "Total", ja: "合計") + } + + static var countUnit: String { + pick(ko: "명", en: "", ja: "名") + } + + static var noFollowingChannels: String { + pick(ko: "팔로우 중인 채널이 없습니다.", en: "There are no followed channels.", ja: "フォロー中のチャンネルがありません。") + } + } + enum SignOut { + static var title: String { + pick(ko: "회원탈퇴", en: "Delete account", ja: "退会") + } + + static var headline: String { + pick( + ko: "정말로 탈퇴하실 거에요?\n한 번 더 생각해보지 않으실래요?", + en: "Are you sure you want to leave?\nWould you like to think one more time?", + ja: "本当に退会しますか?\nもう一度ご検討いただけませんか?" + ) + } + + static var reasonGuide: String { + pick( + ko: "계정을 삭제하려는 이유를 선택해주세요.\n서비스 개선에 중요한 자료로 활용하겠습니다.", + en: "Please select the reason for deleting your account.\nThis will be used as important feedback to improve our service.", + ja: "アカウント削除の理由を選択してください。\nサービス改善のための重要な資料として活用します。" + ) + } + + static var reasonInputPlaceholder: String { + pick(ko: "입력해주세요", en: "Please enter", ja: "入力してください") + } + + static var accountDeletionNotice: String { + pick( + ko: "계정을 삭제하면 회원님의 모든 콘텐츠와 활동 길고, 캔충전 및 적립, 사용내역 등의 기록이 삭제됩니다. 삭제된 정보는 복구할 수 없으니 신중히 결정해주세요.\n캔 충전하기를 통해 적립한 캔은 계정 삭제시 환불이 불가합니다. 또한 환불 신청 후 환불처리가 되기 전에 계정을 삭제하는 경우 포인트 사용내역을 확인할 수 없어 환불이 불가합니다.", + en: "If you delete your account, all your content and activity records, including can charge/savings and usage history, will be deleted. Deleted information cannot be recovered, so please decide carefully.\nCans earned through can charging are not refundable when deleting your account. Also, if you delete your account before a requested refund is processed, a refund is not possible because point usage history cannot be confirmed.", + ja: "アカウントを削除すると、すべてのコンテンツと活動履歴、canチャージおよび積立、利用履歴などが削除されます。削除された情報は復元できないため、慎重にご判断ください。\ncanチャージで積み立てたcanはアカウント削除時に返金できません。また、返金申請後に返金処理前にアカウントを削除した場合、ポイント利用履歴を確認できないため返金できません。" + ) + } + + static var socialLoginGuide: String { + pick( + ko: "※ 소셜 로그인 이용자는 비밀번호를 입력하지 말고 '탈퇴하기'를 클릭하면 자동 탈퇴됩니다.", + en: "※ If you use social login, do not enter a password and click 'Delete account' to complete withdrawal automatically.", + ja: "※ ソーシャルログイン利用者はパスワードを入力せず、「退会する」を押すと自動で退会されます。" + ) + } + + static var submit: String { + pick(ko: "탈퇴하기", en: "Delete account", ja: "退会する") + } + // 탈퇴 사유 목록 (UI에서 그대로 배열로 사용 가능) static var reasons: [String] { [ diff --git a/SodaLive/Sources/Settings/Content/ContentSettingsView.swift b/SodaLive/Sources/Settings/Content/ContentSettingsView.swift index 03f55dc..4792e5e 100644 --- a/SodaLive/Sources/Settings/Content/ContentSettingsView.swift +++ b/SodaLive/Sources/Settings/Content/ContentSettingsView.swift @@ -18,7 +18,7 @@ struct ContentSettingsView: View { Color.black.ignoresSafeArea() VStack(spacing: 0) { - DetailNavigationBar(title: "콘텐츠 보기 설정") { + DetailNavigationBar(title: I18n.Settings.contentViewSettings) { if AppState.shared.isRestartApp { AppState.shared.setAppStep(step: .splash) } else { @@ -29,7 +29,7 @@ struct ContentSettingsView: View { ScrollView(.vertical) { VStack(spacing: 0) { HStack(spacing: 0) { - Text("민감한 콘텐츠 보기") + Text(I18n.Settings.Content.sensitiveContentTitle) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -59,7 +59,7 @@ struct ContentSettingsView: View { .resizable() .frame(width: 20, height: 20) - Text("전체") + Text(I18n.Settings.Content.all) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) } @@ -79,7 +79,7 @@ struct ContentSettingsView: View { .resizable() .frame(width: 20, height: 20) - Text("남성향") + Text(I18n.Settings.Content.maleOriented) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) } @@ -99,7 +99,7 @@ struct ContentSettingsView: View { .resizable() .frame(width: 20, height: 20) - Text("여성향") + Text(I18n.Settings.Content.femaleOriented) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) } diff --git a/SodaLive/Sources/Settings/Event/EventDetailView.swift b/SodaLive/Sources/Settings/Event/EventDetailView.swift index 83ded69..ebc184a 100644 --- a/SodaLive/Sources/Settings/Event/EventDetailView.swift +++ b/SodaLive/Sources/Settings/Event/EventDetailView.swift @@ -16,7 +16,7 @@ struct EventDetailView: View { BaseView { GeometryReader { proxy in VStack(spacing: 0) { - DetailNavigationBar(title: "이벤트 상세") + DetailNavigationBar(title: I18n.Settings.Event.detailTitle) ScrollView(.vertical, showsIndicators: false) { KFImage(URL(string: event.detailImageUrl!)) @@ -28,7 +28,7 @@ struct EventDetailView: View { Spacer() if let link = event.link, link.count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { - Text("이벤트 참여하기") + Text(I18n.Settings.Event.participateButton) .appFont(size: 18.3, weight: .bold) .foregroundColor(.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Settings/Event/EventListView.swift b/SodaLive/Sources/Settings/Event/EventListView.swift index 70b8535..7db0200 100644 --- a/SodaLive/Sources/Settings/Event/EventListView.swift +++ b/SodaLive/Sources/Settings/Event/EventListView.swift @@ -15,7 +15,7 @@ struct EventListView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "이벤트") + DetailNavigationBar(title: I18n.Settings.Event.title) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 15) { diff --git a/SodaLive/Sources/Settings/Event/EventListViewModel.swift b/SodaLive/Sources/Settings/Event/EventListViewModel.swift index 872c92a..bbf351a 100644 --- a/SodaLive/Sources/Settings/Event/EventListViewModel.swift +++ b/SodaLive/Sources/Settings/Event/EventListViewModel.swift @@ -45,13 +45,13 @@ final class EventListViewModel: 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/Settings/Language/Models/LanguageOption.swift b/SodaLive/Sources/Settings/Language/Models/LanguageOption.swift index d5c6f8a..d35d1c7 100644 --- a/SodaLive/Sources/Settings/Language/Models/LanguageOption.swift +++ b/SodaLive/Sources/Settings/Language/Models/LanguageOption.swift @@ -15,10 +15,10 @@ enum LanguageOption: String, CaseIterable, Equatable { var displayName: String { switch self { - case .system: return String(localized: "시스템 기본") - case .ko: return "한국어" - case .en: return "English" - case .ja: return "日本語" + case .system: return I18n.Settings.Language.systemDefault + case .ko: return I18n.Settings.Language.korean + case .en: return I18n.Settings.Language.english + case .ja: return I18n.Settings.Language.japanese } } diff --git a/SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift b/SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift index 37043f1..921fb34 100644 --- a/SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift +++ b/SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift @@ -17,7 +17,7 @@ struct LanguageSettingsView: View { Color.black.ignoresSafeArea() VStack(spacing: 0) { - DetailNavigationBar(title: String(localized: "언어 설정")) + DetailNavigationBar(title: I18n.Settings.languageSettings) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { @@ -58,7 +58,7 @@ struct LanguageSettingsView: View { Button(action: { Task { await viewModel.applyAndRestart() } }) { - Text("적용") + Text(I18n.Settings.Language.apply) .appFont(size: 16, weight: .bold) .frame(width: cardWidth, height: 50) .background(Color.button) diff --git a/SodaLive/Sources/Settings/Notice/NoticeDetailView.swift b/SodaLive/Sources/Settings/Notice/NoticeDetailView.swift index 665c948..59e21f5 100644 --- a/SodaLive/Sources/Settings/Notice/NoticeDetailView.swift +++ b/SodaLive/Sources/Settings/Notice/NoticeDetailView.swift @@ -15,7 +15,7 @@ struct NoticeDetailView: View { var body: some View { BaseView { VStack(spacing: 0) { - DetailNavigationBar(title: String(localized: "공지사항 상세")) + DetailNavigationBar(title: I18n.Settings.Notice.detailTitle) VStack(alignment: .leading, spacing: 6.7) { Text(notice.title) diff --git a/SodaLive/Sources/Settings/Notice/NoticeListView.swift b/SodaLive/Sources/Settings/Notice/NoticeListView.swift index ca3c5dc..893bd52 100644 --- a/SodaLive/Sources/Settings/Notice/NoticeListView.swift +++ b/SodaLive/Sources/Settings/Notice/NoticeListView.swift @@ -14,7 +14,7 @@ struct NoticeListView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "공지사항") + DetailNavigationBar(title: I18n.Settings.Notice.title) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { diff --git a/SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift b/SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift index cbcaecf..d90f9ff 100644 --- a/SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift +++ b/SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift @@ -44,13 +44,13 @@ final class NoticeListViewModel: 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/Settings/Notification/NotificationSettingsDialog.swift b/SodaLive/Sources/Settings/Notification/NotificationSettingsDialog.swift index 95d1631..0237add 100644 --- a/SodaLive/Sources/Settings/Notification/NotificationSettingsDialog.swift +++ b/SodaLive/Sources/Settings/Notification/NotificationSettingsDialog.swift @@ -20,7 +20,7 @@ struct NotificationSettingsDialog: View { VStack(spacing: 0) { HStack(spacing: 0) { - Text("라이브 알림") + Text(I18n.Settings.Notification.live) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -40,7 +40,7 @@ struct NotificationSettingsDialog: View { .padding(.vertical, 13) HStack(spacing: 0) { - Text("콘텐츠 업로드 알림") + Text(I18n.Settings.Notification.contentUpload) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -60,7 +60,7 @@ struct NotificationSettingsDialog: View { .padding(.vertical, 13) HStack(spacing: 0) { - Text("메시지 알림") + Text(I18n.Settings.Notification.message) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -74,7 +74,7 @@ struct NotificationSettingsDialog: View { } } - Text("확인") + Text(I18n.Settings.Notification.confirm) .appFont(size: 15.3, weight: .bold) .foregroundColor(Color(hex: "ffffff")) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift b/SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift index c794000..ccc95e5 100644 --- a/SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift +++ b/SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift @@ -14,12 +14,12 @@ struct NotificationSettingsView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "알림 설정") + DetailNavigationBar(title: I18n.Settings.Notification.title) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { HStack(spacing: 0) { - Text("라이브 알림") + Text(I18n.Settings.Notification.live) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -39,7 +39,7 @@ struct NotificationSettingsView: View { .foregroundColor(Color(hex: "909090").opacity(0.3)) HStack(spacing: 0) { - Text("콘텐츠 업로드 알림") + Text(I18n.Settings.Notification.contentUpload) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -59,7 +59,7 @@ struct NotificationSettingsView: View { .foregroundColor(Color(hex: "909090").opacity(0.3)) HStack(spacing: 0) { - Text("메시지 알림") + Text(I18n.Settings.Notification.message) .appFont(size: 15, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) @@ -108,11 +108,11 @@ struct NotificationReceiveSettingsView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "알림 수신 설정") + DetailNavigationBar(title: I18n.Settings.Notification.receiveSettingsTitle) ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - Text("서비스 알림") + Text(I18n.Settings.Notification.serviceNotifications) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) .padding(.top, 26.7) @@ -120,7 +120,7 @@ struct NotificationReceiveSettingsView: View { VStack(spacing: 0) { NotificationReceiveServiceRowView( - title: "라이브 알림", + title: I18n.Settings.Notification.live, isOn: viewModel.followingChannelLive, onTapToggle: { viewModel.followingChannelLive.toggle() @@ -132,7 +132,7 @@ struct NotificationReceiveSettingsView: View { .foregroundColor(Color(hex: "909090").opacity(0.3)) NotificationReceiveServiceRowView( - title: "콘텐츠 업로드 알림", + title: I18n.Settings.Notification.contentUpload, isOn: viewModel.followingChannelUploadContent, onTapToggle: { viewModel.followingChannelUploadContent.toggle() @@ -144,7 +144,7 @@ struct NotificationReceiveSettingsView: View { .foregroundColor(Color(hex: "909090").opacity(0.3)) NotificationReceiveServiceRowView( - title: "메시지 알림", + title: I18n.Settings.Notification.message, isOn: viewModel.message, onTapToggle: { viewModel.message.toggle() @@ -158,14 +158,14 @@ struct NotificationReceiveSettingsView: View { .padding(.top, 13.3) .padding(.horizontal, 13.3) - Text("팔로잉 채널") + Text(I18n.Settings.Notification.followingChannels) .appFont(size: 16.7, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) .padding(.top, 26.7) .padding(.horizontal, 13.3) HStack(spacing: 0) { - Text("총") + Text(I18n.Settings.Notification.totalPrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) @@ -173,7 +173,7 @@ struct NotificationReceiveSettingsView: View { .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.mainRed3) - Text("명") + Text(I18n.Settings.Notification.countUnit) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) @@ -212,7 +212,7 @@ struct NotificationReceiveSettingsView: View { } .padding(.top, 13.3) } else { - Text("팔로우 중인 채널이 없습니다.") + Text(I18n.Settings.Notification.noFollowingChannels) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color.grayee) .padding(.top, 13.3) diff --git a/SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift b/SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift index 389f834..58ebd2e 100644 --- a/SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift +++ b/SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift @@ -59,13 +59,13 @@ final class NotificationSettingsViewModel: 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/Settings/SettingsView.swift b/SodaLive/Sources/Settings/SettingsView.swift index ad1ee3a..a96f2d2 100644 --- a/SodaLive/Sources/Settings/SettingsView.swift +++ b/SodaLive/Sources/Settings/SettingsView.swift @@ -26,13 +26,13 @@ struct SettingsView: View { BaseView(isLoading: $viewModel.isLoading) { GeometryReader { geo in VStack(spacing: 0) { - DetailNavigationBar(title: String(localized: "설정")) + DetailNavigationBar(title: I18n.Settings.title) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { VStack(spacing: 0) { HStack(spacing: 0) { - Text("알림 설정") + Text(I18n.Settings.notificationSettings) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -54,7 +54,7 @@ struct SettingsView: View { .foregroundColor(Color.gray90) HStack(spacing: 0) { - Text("언어 설정") + Text(I18n.Settings.languageSettings) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -77,7 +77,7 @@ struct SettingsView: View { .foregroundColor(Color.gray90) HStack(spacing: 0) { - Text("콘텐츠 보기 설정") + Text(I18n.Settings.contentViewSettings) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -103,7 +103,7 @@ struct SettingsView: View { VStack(spacing: 0) { HStack(spacing: 0) { - Text("이용약관") + Text(I18n.Settings.termsOfService) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -125,7 +125,7 @@ struct SettingsView: View { .foregroundColor(Color.gray90) HStack(spacing: 0) { - Text("개인정보처리방침") + Text(I18n.Settings.privacyPolicy) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -149,7 +149,7 @@ struct SettingsView: View { .padding(.top, 13.3) HStack(spacing: 0) { - Text("앱 버전 정보") + Text(I18n.Settings.appVersionInfo) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) @@ -167,27 +167,13 @@ struct SettingsView: View { .cornerRadius(6.7) .padding(.top, 13.3) - Text(""" -- 회사명 : 주식회사 소다라이브 - -- 대표자 : 이재형 - -- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호 - -- 사업자등록번호 : 870-81-03220 - -- 통신판매업신고 : 제2024-성남분당B-1012호 - -- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00) - -- 대표 이메일 : sodalive.official@gmail.com -""") - .appFont(size: 11, weight: .medium) - .foregroundColor(Color.gray77) - .frame(width: cardWidth, alignment: .leading) - .padding(.top, 13.3) + Text(I18n.Settings.companyInfo) + .appFont(size: 11, weight: .medium) + .foregroundColor(Color.gray77) + .frame(width: cardWidth, alignment: .leading) + .padding(.top, 13.3) - Text("로그아웃") + Text(I18n.Settings.logout) .appFont(size: 14.7, weight: .bold) .foregroundColor(Color.grayee) .frame(width: cardWidth, height: 50) @@ -198,7 +184,7 @@ struct SettingsView: View { } .padding(.top, 46.7) - Text("모든 기기에서 로그아웃") + Text(I18n.Settings.logoutAllDevices) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color.gray77) .padding(.top, 13.3) @@ -206,7 +192,7 @@ struct SettingsView: View { isShowLogoutAllDeviceDialog = true } - Text("회원탈퇴") + Text(I18n.Settings.signOut) .appFont(size: 14.7, weight: .medium) .foregroundColor(Color.gray77) .underline() diff --git a/SodaLive/Sources/Settings/SignOut/SignOutView.swift b/SodaLive/Sources/Settings/SignOut/SignOutView.swift index 2f4089c..d203dd6 100644 --- a/SodaLive/Sources/Settings/SignOut/SignOutView.swift +++ b/SodaLive/Sources/Settings/SignOut/SignOutView.swift @@ -14,17 +14,17 @@ struct SignOutView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "회원탈퇴") + DetailNavigationBar(title: I18n.Settings.SignOut.title) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { VStack(spacing: 13.3) { - Text("정말로 탈퇴하실 거에요?\n한 번 더 생각해보지 않으실래요?") + Text(I18n.Settings.SignOut.headline) .appFont(size: 20, weight: .bold) .foregroundColor(Color(hex: "3bb9f1")) .frame(width: screenSize().width - 26.7, alignment: .leading) - Text("계정을 삭제하려는 이유를 선택해주세요.\n서비스 개선에 중요한 자료로 활용하겠습니다.") + Text(I18n.Settings.SignOut.reasonGuide) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "eeeeee")) .frame(width: screenSize().width - 26.7, alignment: .leading) @@ -48,7 +48,7 @@ struct SignOutView: View { if index == viewModel.reasons.count - 1 { VStack(spacing: 6.7) { - TextField("입력해주세요", text: $viewModel.reason) + TextField(I18n.Settings.SignOut.reasonInputPlaceholder, text: $viewModel.reason) .autocapitalization(.none) .disableAutocorrection(true) .appFont(size: 13.3, weight: .medium) @@ -74,7 +74,7 @@ struct SignOutView: View { .foregroundColor(Color(hex: "232323")) .padding(.top, 20) - Text("계정을 삭제하면 회원님의 모든 콘텐츠와 활동 길고, 캔충전 및 적립, 사용내역 등의 기록이 삭제됩니다. 삭제된 정보는 복구할 수 없으니 신중히 결정해주세요.\n캔 충전하기를 통해 적립한 캔은 계정 삭제시 환불이 불가합니다. 또한 환불 신청 후 환불처리가 되기 전에 계정을 삭제하는 경우 포인트 사용내역을 확인할 수 없어 환불이 불가합니다.") + Text(I18n.Settings.SignOut.accountDeletionNotice) .appFont(size: 12, weight: .medium) .foregroundColor(Color(hex: "ff5c49")) .fixedSize(horizontal: false, vertical: true) @@ -91,14 +91,14 @@ struct SignOutView: View { .padding(.top, 20) .padding(.horizontal, 26.7) - Text("※ 소셜 로그인 이용자는 비밀번호를 입력하지 말고 '탈퇴하기'를 클릭하면 자동 탈퇴됩니다.") + Text(I18n.Settings.SignOut.socialLoginGuide) .appFont(size: 12, weight: .medium) .foregroundColor(.grayee) .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 26.7) .padding(.top, 8) - Text("탈퇴하기") + Text(I18n.Settings.SignOut.submit) .appFont(size: 15, weight: .bold) .foregroundColor(.white) .padding(.vertical, 16) diff --git a/SodaLive/Sources/Settings/Terms/TermsViewModel.swift b/SodaLive/Sources/Settings/Terms/TermsViewModel.swift index a30761e..2e3682d 100644 --- a/SodaLive/Sources/Settings/Terms/TermsViewModel.swift +++ b/SodaLive/Sources/Settings/Terms/TermsViewModel.swift @@ -45,13 +45,13 @@ final class TermsViewModel: 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 } } @@ -84,13 +84,13 @@ final class TermsViewModel: 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/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index 649b6a1..3d47c43 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -476,23 +476,23 @@ ### Settings (15) #### Group 1 (1-10) -- [ ] `SodaLive/Sources/Settings/Content/ContentSettingsView.swift` -- [ ] `SodaLive/Sources/Settings/Event/EventDetailView.swift` -- [ ] `SodaLive/Sources/Settings/Event/EventListView.swift` -- [ ] `SodaLive/Sources/Settings/Event/EventListViewModel.swift` -- [ ] `SodaLive/Sources/Settings/Language/Models/LanguageOption.swift` -- [ ] `SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift` -- [ ] `SodaLive/Sources/Settings/Notice/NoticeDetailView.swift` -- [ ] `SodaLive/Sources/Settings/Notice/NoticeListView.swift` -- [ ] `SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift` -- [ ] `SodaLive/Sources/Settings/Notification/NotificationSettingsDialog.swift` +- [x] `SodaLive/Sources/Settings/Content/ContentSettingsView.swift` +- [x] `SodaLive/Sources/Settings/Event/EventDetailView.swift` +- [x] `SodaLive/Sources/Settings/Event/EventListView.swift` +- [x] `SodaLive/Sources/Settings/Event/EventListViewModel.swift` +- [x] `SodaLive/Sources/Settings/Language/Models/LanguageOption.swift` +- [x] `SodaLive/Sources/Settings/Language/Views/LanguageSettingsView.swift` +- [x] `SodaLive/Sources/Settings/Notice/NoticeDetailView.swift` +- [x] `SodaLive/Sources/Settings/Notice/NoticeListView.swift` +- [x] `SodaLive/Sources/Settings/Notice/NoticeListViewModel.swift` +- [x] `SodaLive/Sources/Settings/Notification/NotificationSettingsDialog.swift` #### Group 2 (11-15) -- [ ] `SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift` -- [ ] `SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift` -- [ ] `SodaLive/Sources/Settings/SettingsView.swift` -- [ ] `SodaLive/Sources/Settings/SignOut/SignOutView.swift` -- [ ] `SodaLive/Sources/Settings/Terms/TermsViewModel.swift` +- [x] `SodaLive/Sources/Settings/Notification/NotificationSettingsView.swift` +- [x] `SodaLive/Sources/Settings/Notification/NotificationSettingsViewModel.swift` +- [x] `SodaLive/Sources/Settings/SettingsView.swift` +- [x] `SodaLive/Sources/Settings/SignOut/SignOutView.swift` +- [x] `SodaLive/Sources/Settings/Terms/TermsViewModel.swift` ### UI (6) #### Group 1 (1-6) @@ -717,3 +717,32 @@ - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). - LSP 진단: `No such module 'Kingfisher'` 1건 확인. 해당 모듈 해석은 SourceKit 인덱싱 환경 제약으로 재현되며, 동일 파일은 실제 `xcodebuild`에서 컴파일 성공 확인. + +### 12차 구현 (Settings 모듈 Group 1~2, 15개 파일 처리, 2026-03-31) +- 무엇/왜/어떻게: + - 무엇: `변경 대상 파일 전체 목록`의 `Settings` Group 1~2(15개 파일)에서 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환하고 체크박스를 완료 처리. + - 왜: `String(localized:)`/하드코딩 리터럴/중복 오류 메시지가 혼재되어 Settings 모듈의 i18n 접근이 일관되지 않았기 때문. + - 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search`/`rg` 직접 점검으로 대상 문자열을 확정한 뒤, `I18n.swift`의 `I18n.Settings` 하위 네임스페이스를 확장하고 호출부를 일괄 치환. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_021e5287`, `bg_d081115c`) + - `task(subagent_type="librarian", ...)` x2 (`bg_3dc24f38`, `bg_938d63ee`) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Settings)` + - `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=SodaLive/Sources/Settings)` + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Settings])` + - `bash: rg -n ... SodaLive/Sources/Settings` (`command not found` 확인) + - `lsp_diagnostics(filePath=변경 파일 전체)` + - `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`에 Settings 전용 키셋 추가/확장: + - 루트: `title`, `notificationSettings`, `languageSettings`, `contentViewSettings`, `termsOfService`, `privacyPolicy`, `appVersionInfo`, `logout`, `logoutAllDevices`, `signOut`, `companyInfo` + - 하위: `I18n.Settings.Content`, `I18n.Settings.Event`, `I18n.Settings.Language`, `I18n.Settings.Notice`, `I18n.Settings.Notification`, `I18n.Settings.SignOut`(안내문/버튼/placeholder 추가) + - Settings Group 1~2 대상 15개 파일 치환 완료 및 문서 체크박스 15개 모두 `- [x]` 반영. + - ViewModel 공통 실패 문구 치환 완료: `EventListViewModel`, `NoticeListViewModel`, `NotificationSettingsViewModel`, `TermsViewModel` → `I18n.Common.commonError`. + - 재탐지 결과: Settings 모듈 내 한글 리터럴은 `NoticeDetailView` Preview 샘플 2건(`"제목"`, `"

콘텐츠

"`)만 잔존. + - `String(localized:)`/`NSLocalizedString`/`LocalizedStringKey` 직접 참조는 Settings 모듈에서 제거 확인. + - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약). + - LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/심볼(`Kingfisher`, `RichText`, 앱 내부 타입) 미해결 오류가 대량 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.