feat(i18n): 콘텐츠 모듈 그룹1 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-04-01 15:36:39 +09:00
parent 038d66e363
commit 49e2487617
11 changed files with 173 additions and 41 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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)
)
}

View File

@@ -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)
}

View File

@@ -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: "購入確認")

View File

@@ -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 미구성 확인(코드 실패 아님, 스킴 제약).