From 49e2487617513d13f6832c1b30981243302c5e33 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Wed, 1 Apr 2026 15:36:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EA=B7=B8=EB=A3=B91=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20=EB=AC=B8=EA=B5=AC=EB=A5=BC=20I18n=20?= =?UTF-8?q?=ED=82=A4=EB=A1=9C=20=ED=86=B5=EC=9D=BC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../All/ByTheme/ContentAllByThemeView.swift | 10 +- .../ByTheme/ContentAllByThemeViewModel.swift | 4 +- .../Sources/Content/All/ContentAllView.swift | 10 +- .../Content/All/ContentNewAllItemView.swift | 2 +- .../Content/All/ContentNewAllView.swift | 8 +- .../Content/All/ContentRankingAllView.swift | 8 +- .../All/ContentRankingAllViewModel.swift | 8 +- .../Category/ContentListCategoryView.swift | 4 +- .../Sources/Content/ContentListItemView.swift | 12 +-- SodaLive/Sources/I18n/I18n.swift | 91 +++++++++++++++++++ docs/20260331_하드코딩텍스트_I18n통일계획.md | 57 ++++++++++-- 11 files changed, 173 insertions(+), 41 deletions(-) diff --git a/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift b/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift index 4eca240..427669b 100644 --- a/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift +++ b/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift @@ -22,7 +22,7 @@ struct ContentAllByThemeView: View { HStack(spacing: 13.3) { Spacer() - Text("최신순") + Text(I18n.Content.Sort.newest) .appFont(size: 13.3, weight: .medium) .foregroundColor( Color(hex: "e2e2e2") @@ -34,7 +34,7 @@ struct ContentAllByThemeView: View { } } - Text("높은 가격순") + Text(I18n.Content.Sort.priceHigh) .appFont(size: 13.3, weight: .medium) .foregroundColor( Color(hex: "e2e2e2") @@ -46,7 +46,7 @@ struct ContentAllByThemeView: View { } } - Text("낮은 가격순") + Text(I18n.Content.Sort.priceLow) .appFont(size: 13.3, weight: .medium) .foregroundColor( Color(hex: "e2e2e2") @@ -64,7 +64,7 @@ struct ContentAllByThemeView: View { .padding(.top, 13.3) HStack(spacing: 0) { - Text("전체") + Text(I18n.Content.Count.totalPrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "e2e2e2")) @@ -73,7 +73,7 @@ struct ContentAllByThemeView: View { .foregroundColor(Color(hex: "ff5c49")) .padding(.leading, 8) - Text("개") + Text(I18n.Content.Count.countUnit) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "e2e2e2")) .padding(.leading, 2) diff --git a/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift b/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift index 46a47ed..18c7826 100644 --- a/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift +++ b/SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift @@ -78,13 +78,13 @@ final class ContentAllByThemeViewModel: 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/Content/All/ContentAllView.swift b/SodaLive/Sources/Content/All/ContentAllView.swift index af31be3..c69f8f1 100644 --- a/SodaLive/Sources/Content/All/ContentAllView.swift +++ b/SodaLive/Sources/Content/All/ContentAllView.swift @@ -19,7 +19,11 @@ struct ContentAllView: View { Group { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체")) + DetailNavigationBar( + title: isFree ? + I18n.Content.All.freeTitle : + isPointAvailableOnly ? I18n.Content.All.pointRentalTitle : I18n.Content.All.title + ) if !viewModel.themeList.isEmpty { ContentMainContentThemeView( @@ -32,7 +36,7 @@ struct ContentAllView: View { HStack(spacing: 12) { Spacer() - Text("최신순") + Text(I18n.Content.Sort.newest) .appFont(size: 16, weight: .medium) .foregroundColor( Color(hex: "e2e2e2") @@ -44,7 +48,7 @@ struct ContentAllView: View { } } - Text("인기순") + Text(I18n.Content.Sort.popularity) .appFont(size: 16, weight: .medium) .foregroundColor( Color(hex: "e2e2e2") diff --git a/SodaLive/Sources/Content/All/ContentNewAllItemView.swift b/SodaLive/Sources/Content/All/ContentNewAllItemView.swift index aa4d4bd..7e59581 100644 --- a/SodaLive/Sources/Content/All/ContentNewAllItemView.swift +++ b/SodaLive/Sources/Content/All/ContentNewAllItemView.swift @@ -41,7 +41,7 @@ struct ContentNewAllItemView: View { .appFont(size: 8.5, weight: .medium) .foregroundColor(Color.white) } else { - Text("무료") + Text(I18n.CreateContent.free) .appFont(size: 8.5, weight: .medium) .foregroundColor(Color.white) } diff --git a/SodaLive/Sources/Content/All/ContentNewAllView.swift b/SodaLive/Sources/Content/All/ContentNewAllView.swift index 149cc3a..2c7952c 100644 --- a/SodaLive/Sources/Content/All/ContentNewAllView.swift +++ b/SodaLive/Sources/Content/All/ContentNewAllView.swift @@ -18,9 +18,9 @@ struct ContentNewAllView: View { Group { BaseView(isLoading: $viewModel.isLoading) { VStack(alignment: .leading, spacing: 13.3) { - DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠") + DetailNavigationBar(title: isFree ? I18n.Content.New.freeTitle : I18n.Content.New.title) - Text("※ 최근 2주간 등록된 새로운 콘텐츠 입니다.") + Text(I18n.Content.New.recentTwoWeeksNotice) .appFont(size: 14.7, weight: .medium) .foregroundColor(.graybb) .padding(.horizontal, 13.3) @@ -37,7 +37,7 @@ struct ContentNewAllView: View { ) HStack(spacing: 0) { - Text("전체") + Text(I18n.Content.Count.totalPrefix) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "e2e2e2")) @@ -46,7 +46,7 @@ struct ContentNewAllView: View { .foregroundColor(Color(hex: "ff5c49")) .padding(.leading, 8) - Text("개") + Text(I18n.Content.Count.countUnit) .appFont(size: 13.3, weight: .medium) .foregroundColor(Color(hex: "e2e2e2")) .padding(.leading, 2) diff --git a/SodaLive/Sources/Content/All/ContentRankingAllView.swift b/SodaLive/Sources/Content/All/ContentRankingAllView.swift index 90bae75..a63dfa1 100644 --- a/SodaLive/Sources/Content/All/ContentRankingAllView.swift +++ b/SodaLive/Sources/Content/All/ContentRankingAllView.swift @@ -17,14 +17,14 @@ struct ContentRankingAllView: View { Group { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { - DetailNavigationBar(title: "인기 콘텐츠") + DetailNavigationBar(title: I18n.Content.Ranking.title) VStack(spacing: 8) { Text("\(viewModel.dateString)") .appFont(size: 14.7, weight: .bold) .foregroundColor(Color(hex: "eeeeee")) - Text("※ 인기 콘텐츠의 순위는 매주 업데이트됩니다.") + Text(I18n.Content.Ranking.weeklyUpdateNotice) .appFont(size: 13.3, weight: .light) .foregroundColor(Color(hex: "bbbbbb")) } @@ -82,7 +82,7 @@ struct ContentRankingAllView: View { .cornerRadius(2.6) if item.isPointAvailable { - Text("포인트") + Text(I18n.Common.points) .appFont(size: 8, weight: .medium) .foregroundColor(.white) .padding(2.6) @@ -116,7 +116,7 @@ struct ContentRankingAllView: View { .foregroundColor(Color(hex: "909090")) } } else { - Text("무료") + Text(I18n.CreateContent.free) .appFont(size: 12, weight: .medium) .foregroundColor(Color(hex: "ffffff")) .padding(.horizontal, 5.3) diff --git a/SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift b/SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift index 032292b..bcb6a7d 100644 --- a/SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift +++ b/SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift @@ -71,13 +71,13 @@ final class ContentRankingAllViewModel: 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 self.isLoading = false } @@ -109,13 +109,13 @@ final class ContentRankingAllViewModel: 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/Content/Category/ContentListCategoryView.swift b/SodaLive/Sources/Content/Category/ContentListCategoryView.swift index b2253b0..0d58cf4 100644 --- a/SodaLive/Sources/Content/Category/ContentListCategoryView.swift +++ b/SodaLive/Sources/Content/Category/ContentListCategoryView.swift @@ -49,11 +49,11 @@ struct ContentListCategoryView: View { ContentListCategoryView( categoryList: [ - GetCategoryListResponse(categoryId: 0, category: "전체"), + GetCategoryListResponse(categoryId: 0, category: I18n.Category.all), GetCategoryListResponse(categoryId: 1, category: "test"), GetCategoryListResponse(categoryId: 0, category: "test2") ], selectCategory: { _ in }, - selectedCategory: .constant("전체") + selectedCategory: .constant(I18n.Category.all) ) } diff --git a/SodaLive/Sources/Content/ContentListItemView.swift b/SodaLive/Sources/Content/ContentListItemView.swift index 156313d..c01bfda 100644 --- a/SodaLive/Sources/Content/ContentListItemView.swift +++ b/SodaLive/Sources/Content/ContentListItemView.swift @@ -29,7 +29,7 @@ struct ContentListItemView: View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { if item.isScheduledToOpen { - Text("오픈예정") + Text(I18n.Common.openScheduled) .appFont(size: 11, weight: .medium) .foregroundColor(Color(hex: "3bb9f1")) .padding(2.6) @@ -52,7 +52,7 @@ struct ContentListItemView: View { .cornerRadius(2.6) if item.isPointAvailable { - Text("포인트") + Text(I18n.Common.points) .appFont(size: 11, weight: .medium) .foregroundColor(.white) .padding(2.6) @@ -98,7 +98,7 @@ struct ContentListItemView: View { Spacer() if item.isOwned { - Text("소장중") + Text(I18n.Content.Status.owned) .appFont(size: 14, weight: .medium) .foregroundColor(Color.gray11) .padding(.horizontal, 5.3) @@ -106,7 +106,7 @@ struct ContentListItemView: View { .background(Color(hex: "b1ef2c")) .cornerRadius(2.6) } else if item.isRented { - Text("대여중") + Text(I18n.Content.Status.rented) .appFont(size: 14, weight: .medium) .foregroundColor(Color.white) .padding(.horizontal, 5.3) @@ -114,7 +114,7 @@ struct ContentListItemView: View { .background(Color(hex: "660fd4")) .cornerRadius(2.6) } else if item.isSoldOut { - Text("Sold Out") + Text(I18n.Content.Status.soldOut) .appFont(size: 14, weight: .medium) .foregroundColor(Color.grayd2) .padding(.horizontal, 5.3) @@ -135,7 +135,7 @@ struct ContentListItemView: View { .foregroundColor(.white) } } else { - Text("무료") + Text(I18n.CreateContent.free) .appFont(size: 14, weight: .medium) .foregroundColor(.white) } diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 9352a5d..5702295 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -434,6 +434,97 @@ enum I18n { pick(ko: "재생목록", en: "Playlists", ja: "プレイリスト") } } + + enum Content { + enum All { + static var title: String { + pick(ko: "콘텐츠 전체", en: "All content", ja: "コンテンツ全体") + } + + static var freeTitle: String { + pick(ko: "무료 콘텐츠 전체", en: "All free content", ja: "無料コンテンツ全体") + } + + static var pointRentalTitle: String { + pick(ko: "포인트 대여 전체", en: "All point-rental content", ja: "ポイントレンタル全体") + } + } + + enum New { + static var title: String { + pick(ko: "최신 콘텐츠", en: "Latest content", ja: "最新コンテンツ") + } + + static var freeTitle: String { + pick(ko: "최신 무료 콘텐츠", en: "Latest free content", ja: "最新無料コンテンツ") + } + + static var recentTwoWeeksNotice: String { + pick( + ko: "※ 최근 2주간 등록된 새로운 콘텐츠 입니다.", + en: "※ New content registered in the last 2 weeks.", + ja: "※ 最近2週間で登録された新しいコンテンツです。" + ) + } + } + + enum Ranking { + static var title: String { + pick(ko: "인기 콘텐츠", en: "Popular content", ja: "人気コンテンツ") + } + + static var weeklyUpdateNotice: String { + pick( + ko: "※ 인기 콘텐츠의 순위는 매주 업데이트됩니다.", + en: "※ Popular content rankings are updated weekly.", + ja: "※ 人気コンテンツの順位は毎週更新されます。" + ) + } + } + + enum Sort { + static var newest: String { + pick(ko: "최신순", en: "Newest", ja: "新着順") + } + + static var popularity: String { + pick(ko: "인기순", en: "Most popular", ja: "人気順") + } + + static var priceHigh: String { + pick(ko: "높은 가격순", en: "Highest price", ja: "価格の高い順") + } + + static var priceLow: String { + pick(ko: "낮은 가격순", en: "Lowest price", ja: "価格の低い順") + } + } + + enum Count { + static var totalPrefix: String { + pick(ko: "전체", en: "Total", ja: "全体") + } + + static var countUnit: String { + pick(ko: "개", en: "", ja: "件") + } + } + + enum Status { + static var owned: String { + pick(ko: "소장중", en: "Owned", ja: "所持中") + } + + static var rented: String { + pick(ko: "대여중", en: "Renting", ja: "レンタル中") + } + + static var soldOut: String { + pick(ko: "Sold Out", en: "Sold out", ja: "売り切れ") + } + } + } + enum CharacterDetailGallery { static var purchaseConfirmTitle: String { pick(ko: "구매 확인", en: "Confirmation", ja: "購入確認") diff --git a/docs/20260331_하드코딩텍스트_I18n통일계획.md b/docs/20260331_하드코딩텍스트_I18n통일계획.md index e35fc81..e9baf46 100644 --- a/docs/20260331_하드코딩텍스트_I18n통일계획.md +++ b/docs/20260331_하드코딩텍스트_I18n통일계획.md @@ -141,16 +141,16 @@ ### Content (78) #### Group 1 (1-10) -- [ ] `SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift` -- [ ] `SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift` -- [ ] `SodaLive/Sources/Content/All/ContentAllView.swift` -- [ ] `SodaLive/Sources/Content/All/ContentNewAllItemView.swift` -- [ ] `SodaLive/Sources/Content/All/ContentNewAllView.swift` -- [ ] `SodaLive/Sources/Content/All/ContentRankingAllView.swift` -- [ ] `SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift` -- [ ] `SodaLive/Sources/Content/Category/ContentListCategoryView.swift` -- [ ] `SodaLive/Sources/Content/ContentItemView.swift` -- [ ] `SodaLive/Sources/Content/ContentListItemView.swift` +- [x] `SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift` +- [x] `SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeViewModel.swift` +- [x] `SodaLive/Sources/Content/All/ContentAllView.swift` +- [x] `SodaLive/Sources/Content/All/ContentNewAllItemView.swift` +- [x] `SodaLive/Sources/Content/All/ContentNewAllView.swift` +- [x] `SodaLive/Sources/Content/All/ContentRankingAllView.swift` +- [x] `SodaLive/Sources/Content/All/ContentRankingAllViewModel.swift` +- [x] `SodaLive/Sources/Content/Category/ContentListCategoryView.swift` +- [x] `SodaLive/Sources/Content/ContentItemView.swift` +- [x] `SodaLive/Sources/Content/ContentListItemView.swift` #### Group 2 (11-20) - [ ] `SodaLive/Sources/Content/ContentListView.swift` @@ -975,3 +975,40 @@ - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약). - LSP 진단 참고: SourceKit 단독 해석에서 외부 모듈/프로젝트 심볼(`Moya`, `Kingfisher`, `I18n` 등) 미해결 오류가 보고되었으나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증했다. + +### 21차 구현 (Content 모듈 Group 1, 10개 파일 처리, 2026-04-01) +- 무엇/왜/어떻게: + - 무엇: `변경 대상 파일 전체 목록`의 `Content` Group 1(10개 파일)을 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환했다. + - 왜: 콘텐츠 전체/신규/랭킹/테마별 목록 구간에 하드코딩 문자열, `String(localized:)` 직접 참조, ViewModel 공통 오류 문구가 혼재되어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문이다. + - 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search`/`lsp_symbols` 직접 점검으로 치환 대상을 확정하고, `I18n.swift`에 `I18n.Content` 네임스페이스를 추가한 뒤 Group 1 호출부를 교체했다. +- 실행 명령/도구: + - `task(subagent_type="explore", ...)` x2 (`bg_65648347`, `bg_d4b726f6`) + - `task(subagent_type="librarian", ...)` x2 (`bg_c8e277d6`, `bg_a66e0329`) + - `background_output(task_id=...)` x4 (위 4개 task 결과 수집) + - `grep("\"[^\"]*[가-힣][^\"]*\"", include=Group1 대상 파일)` + - `grep("String\\(localized:|NSLocalizedString\\(|LocalizedStringKey\\(", include=Group1 대상 파일)` + - `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Content])` + - `lsp_symbols(filePath=I18n.swift, scope=document, query=Content)` + - `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`에 `I18n.Content` 키셋 추가: + - `All(title/freeTitle/pointRentalTitle)` + - `New(title/freeTitle/recentTwoWeeksNotice)` + - `Ranking(title/weeklyUpdateNotice)` + - `Sort(newest/popularity/priceHigh/priceLow)` + - `Count(totalPrefix/countUnit)` + - `Status(owned/rented/soldOut)` + - 치환 완료 파일(실치환 9개): + - `ContentAllByThemeView.swift`, `ContentAllByThemeViewModel.swift`, `ContentAllView.swift`, `ContentNewAllItemView.swift`, `ContentNewAllView.swift`, `ContentRankingAllView.swift`, `ContentRankingAllViewModel.swift`, `ContentListCategoryView.swift`, `ContentListItemView.swift` + - 점검만 수행(실치환 없음, 체크 완료 1개): + - `ContentItemView.swift` (런타임 하드코딩 문구 없음, Preview 샘플 문자열만 존재) + - Group 1 체크박스 10개 `- [x]` 완료 반영. + - Group 1 재탐지 결과, 남은 한글 리터럴은 `ContentRankingAllViewModel.swift`의 API 정렬 파라미터 기본값(`"매출"`)과 Preview 샘플(`ContentItemView.swift`, `ContentListItemView.swift`)만 존재. + - `ContentAllView.swift`의 `String(localized:)` 직접 참조(내비게이션 타이틀)를 `I18n.Content.All.*`로 전환해 호출 경로를 통일. + - LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Kingfisher`, `BaseView`, `I18n` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료. + - 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`). + - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약).