Files
sodalive-ios/docs/20260331_하드코딩텍스트_I18n통일계획.md

1053 lines
83 KiB
Markdown

# 20260331 하드코딩 텍스트 I18n.swift 통일 계획
## 작업 체크리스트
- [x] 코드베이스 하드코딩 문구 탐색 전략 수립 (병렬 탐색 + 정적 검색)
- [x] `I18n.swift``Localizable.xcstrings` 혼재 지점 식별
- [x] 하드코딩 문구 변경 대상 파일 인벤토리 산출 및 문서화
- [ ] 1차 전환: SwiftUI/View 레이어 하드코딩 문구를 `I18n.*` 참조로 교체
- [ ] 2차 전환: ViewModel/Manager/Repository 레이어의 사용자 노출 문구를 `I18n.*`로 이관
- [ ] 3차 정리: `Localizable.xcstrings` 의존 경로 제거 및 회귀 점검
- [ ] 최종 검증: 빌드/테스트/수동 QA 완료 후 체크리스트 마감
## 커밋 전략 (권장)
- **권장 방식: 모듈별 커밋** (일괄 커밋 비권장)
- 이유:
- 변경량이 커서(현재 대상 330개) 일괄 커밋 시 리뷰/롤백/원인추적 비용이 급증함.
- 모듈별 커밋은 회귀가 발생해도 영향 범위를 모듈 단위로 제한할 수 있음.
- 충돌 해결 시에도 모듈 경계가 명확해 병합 리스크가 낮아짐.
- 권장 순서:
1. 공통 선행 커밋: `I18n.swift` 공통 키/네임스페이스 토대 정리
2. 모듈별 커밋: 각 모듈의 키 추가 + 호출부 교체 + 모듈 단위 검증
3. 마무리 커밋: `Localizable.xcstrings` 의존 제거/정리 및 최종 검증 결과 반영
## 수용 기준 (Acceptance Criteria)
- [ ] 사용자에게 노출되는 하드코딩 문자열이 `SodaLive/Sources/**`에 남아있지 않다.
- [ ] 신규/기존 UI 문구 접근이 `I18n.swift` 네임스페이스를 통해 일관되게 이루어진다.
- [ ] `Localizable.xcstrings` 기반 직접 참조(`LocalizedStringKey`/`String(localized:)`/`NSLocalizedString`)가 제거되거나 예외 사유가 문서화된다.
- [ ] 문자열 치환 후 `SodaLive`, `SodaLive-dev` Debug 빌드가 성공한다.
- [ ] 테스트 가능 범위(스킴 제약 감안)와 주요 화면 수동 QA 결과가 문서 하단 검증 기록에 누적된다.
## 탐지 기준 및 범위
- 범위: `SodaLive/Sources/**/*.swift`
- 기준:
- 문자열 리터럴 중 한글 포함(`"...가-힣..."`) 항목
- SwiftUI 하드코딩 패턴 (`Text("...")`, `Button("...")`, `placeholder: "..."`)
- 사용자 노출 가능 메시지 패턴 (`errorMessage = "..."`, `showToast(...)`, `SodaDialog(...)`)
- 제외 원칙(구현 단계에서 최종 필터링): 로그/디버그 전용 문자열, URL/키/식별자/이벤트명 등 비노출 텍스트
## 현황 스냅샷
- `I18n.` 사용 파일: 93개 (`grep: \bI18n\.`)
- String Catalog 계열 API 사용 파일: 21개 (`grep: NSLocalizedString|String(localized:)|LocalizedStringKey`)
- 하드코딩 후보 파일(자동 탐지 합집합): 360개
- 제외 확정: 비노출 문자열(로그/채널 prefix/API path/아이콘명/색상코드/Preview 샘플) 30개 파일
- 최종 변경 대상: 330개 파일
## 외부 레퍼런스 기반 전환 원칙 (librarian 반영)
- `LocalizedStringResource`를 로컬라이즈 문자열 표현의 기본 타입으로 사용한다. (`I18n.swift` accessor 반환 타입 설계 시 우선 고려)
- String Catalog(`.xcstrings`)와 레거시 포맷은 병존 가능하므로, 모듈 단위 점진 이관 전략을 사용한다.
- Swift 문자열 자동 추출 설정(`Use Compiler to Extract Swift Strings`)을 점검해 누락 리스크를 줄인다.
- 동적 키/런타임 생성 문자열은 자동 추출이 어려우므로, 수동 관리 대상(manual string)으로 별도 추적한다.
- 복수형(pluralization), 포맷 토큰(`%d`, `%@`)은 이관 시 깨지기 쉬운 구간이므로 변환 검증 체크포인트를 별도 둔다.
- 접근성 문자열(`accessibilityLabel`, `accessibilityHint`)도 i18n 적용 범위에 포함한다.
## 주요 리스크 체크포인트
- [ ] 포맷 토큰/인자 순서 불일치로 인한 문구 깨짐 여부 검증
- [ ] plural 규칙 손실 여부 검증
- [ ] 동적 문자열 누락 여부 검증(수동 관리 목록 대조)
- [ ] 접근성 레이블/힌트 미전환 누락 검증
## I18n 키 네이밍/네임스페이스 전략 (librarian 반영)
- 최상위 네임스페이스는 모듈 기준(`I18n.Live`, `I18n.Chat`, `I18n.MyPage`)으로 고정한다.
- 2차 네임스페이스는 화면/도메인 기준(`I18n.Live.Room`, `I18n.Chat.Talk`)으로 확장한다.
- 3차 네임스페이스는 컴포넌트/다이얼로그 기준(`I18n.Live.Room.Dialog`)으로 분리한다.
- 키 이름은 문구 원문이 아닌 역할 중심(`title`, `subtitle`, `confirmButton`, `emptyStateMessage`)으로 작성한다.
- 파라미터가 필요한 문구는 프로퍼티가 아닌 함수(`func rankTitle(_ count: Int) -> String`)로 선언한다.
- 중복 문구는 도메인 내부 중복 생성 대신 `I18n.Common`으로 승격해 재사용한다.
- 대규모 이관 단계에서는 키 rename을 금지하고, 화면 단위 교체 + 화면 단위 QA 순서로 진행한다.
## 변경 대상 파일 요약
- 총 대상 파일 수: 330
- 상위 모듈 수: 21
| 모듈 | 파일 수 |
|---|---:|
| `Audition` | 13 |
| `Chat` | 28 |
| `Content` | 78 |
| `CustomView` | 3 |
| `Dialog` | 6 |
| `Explorer` | 40 |
| `Follow` | 1 |
| `Home` | 9 |
| `IAP` | 1 |
| `ImagePicker` | 1 |
| `Live` | 56 |
| `Main` | 3 |
| `Message` | 13 |
| `MyPage` | 41 |
| `Notification` | 2 |
| `Onboarding` | 1 |
| `Report` | 4 |
| `SearchChannel` | 1 |
| `Settings` | 15 |
| `UI` | 6 |
| `User` | 8 |
## 변경 대상 파일 전체 목록
### Audition (13)
- [x] `SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift`
- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantItemView.swift`
- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift`
- [x] `SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift`
- [x] `SodaLive/Sources/Audition/AuditionItemView.swift`
- [x] `SodaLive/Sources/Audition/AuditionView.swift`
- [x] `SodaLive/Sources/Audition/AuditionViewModel.swift`
- [x] `SodaLive/Sources/Audition/Detail/AuditionDetailView.swift`
- [x] `SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift`
- [x] `SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift`
- [x] `SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift`
- [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift`
- [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift`
### Chat (28)
- [x] `SodaLive/Sources/Chat/Character/CharacterItemView.swift`
- [x] `SodaLive/Sources/Chat/Character/CharacterSectionView.swift`
- [x] `SodaLive/Sources/Chat/Character/CharacterView.swift`
- [x] `SodaLive/Sources/Chat/Character/CharacterViewModel.swift`
- [x] `SodaLive/Sources/Chat/Character/Detail/CharacterDetailView.swift`
- [x] `SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift`
- [x] `SodaLive/Sources/Chat/Character/New/ViewModels/NewCharacterListViewModel.swift`
- [x] `SodaLive/Sources/Chat/Character/New/Views/NewCharacterListView.swift`
- [x] `SodaLive/Sources/Chat/Character/Recent/RecentCharacterItemView.swift`
- [x] `SodaLive/Sources/Chat/Character/Recent/RecentCharacterSectionView.swift`
- [x] `SodaLive/Sources/Chat/ChatTabView.swift`
- [x] `SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailHeaderView.swift`
- [x] `SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift`
- [x] `SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailViewModel.swift`
- [x] `SodaLive/Sources/Chat/Original/OriginalTabItemView.swift`
- [x] `SodaLive/Sources/Chat/Original/OriginalWorkViewModel.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/ChatRoomViewModel.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Message/AiMessageItemView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Message/TypingIndicatorItemView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Message/UserMessageItemView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Quota/ChatQuotaNoticeItemView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Settings/ChatBgSelectionView.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Settings/ChatBgSelectionViewModel.swift`
- [x] `SodaLive/Sources/Chat/Talk/Room/Settings/ChatSettingsView.swift`
- [x] `SodaLive/Sources/Chat/Talk/TalkItemView.swift`
- [x] `SodaLive/Sources/Chat/Talk/TalkView.swift`
- [x] `SodaLive/Sources/Chat/Talk/TalkViewModel.swift`
### Content (78)
#### Group 1 (1-10)
- [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)
- [x] `SodaLive/Sources/Content/ContentListView.swift`
- [x] `SodaLive/Sources/Content/ContentPlayManager.swift`
- [x] `SodaLive/Sources/Content/ContentRepository.swift`
- [x] `SodaLive/Sources/Content/Create/ContentCreateSelectThemeView.swift`
- [x] `SodaLive/Sources/Content/Create/ContentCreateSelectThemeViewModel.swift`
- [x] `SodaLive/Sources/Content/Create/ContentCreateView.swift`
- [x] `SodaLive/Sources/Content/Create/ContentCreateViewModel.swift`
- [x] `SodaLive/Sources/Content/Create/QuarterTimePickerView.swift`
- [x] `SodaLive/Sources/Content/Create/SelectDatePicker.swift`
- [x] `SodaLive/Sources/Content/Curation/ContentCurationView.swift`
#### Group 3 (21-30)
- [ ] `SodaLive/Sources/Content/Curation/ContentCurationViewModel.swift`
- [ ] `SodaLive/Sources/Content/Detail/AudioContentDeleteDialogView.swift`
- [ ] `SodaLive/Sources/Content/Detail/AudioContentReportDialogView.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/AudioContentCommentItemView.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListViewModel.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyView.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyViewModel.swift`
- [ ] `SodaLive/Sources/Content/Detail/Comment/ContentDetailCommentView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailInfoLimitedEditionView.swift`
#### Group 4 (31-40)
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailInfoView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailMenuView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailMosaicView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailOtherContentView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailPlayView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailPreviousNextContentButtonView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailPurchaseButton.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailView.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentDetailViewModel.swift`
- [ ] `SodaLive/Sources/Content/Detail/ContentOrderConfirmDialogView.swift`
#### Group 5 (41-50)
- [ ] `SodaLive/Sources/Content/Detail/ContentOrderDialogView.swift`
- [ ] `SodaLive/Sources/Content/Detail/LiveRoomDonationDialogView.swift`
- [ ] `SodaLive/Sources/Content/Main/Banner/ContentMainBannerViewModel.swift`
- [ ] `SodaLive/Sources/Content/Main/V2/ContentMainContentThemeView.swift`
- [ ] `SodaLive/Sources/Content/Modify/ContentModifyView.swift`
- [ ] `SodaLive/Sources/Content/Modify/ContentModifyViewModel.swift`
- [ ] `SodaLive/Sources/Content/Player/ContentPlayerView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/ContentPlaylistItemView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/ContentPlaylistListViewModel.swift`
#### Group 6 (51-60)
- [ ] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateViewModel.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistCreateContentView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailViewModel.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyViewModel.swift`
#### Group 7 (61-70)
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentAllView.swift`
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentAllViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentListItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/DayOfWeekSeriesView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailHomeView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailIntroductionView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/ByGenre/SeriesMainByGenreViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekView.swift`
#### Group 8 (71-78)
- [ ] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeView.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/SeriesMainItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/SeriesMainView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesListAllView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift`
### CustomView (3)
- [x] `SodaLive/Sources/CustomView/ChatTextFieldView.swift`
- [x] `SodaLive/Sources/CustomView/ExpandableTextView.swift`
- [x] `SodaLive/Sources/CustomView/IconAndTitleToggleButton.swift`
### Dialog (6)
- [x] `SodaLive/Sources/Dialog/ApplyAuditionCompleteDialog.swift`
- [x] `SodaLive/Sources/Dialog/CommunityPostPurchaseDialog.swift`
- [x] `SodaLive/Sources/Dialog/CreatorFollowNotifyDialog.swift`
- [x] `SodaLive/Sources/Dialog/LivePaymentDialog.swift`
- [x] `SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift`
- [x] `SodaLive/Sources/Dialog/MemberProfileDialog.swift`
### Explorer (40)
#### Group 1 (1-10)
- [x] `SodaLive/Sources/Explorer/ExplorerSectionView.swift`
- [x] `SodaLive/Sources/Explorer/ExplorerView.swift`
- [x] `SodaLive/Sources/Explorer/ExplorerViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/ChannelDonation/ChannelDonationAllView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/ChannelDonation/ChannelDonationItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentListView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentListViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentReplyView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentReplyViewModel.swift`
#### Group 2 (11-20)
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemLockView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityMenuView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Player/CreatorCommunityMediaPlayerManager.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityMoreItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityNoPostsItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyView.swift`
#### Group 3 (21-30)
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityRecordingVoiceView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunitySoundManager.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkAllView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkCheersItemView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListView.swift`
#### Group 4 (31-40)
- [x] `SodaLive/Sources/Explorer/Profile/Series/UserProfileSeriesView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileActivitySummaryView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileContentView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileDonationAllView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileDonationAllViewModel.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileDonationView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileIntroduceView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileLiveView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileView.swift`
- [x] `SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift`
### Follow (1)
- [x] `SodaLive/Sources/Follow/FollowCreatorView.swift`
### Home (9)
#### Group 1 (1-9)
- [x] `SodaLive/Sources/Home/HomeAuditionView.swift`
- [x] `SodaLive/Sources/Home/HomeCreatorRankingItemView.swift`
- [x] `SodaLive/Sources/Home/HomeLatestContentView.swift`
- [x] `SodaLive/Sources/Home/HomeLiveItemView.swift`
- [x] `SodaLive/Sources/Home/HomeTabView.swift`
- [x] `SodaLive/Sources/Home/HomeWeeklyChartItemView.swift`
- [x] `SodaLive/Sources/Home/HomeWeeklyChartView.swift`
- [x] `SodaLive/Sources/Home/RecommendChannel/RecommendChannelContentItemView.swift`
- [x] `SodaLive/Sources/Home/RecommendChannel/RecommendChannelItemView.swift`
### IAP (1)
- [x] `SodaLive/Sources/IAP/StoreManager.swift`
### ImagePicker (1)
- [x] `SodaLive/Sources/ImagePicker/ImagePicker.swift`
### Live (56)
#### Group 1 (1-10)
- [x] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift`
- [x] `SodaLive/Sources/Live/LatestFinishedLiveItemView.swift`
- [x] `SodaLive/Sources/Live/LiveReplayListView.swift`
- [x] `SodaLive/Sources/Live/LiveView.swift`
- [x] `SodaLive/Sources/Live/LiveViewModel.swift`
- [x] `SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift`
- [x] `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift`
- [x] `SodaLive/Sources/Live/Now/LiveNowItemView.swift`
- [x] `SodaLive/Sources/Live/Now/SectionLiveNowView.swift`
- [x] `SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift`
#### Group 2 (11-20)
- [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift`
- [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift`
- [x] `SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift`
- [x] `SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift`
- [x] `SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift`
- [x] `SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift`
- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift`
#### Group 3 (21-30)
- [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift`
- [x] `SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagView.swift`
- [x] `SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/Detail/LiveDetailView.swift`
- [x] `SodaLive/Sources/Live/Room/Detail/LiveDetailViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomDonationMessageDialog.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomDonationMessageItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomDonationRankingDialog.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomDonationRankingItemView.swift`
#### Group 4 (31-40)
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomDonationRankingTotalCanView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomHeartRankingDialog.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomHeartRankingItemView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomInfoEditDialog.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomNoChattingDialogView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomProfileDialog.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomProfileItemTitleView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomProfilesDialogView.swift`
- [x] `SodaLive/Sources/Live/Room/Dialog/LiveRoomUserProfileDialogView.swift`
- [x] `SodaLive/Sources/Live/Room/Edit/LiveRoomEditView.swift`
#### Group 5 (41-50)
- [x] `SodaLive/Sources/Live/Room/Edit/LiveRoomEditViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/LiveRoomViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/Menu/LiveRoomMenuSelectView.swift`
- [x] `SodaLive/Sources/Live/Room/Menu/MenuSettingsView.swift`
- [x] `SodaLive/Sources/Live/Room/Menu/MenuSettingsViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsOptionView.swift`
- [x] `SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsView.swift`
- [x] `SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsViewModel.swift`
- [x] `SodaLive/Sources/Live/Room/Routlette/RoulettePreviewDialog.swift`
- [x] `SodaLive/Sources/Live/Room/V2/Component/Button/LiveRoomNewChatView.swift`
#### Group 6 (51-56)
- [x] `SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoGuestView.swift`
- [x] `SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoHostView.swift`
- [x] `SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInputChatView.swift`
- [x] `SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift`
- [x] `SodaLive/Sources/Live/SectionCommunityPostView.swift`
- [x] `SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift`
### Main (3)
- [x] `SodaLive/Sources/Main/EventPopupDialogView.swift`
- [x] `SodaLive/Sources/Main/Home/BottomTabView.swift`
- [x] `SodaLive/Sources/Main/Home/HomeView.swift`
### Message (13)
#### Group 1 (1-10)
- [x] `SodaLive/Sources/Message/MessageFilterTabView.swift`
- [x] `SodaLive/Sources/Message/MessageView.swift`
- [x] `SodaLive/Sources/Message/Text/Detail/TextMessageDetailView.swift`
- [x] `SodaLive/Sources/Message/Text/Detail/TextMessageDetailViewModel.swift`
- [x] `SodaLive/Sources/Message/Text/SelectRecipient/SelectRecipientView.swift`
- [x] `SodaLive/Sources/Message/Text/SelectRecipient/SelectRecipientViewModel.swift`
- [x] `SodaLive/Sources/Message/Text/TextMessageView.swift`
- [x] `SodaLive/Sources/Message/Text/Write/TextMessageWriteView.swift`
- [x] `SodaLive/Sources/Message/Voice/SoundManager.swift`
- [x] `SodaLive/Sources/Message/Voice/VoiceMessageItemView.swift`
#### Group 2 (11-13)
- [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)
- [x] `SodaLive/Sources/MyPage/Auth/AuthButtonView.swift`
- [x] `SodaLive/Sources/MyPage/Block/BlockMemberListView.swift`
- [x] `SodaLive/Sources/MyPage/Block/BlockedMemberListItemView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Charge/CanChargeView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Charge/CanChargeViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Can/Coupon/CanChargeCouponButtonView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Coupon/CanCouponNoticeItemView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Payment/CanPaymentView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Payment/CanPaymentViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentView.swift`
#### Group 2 (11-20)
- [x] `SodaLive/Sources/MyPage/Can/Payment/CanPgPaymentViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Payment/Temp/CanPaymentTempViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Can/Status/CanStatusView.swift`
- [x] `SodaLive/Sources/MyPage/Can/Status/CanStatusViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Can/Status/CanUseStatusView.swift`
- [x] `SodaLive/Sources/MyPage/CanCardView.swift`
- [x] `SodaLive/Sources/MyPage/MyPageView.swift`
- [x] `SodaLive/Sources/MyPage/MyPageViewModel.swift`
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListAllInnerView.swift`
#### Group 3 (21-30)
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListAllView.swift`
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListAllViewModel.swift`
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListItemView.swift`
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListView.swift`
- [x] `SodaLive/Sources/MyPage/Point/PointStatusView.swift`
- [x] `SodaLive/Sources/MyPage/Point/PointStatusViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Point/Use/PointUseStatusView.swift`
- [x] `SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateView.swift`
- [x] `SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateViewModel.swift`
- [x] `SodaLive/Sources/MyPage/Profile/ProfileUpdateView.swift`
#### Group 4 (31-40)
- [x] `SodaLive/Sources/MyPage/Profile/Tag/MemberTagView.swift`
- [x] `SodaLive/Sources/MyPage/Profile/Tag/MemberTagViewModel.swift`
- [x] `SodaLive/Sources/MyPage/ReservationStatus/Cancel/LiveReservationCancelView.swift`
- [x] `SodaLive/Sources/MyPage/ReservationStatus/LiveReservationStatusItemView.swift`
- [x] `SodaLive/Sources/MyPage/ReservationStatus/LiveReservationStatusView.swift`
- [x] `SodaLive/Sources/MyPage/ReservationStatus/LiveReservationStatusViewModel.swift`
- [x] `SodaLive/Sources/MyPage/ReservationStatusView.swift`
- [x] `SodaLive/Sources/MyPage/ServiceCenter/FaqView.swift`
- [x] `SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterButtonView.swift`
- [x] `SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterView.swift`
#### Group 5 (41-41)
- [x] `SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift`
### Notification (2)
- [x] `SodaLive/Sources/Notification/List/PushNotificationListItemView.swift`
- [x] `SodaLive/Sources/Notification/List/PushNotificationListView.swift`
### Report (4)
- [x] `SodaLive/Sources/Report/CheersReportDialogView.swift`
- [x] `SodaLive/Sources/Report/ProfileReportDialogView.swift`
- [x] `SodaLive/Sources/Report/ProfileReportMenuView.swift`
- [x] `SodaLive/Sources/Report/UserReportDialogView.swift`
### SearchChannel (1)
- [x] `SodaLive/Sources/SearchChannel/SearchChannelView.swift`
### Settings (15)
#### Group 1 (1-10)
- [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)
- [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)
- [x] `SodaLive/Sources/UI/Component/SelectedButtonView.swift`
- [x] `SodaLive/Sources/UI/Component/SeriesDetailTabView.swift`
- [x] `SodaLive/Sources/UI/Component/SeriesItemBadgeView.swift`
- [x] `SodaLive/Sources/UI/Component/SeriesKeywordChipView.swift`
- [x] `SodaLive/Sources/UI/Component/SeriesListBigItemView.swift`
- [x] `SodaLive/Sources/UI/Component/SeriesListItemView.swift`
### User (8)
- [x] `SodaLive/Sources/User/FindPassword/FindPasswordView.swift`
- [x] `SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift`
- [x] `SodaLive/Sources/User/Login/LoginView.swift`
- [x] `SodaLive/Sources/User/Login/LoginViewModel.swift`
- [x] `SodaLive/Sources/User/SignUp/SignUpView.swift`
- [x] `SodaLive/Sources/User/SignUp/SignUpViewModel.swift`
- [x] `SodaLive/Sources/User/UserTextField.swift`
- [x] `SodaLive/Sources/User/UserViewModel.swift`
## 검증 기록
### 1차 계획 수립 (2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 하드코딩 문구 전수 후보를 수집하고, `I18n.swift` 단일화 전환 계획 문서를 작성.
- 왜: 현재 `Localizable.xcstrings``I18n.swift` 혼재로 인해 유지보수/일관성 비용이 증가하고 있어 단일 경로 정리가 필요.
- 어떻게: explore/librarian 병렬 탐색 + grep/ast-grep 직접 검색을 병행해 대상 파일 인벤토리를 생성.
- 실행 명령/도구:
- `task(subagent_type="explore", ... )` x3 (SwiftUI/UIKIt/혼재지점 탐색)
- `task(subagent_type="librarian", ... )` x2 (외부 레퍼런스/네이밍 전략)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources)`
- `grep("\bI18n\.", include=*.swift, path=SodaLive/Sources)`
- `grep("NSLocalizedString\(|String\(localized:|LocalizedStringKey\(", include=*.swift, path=SodaLive/Sources)`
- `grep("Text\(\"[^\"]+\"\)", include=*.swift, path=SodaLive/Sources)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources])`
- 보조 집계 스크립트: `python3` (후보 파일 dedupe/모듈별 집계)
- 결과:
- 하드코딩 후보 파일 360개 산출 및 모듈별 분류 완료.
- `rg` 명령은 환경에 설치되어 있지 않아(`command not found`) grep/ast-grep/파이썬 스캔으로 대체.
### 2차 계획 보강 (외부 i18n 레퍼런스 반영, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: iOS i18n 통합 시 주의점(포맷 토큰/복수형/접근성/동적 키)을 계획 문서에 체크포인트로 반영.
- 왜: 파일 목록만으로는 전환 품질을 담보하기 어려워, 이행 단계별 검증 기준을 사전에 고정할 필요가 있음.
- 어떻게: `librarian` 조사 결과를 기반으로 전환 원칙/리스크 항목을 문서 상단에 추가.
- 실행 명령/도구:
- `background_output(task_id="bg_535935bc")`
- 결과:
- 외부 레퍼런스 기반 전환 원칙 6개 및 리스크 체크포인트 4개 반영 완료.
- 남은 `librarian` 1개(네이밍 전략)는 완료 알림 수신 후 동일 문서에 추가 반영 예정.
### 3차 계획 보강 (I18n 키 네이밍 전략 반영, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `I18n.swift` 단일화 시 키 충돌/확장 혼선을 줄이기 위한 네이밍/계층 전략을 반영.
- 왜: 파일 전수 목록만으로는 장기 유지보수 규칙이 부족해, 이관 이후에도 일관성을 유지할 기준이 필요함.
- 어떻게: `librarian` 네이밍 전략 조사 결과를 정리해 문서 상단에 별도 전략 섹션으로 추가.
- 실행 명령/도구:
- `background_output(task_id="bg_3bd27691")`
- 결과:
- 모듈/화면/컴포넌트 3단계 네임스페이스와 역할 중심 키 네이밍 기준 반영 완료.
- 파라미터 문자열 함수화, 공통 키 승격(`I18n.Common`), 키 rename 금지 원칙 반영 완료.
- 중복 계획 문서(`20260331_I18n키네임스페이스구조계획.md`)는 단일 계획 문서 원칙에 맞춰 제거.
### 4차 계획 보강 (Agora 후보 제외 반영, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `SodaLive/Sources/Agora/Agora.swift`를 변경 대상 목록에서 제외.
- 왜: 해당 파일의 문자열은 `DEBUG_LOG` 메시지/내부 채널 prefix 등 사용자 노출 텍스트가 아니라 i18n 전환 범위와 맞지 않음.
- 어떻게: 변경 대상 섹션에서 `Agora` 모듈과 파일 체크리스트 항목을 제거하고 요약 카운트를 재산정.
- 실행 명령/도구:
- `grep("\"[^\"]*\"", include=Agora.swift, path=SodaLive/Sources/Agora)`
- `grep("Agora|총 대상 파일 수|상위 모듈 수", include=20260331_하드코딩텍스트_I18n통일계획.md, path=docs)`
- `python3` (문서 내 체크리스트 파일 수/모듈 수 재계산)
- 결과:
- 변경 대상 파일 수 `360 → 359`, 상위 모듈 수 `25 → 24`로 갱신.
- `Agora.swift` 체크리스트 항목 제거 및 문서 내 제외 사유 명시 완료.
### 5차 계획 보강 (비노출 문자열 전수 제외 반영, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 359개를 재검토해 비노출 문자열만 가진 파일을 계획에서 제외.
- 왜: i18n 전환 대상은 사용자 노출 문구여야 하며, 내부 로그/채널명/API 경로/프리뷰 샘플 문자열은 범위 외이기 때문.
- 어떻게: 후보 전수 스캔 후 파일별 문자열 컨텍스트를 리뷰해 비노출 29개 파일을 추가 제외(이전 Agora 1개 제외 포함 총 30개 제외).
- 실행 명령/도구:
- `read(SodaLive/Sources/Common/TextViewWrapper.swift)`
- `grep("\"[^\"]*\"", include=Agora.swift, path=SodaLive/Sources/Agora)`
- `python3` (후보 파일 컨텍스트 분류/제외 리스트 산출/문서 섹션 재생성)
- 결과:
- `Common/TextViewWrapper.swift` 제외(실사용 문자열 없음, Preview 샘플만 존재).
- 내부 로그/채널 prefix/API path/아이콘명/색상코드/Preview 샘플만 가진 29개 파일 추가 제외 완료.
- 변경 대상 파일 수 `359 → 330`, 상위 모듈 수 `24 → 21`으로 갱신.
- 예외 유지: `Content/Detail/ContentDetailPurchaseButton.swift``"원으로"/"캔으로"` 사용자 노출 텍스트가 있어 유지.
### 6차 구현 (Audition 모듈 13개 i18n 전환, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록의 `Audition` 모듈 13개 파일을 전수 처리해 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 교체.
- 왜: Audition 영역의 UI/토스트/다이얼로그/오류 메시지가 하드코딩 상태여서 다국어 일관성이 깨지고 유지보수 비용이 높았기 때문.
- 어떻게: explore/librarian/Oracle 병렬 분석 + `grep`/`ast_grep_search` 직접 검증으로 누락 지점을 수집한 뒤, `I18n.swift``I18n.Audition` 네임스페이스를 추가하고 호출부를 모듈 단위로 일괄 치환.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_f58a087c`, `bg_02b03b28`)
- `task(subagent_type="librarian", ...)` x2 (`bg_f62866ac`, `bg_5cd8656b`)
- `task(subagent_type="oracle", ...)` x1 (`bg_81c2c04e`)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Audition)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Audition])`
- `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`
- 결과:
- Audition 호출부 치환 완료 파일: `ApplyMethodView`, `AuditionApplicantRecordingView`, `AuditionApplyView`, `AuditionView`, `AuditionViewModel`, `AuditionDetailView`, `AuditionDetailViewModel`, `AuditionSoundManager`, `AuditionDetailRoleItemView`, `AuditionRoleDetailView`, `AuditionRoleDetailViewModel`.
- `I18n.swift``I18n.Audition`(List/ApplyMethod/Apply/Recording/Detail/Vote/Sound) 키셋 추가 및 공통 오류는 `I18n.Common.commonError`로 통합.
- 녹음 자동 파일명(`voiceon_now_voice_*`)이 사용자에게 그대로 보이던 문제를 `displayFileName` 처리(`I18n.Audition.Apply.recordedVoiceFileName`)로 보정.
- Audition 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/DEBUG_LOG/서버 메시지 분기 비교(비노출 로직)만 존재.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 7차 구현 (Chat 모듈 28개 i18n 전환, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록의 `Chat` 모듈 28개 파일을 전수 처리해 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 교체.
- 왜: Chat 영역에 `String(localized:)` 직접 참조, 뷰 리터럴 문구, ViewModel 반복 오류 문구가 혼재되어 다국어 일관성이 깨져 있었기 때문.
- 어떻게: explore/librarian/oracle + `grep`/`ast_grep_search`/`rg`(미설치 확인) 병렬 탐색으로 런타임 노출 문자열을 추출하고, `I18n.swift``I18n.Chat` 네임스페이스를 추가한 뒤 호출부를 치환.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_c33457a5`, `bg_e543550a`)
- `task(subagent_type="librarian", ...)` x2 (`bg_47a108d5`, `bg_91c00954`)
- `task(subagent_type="oracle", ...)` x1 (`bg_a6465165`)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Chat)`
- `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=SodaLive/Sources/Chat)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Chat])`
- `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" -configuration Debug build` (Oracle 후속 보정 후 재검증)
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` (Oracle 후속 보정 후 재검증)
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- 결과:
- `I18n.swift``I18n.Chat`(Auth/Character/Original/Talk/Room) 키셋 추가.
- Chat 섹션 28개 파일 체크박스 전체 완료 처리.
- 실치환 24개 파일 + Preview/비노출 예외 4개 파일(샘플 데이터 등)로 전수 처리 완료.
- Chat 모듈의 `String(localized:)` 직접 참조 제거 확인.
- Oracle 후속 보정: Bootpay 입력값(`payload.pg`/`payload.method`/`payload.orderName`) 고정값 복원, `characterType.rawValue` 직접 출력 제거, 전송 실패 시 `error.localizedDescription` 사용자 노출 제거(`I18n.Common.commonError`), 최근 대화 헤더 trailing space 제거.
- Chat 모듈 하드코딩 한글 재검증 결과, 남은 문자열은 Preview 샘플/SDK 입력값/비노출 분기 로직만 존재.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 8차 구현 (ImagePicker/CustomView/IAP/Follow/Main/Dialog 15개 파일 i18n 전환, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록 중 `ImagePicker`, `CustomView`, `IAP`, `Follow`, `Main`, `Dialog` 모듈의 15개 파일을 처리해 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 정리.
- 왜: 주요 공통 UI(버튼/다이얼로그/토스트/탭 라벨/인증 안내)에 하드코딩 문자열이 남아 있어 모듈 간 다국어 일관성이 깨지는 상태였기 때문.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 검증으로 치환 대상을 확정하고, `I18n.swift`에 모듈별 네임스페이스(`ImagePicker`, `CustomView`, `IAP`, `Follow`, `Main`, `Dialog`)를 추가한 뒤 호출부를 교체.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_da05186f`, `bg_81c85d58`)
- `task(subagent_type="librarian", ...)` x2 (`bg_4b24d2ad`, `bg_d8b1253f`)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/{ImagePicker,CustomView,IAP,Follow,Main,Dialog})` (모듈별 개별 실행)
- `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=...)` (모듈별 개별 실행)
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[...])`
- `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`
- 결과:
- 호출부 치환 완료 파일: `ImagePicker.swift`, `ExpandableTextView.swift`, `StoreManager.swift`, `FollowCreatorView.swift`, `EventPopupDialogView.swift`, `BottomTabView.swift`, `HomeView.swift`, `ApplyAuditionCompleteDialog.swift`, `CommunityPostPurchaseDialog.swift`, `CreatorFollowNotifyDialog.swift`, `LivePaymentDialog.swift`, `LiveRoomPasswordDialog.swift`, `MemberProfileDialog.swift`.
- 점검만 수행(실치환 없음) 파일: `ChatTextFieldView.swift`, `IconAndTitleToggleButton.swift` (Preview 샘플 문자열만 존재, 런타임 노출 문자열 없음).
- `I18n.swift` 추가 키셋: `I18n.ImagePicker`, `I18n.CustomView`, `I18n.IAP`, `I18n.Follow`, `I18n.Main`(EventPopup/Tab/Auth), `I18n.Dialog`(ApplyAuditionComplete/CommunityPostPurchase/LivePayment/LiveRoomPassword/MemberProfile).
- `Main/Home/HomeView.swift`의 Bootpay 입력값(`payload.pg`, `payload.method`, `payload.orderName`)은 SDK 입력값 유지 원칙에 따라 비노출 고정값으로 유지.
- 모듈 재검증 결과, 남은 한글 문자열은 Preview 샘플/`DEBUG_LOG`/SDK 입력값(비노출)만 존재.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 9차 구현 (User/SearchChannel/Report/Notification 15개 파일 i18n 전환, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록 중 `User`, `SearchChannel`, `Report`, `Notification` 모듈의 15개 파일을 처리해 화면 문자열과 사용자 노출 에러 메시지를 `I18n.*`로 통일했다.
- 왜: 로그인/회원가입/비밀번호 재설정/채널 탐색/신고/알림 화면에 하드코딩 문구가 남아 있어 다국어 접근이 일관되지 않았기 때문이다.
- 어떻게: 관련 뷰와 뷰모델의 문자열을 교체하고, `I18n.swift``User`, `SignUp`, `FindPassword`, `SearchChannel`, `NotificationList`, `Report` 키를 보강했다.
- 실행 명령/도구:
- `rg -n 'Text\\(\"|SecureField\\(\"|TextField\\(\"|DetailNavigationBar\\(title: \\\"|String\\(localized: \\\"|errorMessage = \\\"|let reasons = \\[' SodaLive/Sources/User SodaLive/Sources/SearchChannel SodaLive/Sources/Report SodaLive/Sources/Notification -g '!**/generated/**'`
- `rg -n 'I18n\\.(User|SignUp|FindPassword|Login|SearchChannel|NotificationList|Report|Common)' SodaLive/Sources/User SodaLive/Sources/SearchChannel SodaLive/Sources/Report SodaLive/Sources/Notification -g '!**/generated/**'`
- `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive" -configuration Debug build`
- `HOME=/tmp/codexhome xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive" -configuration Debug -derivedDataPath /tmp/SodaLiveDerivedData -clonedSourcePackagesDirPath /tmp/SodaLiveSPM build`
- 결과:
- `User` 8개 파일, `SearchChannel` 1개 파일, `Report` 4개 파일, `Notification` 2개 파일 체크박스를 완료 처리했다.
- `PushNotificationListItemView.swift`의 시간 구분자도 `I18n.NotificationList.timestampSeparator`로 이관했다.
- `xcodebuild`는 샌드박스 내 캐시/시뮬레이터 접근 제약과 이후 네트워크 차단으로 실패했다. 첫 시도는 workspace 인식 문제와 CoreSimulator 환경 오류가 섞여 있었고, 프로젝트 빌드로 전환한 뒤에는 Swift Package 의존성(`objectbox-swift-spm`)을 GitHub에서 가져오지 못해 중단되었다.
- 따라서 이번 턴에서는 정적 치환과 문서 동기화까지 완료했고, 실제 컴파일 성공 여부는 네트워크가 허용되는 환경에서 추가 확인이 필요하다.
### 10차 계획 보강 (미완료 체크리스트 10개 그룹 재배치, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록`에서 미완료(`- [ ]`) 항목을 모듈 내부 기준으로 10개 단위 그룹으로 재배치.
- 왜: 모듈 단위 처리 시 파일 수가 큰 구간(`Content`, `Live`, `MyPage` 등)의 작업 분할/추적 난이도를 낮추기 위해.
- 어떻게: 기존 파일 순서를 유지한 채 미완료 항목 구간에 `#### Group N` 헤더를 삽입해 10개 배치 단위로 분할.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_7f47b389`, `bg_6b7f2435`)
- `grep("^- \\[ \\] `SodaLive/Sources/.*\\.swift`$", include=20260331_하드코딩텍스트_I18n통일계획.md, path=docs)`
- `grep("^- \\[x\\] `SodaLive/Sources/.*\\.swift`$", include=20260331_하드코딩텍스트_I18n통일계획.md, path=docs)`
- `grep("^#### Group [0-9]+ \\(", include=20260331_하드코딩텍스트_I18n통일계획.md, path=docs)`
- `read(docs/20260331_하드코딩텍스트_I18n통일계획.md)`
- 결과:
- 그룹 재배치 적용 모듈: `Content(78)`, `Explorer(40)`, `Home(9)`, `Live(56)`, `Message(13)`, `MyPage(41)`, `Settings(15)`, `UI(6)`.
-`#### Group` 헤더 29개를 삽입해 미완료 항목을 10개 단위(마지막 그룹은 잔여 수)로 분할 완료.
- 체크 상태 정합성 유지 확인: 미완료 258개, 완료 71개(기존 총합과 동일).
### 11차 구현 (UI 모듈 Group 1, 6개 파일 처리, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: 변경 대상 목록의 `UI` 모듈 Group 1(6개 파일)을 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*`로 전환했다.
- 왜: 공용 시리즈 카드 컴포넌트 중 `SeriesListItemView.swift`에 배지/회차 문구 하드코딩이 남아 있어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문이다.
- 어떻게: explore/librarian 병렬 탐색 결과를 바탕으로 6개 파일 전수 분류(런타임 노출 vs Preview 샘플) 후, 런타임 노출 문자열만 `I18n.Series`로 치환했다.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_f61d6b84`, `bg_9364b9d5`)
- `task(subagent_type="librarian", ...)` x2 (`bg_83bba666`, `bg_3a95eb36`)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/UI/Component)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/UI/Component])`
- `lsp_diagnostics(filePath=SodaLive/Sources/UI/Component/SeriesListItemView.swift)`
- `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`
- 결과:
- 실치환 파일: `SodaLive/Sources/UI/Component/SeriesListItemView.swift`
- `"신작"``I18n.Series.new`
- `"완결"``I18n.Series.complete`
- `"인기"``I18n.Series.popular`
- `"총 \(item.numberOfContent)화"``I18n.Series.totalEpisodes(item.numberOfContent)`
- 점검만 수행(실치환 없음): `SelectedButtonView.swift`, `SeriesDetailTabView.swift`, `SeriesItemBadgeView.swift`, `SeriesKeywordChipView.swift`, `SeriesListBigItemView.swift` (남은 한글은 Preview 샘플 데이터/Preview 라벨).
- 빌드 검증: `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건(`"제목"`, `"<h1>콘텐츠</h1>"`)만 잔존.
- `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` 실컴파일 통과로 검증 완료.
### 13차 구현 (Message 모듈 Group 1, 10개 파일 처리, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Message` Group 1(10개 파일)에서 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환하고 체크박스를 완료 처리.
- 왜: Message 탭/필터/상세/수신자 검색/텍스트 작성/녹음 오류 메시지에 하드코딩 문자열이 남아 있어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 점검으로 치환 범위를 확정하고, `I18n.swift``I18n.Message` 네임스페이스를 추가한 뒤 Group 1 파일 호출부를 치환.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_21246137`, `bg_bc8d6ca7`)
- `task(subagent_type="librarian", ...)` x2 (`bg_fdbe065d`, `bg_ce19e89c`)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Message)`
- `grep("NSLocalizedString\\(|String\\(localized:|LocalizedStringKey\\(", include=*.swift, path=SodaLive/Sources/Message)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Message])`
- `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.Message`(`Tab`, `FilterTab`, `Text.SelectRecipient`, `Text.Write`, `Text.Detail`, `Voice.Sound`) 키셋 추가.
- Oracle 후속 보정: `TextMessageDetailViewModel` 삭제 실패 fallback 키를 `keepFailed``deleteFailed`로 수정, `TextMessageWriteView` 수신자 라벨을 placeholder 키와 분리(`I18n.Message.Text.Write.recipientLabel`), Message 보관 관련 영문/일문 용어를 `Archive/Archived` 기준으로 통일.
- 실치환 파일: `MessageFilterTabView.swift`, `MessageView.swift`, `TextMessageDetailView.swift`, `TextMessageDetailViewModel.swift`, `SelectRecipientView.swift`, `SelectRecipientViewModel.swift`, `TextMessageView.swift`, `TextMessageWriteView.swift`, `SoundManager.swift`.
- 점검만 수행(실치환 없음): `VoiceMessageItemView.swift` (사용자 노출 한글 하드코딩 없음, 시간 표기 `00:00` 숫자 포맷만 존재).
- Message Group 1 체크박스 10개 `- [x]` 완료 반영.
- Group 1 재탐지 결과 한글 리터럴은 `TextMessageDetailView.swift` Preview 샘플 2건(`"누군가"`, `"테스터"`)만 잔존.
- `TextMessageDetailView` 날짜 표기는 기존 `convertDateFormat` 경로를 유지해 현재 기기 locale 기준으로 출력됨(앱 언어 설정과 다른 locale일 경우 혼합 표기 가능성은 후속 정리 체크포인트로 유지).
- Message 모듈 내 `String(localized:)`/`NSLocalizedString`/`LocalizedStringKey` 직접 참조 0건 확인.
- 빌드 검증: `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` 실컴파일 통과로 검증 완료.
### 15차 구현 (MyPage 모듈 Group 1~2, 20개 파일 처리, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``MyPage` Group 1~2(20개 파일)에서 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환하고 체크박스를 완료 처리.
- 왜: MyPage 영역에 `String(localized:)` 직접 참조, 뷰 리터럴, ViewModel 반복 에러 문구가 혼재되어 모듈 단위 i18n 일관성이 깨져 있었기 때문.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search`로 런타임 노출 문자열을 분류한 뒤, `I18n.swift``I18n.MyPage` 네임스페이스를 추가하고 Group 1~2 호출부를 일괄 치환.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_2804258c`, `bg_56679c82`)
- `task(subagent_type="librarian", ...)` x2 (`bg_82e0b3b7`, `bg_a708658e`)
- `grep("\b(String\(localized:|NSLocalizedString\(|LocalizedStringKey\()", include=*.swift, path=SodaLive/Sources/MyPage)`
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/MyPage)` + 대상 파일별 개별 재검증
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/MyPage])`
- `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.MyPage` 키셋 추가(`Common`, `Auth`, `Block`, `Can`, `Can.Payment`, `Main`, `Category`).
- 치환 완료 파일: `AuthButtonView`, `BlockMemberListView`, `BlockedMemberListItemView`, `CanChargeView`, `CanChargeViewModel`, `CanChargeCouponButtonView`, `CanPaymentView`, `CanPaymentViewModel`, `CanPgPaymentView`, `CanPgPaymentViewModel`, `CanPaymentTempView`, `CanPaymentTempViewModel`, `CanStatusView`, `CanStatusViewModel`, `CanCardView`, `MyPageView`, `MyPageViewModel`, `OrderListAllInnerView`.
- 점검만 수행(실치환 없음): `CanCouponNoticeItemView.swift`, `CanUseStatusView.swift` (런타임 노출 하드코딩 없음, Preview/불릿/데이터 바인딩만 존재).
- `MyPageView``CategoryButtonItem``LocalizedStringResource``String`으로 조정해 `I18n.*` 문자열 접근을 통일.
- 반복 실패 문구는 `I18n.Common.commonError`로 통합했고, 본인인증 장문 오류는 `I18n.MyPage.Auth.verificationErrorWithSupport`로 분리.
- Group 1~2 체크박스 20개 `- [x]` 반영 완료.
- 대상 파일 재탐지 결과, 남은 한글 리터럴은 Preview 샘플/SDK 전달 상수(`payload.pg`, `payload.method`, `payload.orderName`, PG method rawValue)만 존재.
- LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Bootpay`, `Kingfisher`, `I18n` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 16차 구현 (MyPage 모듈 Group 3~5, 21개 파일 처리, 2026-03-31)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``MyPage` Group 3~5(21개 파일)에서 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환하고 체크박스를 완료 처리.
- 왜: OrderList/Point/Profile/ReservationStatus/ServiceCenter 구간에 뷰 리터럴과 ViewModel 공통 오류 문구가 남아 있어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 검증으로 런타임 노출 문자열만 추출한 뒤, `I18n.swift``I18n.MyPage` 하위 네임스페이스(`OrderList`, `Point`, `Nickname`, `Profile`, `ReservationStatus`, `Reservation`, `ServiceCenter`)를 추가하고 호출부를 일괄 치환.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_df9bea1f`, `bg_a7a90096`)
- `task(subagent_type="librarian", ...)` x2 (`bg_a3ce40e4`, `bg_0d380792`)
- `background_output(task_id=...)` x4 (위 4개 task 결과 수집)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/MyPage/**)`
- `grep("String\\(localized:|NSLocalizedString\\(|LocalizedStringKey\\(", include=*.swift, path=SodaLive/Sources/MyPage)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/MyPage/{OrderList,Point,Profile,ReservationStatus,ServiceCenter}])`
- `bash: rg -n ...` (`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``I18n.MyPage` Group 3~5 대응 키셋 추가/확장 완료.
- 추가: `OrderList`, `Point`, `Nickname`, `Profile`, `ReservationStatus`, `Reservation.LiveStatus`, `Reservation.Cancel`, `ServiceCenter`.
- 재사용: 공통 실패 문구는 `I18n.Common.commonError`로 통합.
- 치환 완료 파일(20개):
- `OrderListAllView`, `OrderListAllViewModel`, `OrderListItemView`, `OrderListView`
- `PointStatusView`, `PointStatusViewModel`
- `NicknameUpdateView`, `NicknameUpdateViewModel`, `ProfileUpdateView`
- `MemberTagView`, `MemberTagViewModel`
- `LiveReservationCancelView`, `LiveReservationStatusItemView`, `LiveReservationStatusView`, `LiveReservationStatusViewModel`, `ReservationStatusView`
- `FaqView`, `ServiceCenterButtonView`, `ServiceCenterView`, `ServiceCenterViewModel`
- 점검만 수행(실치환 없음, 체크 완료): `PointUseStatusView.swift` (런타임 노출 하드코딩 문자열 없음).
- 대상 재탐지 결과, Group 3~5 영역의 잔여 한글 리터럴은 Preview 샘플 데이터(`"여행"`, `"질문1"` 등)만 존재.
- Group 3~5 체크박스 21개 `- [x]` 완료 반영.
- LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Kingfisher`, `RichText`, `AppState` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
### 17차 구현 (Home 모듈 Group 1, 9개 파일 처리, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Home` Group 1(9개 파일)을 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환했다.
- 왜: 홈 탭의 섹션 헤더/버튼/본인인증 다이얼로그/오류 문구가 하드코딩 상태여서 모듈 간 i18n 접근 방식이 일관되지 않았기 때문이다.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search`/`read` 직접 검증으로 런타임 문자열과 Preview 샘플 문자열을 분리한 뒤, `I18n.swift``I18n.Home` 네임스페이스를 추가하고 호출부를 치환했다.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_7a1064e4`, `bg_2856a903`)
- `task(subagent_type="librarian", ...)` x1 (`bg_2220e841`)
- `background_output(task_id=...)` x3 (위 3개 task 결과 수집)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Home)`
- `grep("I18n\\.|String\\(localized:|NSLocalizedString\\(|LocalizedStringKey\\(", include=*.swift, path=SodaLive/Sources/Home)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Home])`
- `bash: rg -n ...` (`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``I18n.Home` 키셋 추가: `liveNowSectionTitle`, `popularCreatorSectionTitle`, `onlyOnVoiceOnSectionTitle`, `popularCharacterChatSectionTitle`, `recommendChannelSectionTitle`, `freeContentSectionTitle`, `pointRentalContentSectionTitle`, `recommendContentSectionTitle`, `weeklyChartSectionTitle`, `RecommendChannel.contentLabel`.
- 치환 완료 파일(실치환 5개): `HomeCreatorRankingItemView.swift`, `HomeLatestContentView.swift`, `HomeTabView.swift`, `HomeWeeklyChartView.swift`, `RecommendChannelItemView.swift`.
- 점검만 수행(실치환 없음 4개): `HomeAuditionView.swift`, `HomeLiveItemView.swift`, `HomeWeeklyChartItemView.swift`, `RecommendChannelContentItemView.swift` (잔여 한글은 Preview 샘플 데이터만 존재).
- 공통 키 재사용 정리: `I18n.Common.viewAll`, `I18n.Common.latestContent`, `I18n.Settings.companyInfo`, `I18n.Chat.Auth.*`, `I18n.LiveRoom.follow/following` 적용.
- Oracle 후속 보정: 홈 FAB 버튼 문구를 제목형 키(`uploadTitle`)에서 CTA 전용 키(`I18n.CreateContent.uploadAction`)로 분리해 영문/일문 의미를 버튼 행동과 일치시킴.
- Home Group 1 체크박스 9개 `- [x]` 완료 반영.
### 18차 구현 (Live 모듈 Group 1~2, 20개 파일 처리, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Live` Group 1~2(20개 파일)를 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*`로 전환했다.
- 왜: Live 메인/실시간 목록/예약/채팅 아이템에 하드코딩 문구가 남아 있어 모듈 간 i18n 접근이 불일치했고, 동일 의미 문구가 ViewModel에 중복되어 유지보수 비용이 높았기 때문이다.
- 어떻게: explore/librarian 병렬 탐색 + `grep`/`ast_grep_search` 직접 점검으로 대상 문자열을 분류한 뒤, `I18n.swift`에 Live 전용 키셋(`LiveMain`, `LiveNow`, `LiveReservation`, `LiveChat`)을 추가하고 호출부를 치환했다. 기존 공통 키(`I18n.Common`, `I18n.MemberChannel`, `I18n.Main.Auth`)는 재사용했다.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_d093725e`, `bg_d4acf3b2`)
- `task(subagent_type="librarian", ...)` x2 (`bg_cfe29077`, `bg_b4c29632`)
- `background_output(task_id=...)` x4 (위 4개 task 결과 수집)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=대상파일, path=SodaLive/Sources/Live/**)`
- `grep("String\\(localized:|LocalizedStringKey\\(|NSLocalizedString\\(", include=*.swift, path=SodaLive/Sources/Live)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Live])`
- `bash: rg -n ...` (`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` 추가/확장 키셋:
- 신규: `I18n.LiveMain`, `I18n.LiveReservation(Section/All/Item/Complete)`, `I18n.LiveChat`
- 확장: `I18n.LiveNow(sectionTitle/emptyStateMessage/refreshButton/followingChannelsTitle/liveBadge/moreButton)`, `I18n.LiveCancel(title/cancelButton/confirmButton)`, `I18n.MemberChannel.alreadyEndedLive`
- 치환 완료 파일(실치환 18개):
- `LiveCancelDialog.swift`, `LiveReplayListView.swift`, `LiveView.swift`, `LiveViewModel.swift`
- `LiveNowAllView.swift`, `LiveNowItemView.swift`, `SectionLiveNowView.swift`, `SectionRecommendChannelView.swift`
- `LiveReservationAllItemView.swift`, `LiveReservationAllView.swift`, `LiveReservationCompleteView.swift`, `LiveReservationItemView.swift`, `MyLiveReservationItemView.swift`, `SectionLiveReservationView.swift`
- `LiveRoomChatItemView.swift`, `LiveRoomDonationChatItemView.swift`, `LiveRoomHeartDonationChatItemView.swift`, `LiveRoomJoinChatItemView.swift`
- 점검만 수행(실치환 없음, 체크 완료 2개):
- `LatestFinishedLiveItemView.swift` (런타임 고정 문구 없음, 표시값은 API 기반)
- `LiveNowAllItemView.swift` (런타임 문구가 기존 `I18n` 참조 또는 데이터 바인딩)
- Group 1~2 체크박스 20개 `- [x]` 반영 완료.
- 대상 재탐지 결과, 잔여 한글 리터럴은 Preview 샘플/SDK 입력값(`payload.pg`, `payload.method`, `payload.orderName`)/서버 메시지 분기 비교(`message.contains("종료")`)만 존재.
- 빌드 검증:
- `SodaLive` Debug 빌드 성공(`** BUILD SUCCEEDED **`).
- `SodaLive-dev` Debug 빌드는 병렬 실행 시 `build.db` lock으로 1회 실패 후, 단독 재실행에서 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).
- LSP 진단: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`RefreshableScrollView`, `Kingfisher`, `AppState` 등)가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증 완료.
### 19차 구현 (Live 모듈 Group 3~6 마감 보정, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: Live Group 3~6 마감 단계에서 남아 있던 컴파일 오류(`I18n` 경로 오참조)를 수정하고, 체크리스트와 검증 기록을 최신화했다.
- 왜: 기존 치환 반영 이후 `LiveRoomViewModel.swift`의 일부 i18n 경로가 실제 `I18n.swift` 네임스페이스와 불일치해 빌드를 막고 있었기 때문이다.
- 어떻게: 오류 라인(1919, 1964, 1986)을 `I18n.swift` 정의와 대조해 정확한 경로로 교정한 뒤, `SodaLive`/`SodaLive-dev` Debug 빌드와 test 액션 상태를 재검증했다.
- 실행 명령/도구:
- `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`
- `read`/`grep``LiveRoomViewModel.swift`, `I18n.swift`, 계획 문서 대조
- 결과:
- `LiveRoomViewModel.swift` 경로 교정 완료:
- `I18n.LiveRoomPassword.MemberProfile.reportProfile``I18n.Dialog.MemberProfile.reportProfile`
- `I18n.ChannelManagement.userBlocked``I18n.MemberChannel.userBlocked`
- `I18n.ChannelManagement.userUnblocked``I18n.MemberChannel.userUnblocked`
- Live Group 3~6 체크박스 36개 `- [x]` 반영 완료.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 오류 없이 완료(경고만 존재).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약).
- LSP 진단 참고: 단일 파일 진단 시 `No such module 'Moya'`가 보고되나, SourceKit 단독 해석 한계이며 실제 `xcodebuild` 컴파일은 통과했다.
### 20차 구현 (Explorer 모듈 Group 1~4, 40개 파일 처리, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Explorer` Group 1~4(40개 파일)를 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*`로 치환했다.
- 왜: Explorer(탐색/프로필/팬톡/크리에이터 커뮤니티) 구간에서 하드코딩 문구와 직접 로컬라이제이션 API(`String(localized:)`)가 혼재되어 `I18n.swift` 단일 접근 원칙과 충돌했기 때문이다.
- 어떻게: Explorer 대상 파일을 선별해 하드코딩/직접 API 사용을 재탐지하고, `I18n.swift``I18n.Explorer` 네임스페이스를 추가한 뒤 호출부를 치환했다. 공통 문구는 `I18n.Common`으로 통합 재사용했다.
- 실행 명령/도구:
- `lsp_diagnostics(filePath=SodaLive/Sources/Explorer, extension=.swift, severity=all)`
- `lsp_diagnostics(filePath=SodaLive/Sources/I18n/I18n.swift, severity=all)`
- `grep("String\\(localized:|NSLocalizedString\\(|LocalizedStringKey\\(", include=*.swift, path=SodaLive/Sources/Explorer)`
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Explorer)`
- `git diff --name-only -- "SodaLive/Sources/Explorer"`
- `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`
- 결과:
- Explorer Group 1~4 체크박스 40개 `- [x]` 완료 상태를 유지/확인했다.
- 실치환 파일은 총 37개였고, 나머지 3개(`ChannelDonationAllView.swift` 등)는 런타임 사용자 노출 하드코딩이 없어 점검만 수행했다.
- `String(localized:)`/`NSLocalizedString`/`LocalizedStringKey`의 Explorer 직접 사용은 0건으로 확인했다.
- Explorer 잔여 한글 리터럴은 Preview 샘플 데이터 및 `DEBUG_LOG` 메시지(비사용자 노출)만 존재함을 재확인했다.
- 빌드 검증: `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 미구성 확인(코드 실패 아님, 스킴 제약).
### 22차 구현 (Content 모듈 Group 2, 10개 파일 처리, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Content` Group 2(11~20, 10개 파일)를 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환했다.
- 왜: 콘텐츠 목록/큐레이션/생성 플로우 구간에서 하드코딩 문자열이 남아 있어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문이다.
- 어떻게: Group 2 대상 파일을 병렬 탐색한 뒤(`explore`/`librarian`), `grep`/`ast_grep_search`/`lsp_diagnostics`로 치환 대상을 확정하고 `I18n.Content.*`, `I18n.CreateContent.*` 키를 추가·연결했다. 동적 글자수 표기는 함수형 키(`characterCount(_:)`)로 처리했다.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_70fab19d`, `bg_9fe99782`)
- `task(subagent_type="librarian", ...)` x2 (`bg_934b15f1`, `bg_faea5e5d`)
- `background_output(task_id=...)` x4 (위 4개 task 결과 수집)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=Group2 대상 파일)`
- `grep("String\\(localized:|NSLocalizedString\\(|LocalizedStringKey\\(", include=Group2 대상 파일)`
- `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.List.*`(목록 섹션/필터/정렬 라벨)
- `I18n.Content.Playback.playFailed`
- `I18n.CreateContent.*`(테마 선택/입력 폼/알림/검증 문구)
- `I18n.CreateContent.characterCount(_ count: Int)`
- 치환 완료 파일(실치환 9개):
- `ContentListView.swift`, `ContentPlayManager.swift`
- `ContentCreateSelectThemeView.swift`, `ContentCreateSelectThemeViewModel.swift`
- `ContentCreateView.swift`, `ContentCreateViewModel.swift`
- `QuarterTimePickerView.swift`, `SelectDatePicker.swift`
- `ContentCurationView.swift`
- 점검만 수행(실치환 없음, 체크 완료 1개):
- `ContentRepository.swift` (사용자 노출 문구 없음; API 정렬 기본 파라미터 `"매출"`만 존재)
- Group 2 체크박스 10개 `- [x]` 완료 반영.
- Group 2 재탐지 결과, 남은 한글 리터럴은 `ContentPlayManager.swift`의 디버그 `print` 로그 3건 및 `ContentRepository.swift`의 API 파라미터 기본값 1건(비노출)만 확인.
- 직접 로컬라이제이션 API(`String(localized:)`, `NSLocalizedString`, `LocalizedStringKey`)는 Group 2 대상 파일에서 0건으로 확인.
- 빌드 검증: `SodaLive`, `SodaLive-dev` Debug 빌드 모두 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약).
- 수동 QA(문구 경로 수동 점검): Group 2 변경 파일에서 사용자 노출 텍스트가 `I18n.*` 경유인지 라인 단위 검토 완료. 비노출 문자열(디버그 로그/API 파라미터)은 예외로 문서화했다.
- LSP 진단 참고: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼(`Kingfisher`, `ObjectBox`, `I18n`, `AppState` 등) 미해결 오류가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증했다.