feat(i18n): 시리즈/재생목록 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-04-01 17:33:13 +09:00
parent c039931f34
commit 1ec56a1f15
27 changed files with 253 additions and 160 deletions

View File

@@ -855,6 +855,7 @@
} }
}, },
"%@님이" : { "%@님이" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -982,6 +983,16 @@
} }
} }
}, },
"%lld%@" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$lld%2$@"
}
}
}
},
"%lld%% 보유중" : { "%lld%% 보유중" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -1031,6 +1042,7 @@
} }
}, },
"%lld원" : { "%lld원" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3090,6 +3102,7 @@
} }
}, },
"님의 룰렛 결과?" : { "님의 룰렛 결과?" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3266,6 +3279,7 @@
} }
}, },
"답글쓰기" : { "답글쓰기" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3890,6 +3904,7 @@
} }
}, },
"리스너로 변경" : { "리스너로 변경" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -4146,8 +4161,21 @@
} }
} }
}, },
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : { "모든 기기에서 로그아웃" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log out from all devices"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "全端末からログアウト"
}
}
}
}, },
"목" : { "목" : {
"localizations" : { "localizations" : {
@@ -4165,21 +4193,8 @@
} }
} }
}, },
"모든 기기에서 로그아웃" : { "모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log out from all devices"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "全端末からログアウト"
}
}
}
}, },
"모집완료" : { "모집완료" : {
"localizations" : { "localizations" : {
@@ -5532,6 +5547,7 @@
} }
}, },
"스피커 초대" : { "스피커 초대" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -8682,22 +8698,6 @@
} }
} }
}, },
"캔" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cans"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "CAN"
}
}
}
},
"캐릭터 정보" : { "캐릭터 정보" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -8714,6 +8714,22 @@
} }
} }
}, },
"캔" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cans"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "CAN"
}
}
}
},
"캔 충전" : { "캔 충전" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -8988,6 +9004,7 @@
} }
}, },
"콘텐츠를 %@하시겠습니까?" : { "콘텐츠를 %@하시겠습니까?" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View File

@@ -26,14 +26,14 @@ struct ContentPlaylistCreateView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("새 재생목록 만들기") Text(I18n.Content.Playlist.createTitle)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
} }
Spacer() Spacer()
Text("저장") Text(I18n.Content.Playlist.createSave)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.frame(minHeight: 48) .frame(minHeight: 48)
@@ -50,7 +50,7 @@ struct ContentPlaylistCreateView: View {
.background(Color.black) .background(Color.black)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("재생목록 제목") Text(I18n.Content.Playlist.titleLabel)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -83,7 +83,7 @@ struct ContentPlaylistCreateView: View {
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("재생목록 설명을 입력해 주세요") Text(I18n.Content.Playlist.descriptionLabel)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -118,7 +118,7 @@ struct ContentPlaylistCreateView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image("btn_plus_round") Image("btn_plus_round")
Text("새로운 콘텐츠 추가/제거") Text(I18n.Content.Playlist.addContentAction)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }

View File

@@ -57,13 +57,13 @@ final class ContentPlaylistCreateViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
self.isLoading = false self.isLoading = false
} }
@@ -74,13 +74,13 @@ final class ContentPlaylistCreateViewModel: ObservableObject {
private func validate() -> Bool { private func validate() -> Bool {
if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) { if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) {
errorMessage = "제목을 3자 이상 입력하세요" errorMessage = I18n.Content.Playlist.titleValidation
isShowPopup = true isShowPopup = true
return false return false
} }
if (contentList.isEmpty) { if (contentList.isEmpty) {
errorMessage = "콘텐츠를 1개 이상 추가하세요" errorMessage = I18n.Content.Playlist.contentValidation
isShowPopup = true isShowPopup = true
return false return false
} }

View File

@@ -17,14 +17,14 @@ struct PlaylistAddContentView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
ZStack { ZStack {
Text("새로운 콘텐츠 추가/제거") Text(I18n.Content.Playlist.addContentAction)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
HStack(spacing: 0) { HStack(spacing: 0) {
Spacer() Spacer()
Text("닫기") Text(I18n.Content.Playlist.close)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.frame(minHeight: 48) .frame(minHeight: 48)
@@ -36,11 +36,11 @@ struct PlaylistAddContentView: View {
.background(Color.black) .background(Color.black)
HStack(alignment: .center, spacing: 5.3) { HStack(alignment: .center, spacing: 5.3) {
Text("전체") Text(I18n.Content.Playlist.totalLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
Text("\(viewModel.totalCount)") Text(I18n.Content.Playlist.selectionCount(viewModel.totalCount))
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray90) .foregroundColor(Color.gray90)
} }

View File

@@ -125,13 +125,13 @@ struct ContentPlaylistDetailView: View {
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text("만든 날짜 \(response.createdDate)") Text(I18n.Content.Playlist.createdDate(response.createdDate))
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.gray90) .foregroundColor(Color.gray90)
Spacer() Spacer()
Text("\(response.contentCount)") Text(I18n.Content.Playlist.contentCount(response.contentCount))
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
} }
@@ -141,7 +141,7 @@ struct ContentPlaylistDetailView: View {
HStack(spacing: 5.3) { HStack(spacing: 5.3) {
Image("ic_playlist_play") Image("ic_playlist_play")
Text("Play") Text(I18n.Content.Playlist.play)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
@@ -159,7 +159,7 @@ struct ContentPlaylistDetailView: View {
HStack(spacing: 5.3) { HStack(spacing: 5.3) {
Image("ic_playlist_shuffle") Image("ic_playlist_shuffle")
Text("Shuffle") Text(I18n.Content.Playlist.shuffle)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
@@ -255,7 +255,7 @@ struct ContentPlaylistDetailView: View {
Spacer() Spacer()
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("삭제") Text(I18n.Common.delete)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)

View File

@@ -50,13 +50,13 @@ final class ContentPlaylistDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -84,7 +84,7 @@ final class ContentPlaylistDetailViewModel: ObservableObject {
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success { if decoded.success {
self.errorMessage = "삭제되었습니다." self.errorMessage = I18n.Playlist.deleteCompleted
self.isShowPopup = true self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -94,13 +94,13 @@ final class ContentPlaylistDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -27,14 +27,14 @@ struct ContentPlaylistModifyView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("재생목록 수정") Text(I18n.Content.Playlist.modifyTitle)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
} }
Spacer() Spacer()
Text("수정") Text(I18n.Content.Playlist.modifyAction)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.frame(minHeight: 48) .frame(minHeight: 48)
@@ -51,7 +51,7 @@ struct ContentPlaylistModifyView: View {
.background(Color.black) .background(Color.black)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("재생목록 제목") Text(I18n.Content.Playlist.titleLabel)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -84,7 +84,7 @@ struct ContentPlaylistModifyView: View {
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("재생목록 설명을 입력해 주세요") Text(I18n.Content.Playlist.descriptionLabel)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -119,7 +119,7 @@ struct ContentPlaylistModifyView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image("btn_plus_round") Image("btn_plus_round")
Text("새로운 콘텐츠 추가/제거") Text(I18n.Content.Playlist.addContentAction)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }

View File

@@ -57,13 +57,13 @@ final class ContentPlaylistModifyViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -112,13 +112,13 @@ final class ContentPlaylistModifyViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
self.isLoading = false self.isLoading = false
} }
@@ -129,13 +129,13 @@ final class ContentPlaylistModifyViewModel: ObservableObject {
private func validate() -> Bool { private func validate() -> Bool {
if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) { if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) {
errorMessage = "제목을 3자 이상 입력하세요" errorMessage = I18n.Content.Playlist.titleValidation
isShowPopup = true isShowPopup = true
return false return false
} }
if (contentList.isEmpty) { if (contentList.isEmpty) {
errorMessage = "콘텐츠를 1개 이상 추가하세요" errorMessage = I18n.Content.Playlist.contentValidation
isShowPopup = true isShowPopup = true
return false return false
} }

View File

@@ -18,12 +18,12 @@ struct SeriesContentAllView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "\(seriesTitle) - 전체회차 듣기") DetailNavigationBar(title: I18n.Series.allEpisodesTitle(seriesTitle))
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Spacer() Spacer()
Text("최신순") Text(I18n.Content.Sort.newest)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
Color.graye2 Color.graye2
@@ -35,7 +35,7 @@ struct SeriesContentAllView: View {
} }
} }
Text("등록순") Text(I18n.Series.registeredOrder)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
Color.graye2 Color.graye2

View File

@@ -66,13 +66,13 @@ final class SeriesContentAllViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -33,7 +33,7 @@ struct SeriesContentListItemView: View {
.cornerRadius(2.6) .cornerRadius(2.6)
if item.isPointAvailable { if item.isPointAvailable {
Text("포인트") Text(I18n.Series.point)
.appFont(size: 8, weight: .medium) .appFont(size: 8, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
.padding(2.6) .padding(2.6)
@@ -50,7 +50,7 @@ struct SeriesContentListItemView: View {
Spacer() Spacer()
if item.isOwned { if item.isOwned {
Text("소장중") Text(I18n.Content.Status.owned)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.gray11) .foregroundColor(Color.gray11)
.padding(.horizontal, 5.3) .padding(.horizontal, 5.3)
@@ -58,7 +58,7 @@ struct SeriesContentListItemView: View {
.background(Color(hex: "b1ef2c")) .background(Color(hex: "b1ef2c"))
.cornerRadius(2.6) .cornerRadius(2.6)
} else if item.isRented { } else if item.isRented {
Text("대여중") Text(I18n.Content.Status.rented)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.horizontal, 5.3) .padding(.horizontal, 5.3)
@@ -74,7 +74,7 @@ struct SeriesContentListItemView: View {
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color(hex: "909090"))
} }
} else { } else {
Text("무료") Text(I18n.Series.free)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.horizontal, 5.3) .padding(.horizontal, 5.3)

View File

@@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct DayOfWeek { struct DayOfWeek {
let dayOfWeekStr: LocalizedStringResource let dayOfWeekStr: String
let dayOfWeek: SeriesPublishedDaysOfWeek let dayOfWeek: SeriesPublishedDaysOfWeek
} }
@@ -20,14 +20,14 @@ struct DayOfWeekSeriesView: View {
@State private var dayOfWeek: SeriesPublishedDaysOfWeek = .FRI @State private var dayOfWeek: SeriesPublishedDaysOfWeek = .FRI
private let dayOfWeekItems: [DayOfWeek] = [ private let dayOfWeekItems: [DayOfWeek] = [
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .MON), DayOfWeek(dayOfWeekStr: I18n.Series.monday, dayOfWeek: .MON),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .TUE), DayOfWeek(dayOfWeekStr: I18n.Series.tuesday, dayOfWeek: .TUE),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .WED), DayOfWeek(dayOfWeekStr: I18n.Series.wednesday, dayOfWeek: .WED),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .THU), DayOfWeek(dayOfWeekStr: I18n.Series.thursday, dayOfWeek: .THU),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .FRI), DayOfWeek(dayOfWeekStr: I18n.Series.friday, dayOfWeek: .FRI),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SAT), DayOfWeek(dayOfWeekStr: I18n.Series.saturday, dayOfWeek: .SAT),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SUN), DayOfWeek(dayOfWeekStr: I18n.Series.sunday, dayOfWeek: .SUN),
DayOfWeek(dayOfWeekStr: "랜덤", dayOfWeek: .RANDOM), DayOfWeek(dayOfWeekStr: I18n.Series.random, dayOfWeek: .RANDOM),
] ]
// //
@@ -45,13 +45,13 @@ struct DayOfWeekSeriesView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("요일별 시리즈") Text(I18n.Series.byDaySectionTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Text("전체보기") Text(I18n.Common.viewAll)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(.init(hex: "78909C")) .foregroundColor(.init(hex: "78909C"))
.onTapGesture { .onTapGesture {
@@ -93,7 +93,7 @@ struct DayOfWeekSeriesView: View {
} }
struct DayOfWeekDayView: View { struct DayOfWeekDayView: View {
let dayOfWeek: LocalizedStringResource let dayOfWeek: String
let isSelected: Bool let isSelected: Bool
var body: some View { var body: some View {

View File

@@ -17,7 +17,7 @@ struct SeriesDetailHomeView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("전체회차 듣기") Text(I18n.Series.allEpisodesListen)
.appFont(size: 16, weight: .bold) .appFont(size: 16, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)

View File

@@ -90,7 +90,7 @@ struct SeriesDetailView: View {
.cornerRadius(2.6) .cornerRadius(2.6)
if seriesDetail.isAdult { if seriesDetail.isAdult {
Text("19세") Text(I18n.Series.age19Badge)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "f1291c")) .foregroundColor(Color(hex: "f1291c"))
.padding(.horizontal, 5.3) .padding(.horizontal, 5.3)
@@ -98,7 +98,7 @@ struct SeriesDetailView: View {
.background(Color(hex: "312827")) .background(Color(hex: "312827"))
.cornerRadius(2.6) .cornerRadius(2.6)
} else { } else {
Text("전체연령가") Text(I18n.SeriesDetail.ageAll)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2")) .foregroundColor(Color(hex: "d2d2d2"))
.padding(.horizontal, 5.3) .padding(.horizontal, 5.3)
@@ -107,7 +107,7 @@ struct SeriesDetailView: View {
.cornerRadius(2.6) .cornerRadius(2.6)
} }
Text("\(seriesDetail.publishedDaysOfWeek) 연재") Text(I18n.Series.publishing(seriesDetail.publishedDaysOfWeek))
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color(hex: "909090"))
} }

View File

@@ -50,13 +50,13 @@ final class SeriesDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -91,13 +91,13 @@ final class SeriesDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -131,13 +131,13 @@ final class SeriesDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -62,13 +62,13 @@ final class SeriesMainByGenreViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -109,13 +109,13 @@ final class SeriesMainByGenreViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -15,14 +15,14 @@ struct SeriesMainDayOfWeekView: View {
@State private var dayOfWeek: SeriesPublishedDaysOfWeek = .SAT @State private var dayOfWeek: SeriesPublishedDaysOfWeek = .SAT
private let dayOfWeekItems: [DayOfWeek] = [ private let dayOfWeekItems: [DayOfWeek] = [
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .MON), DayOfWeek(dayOfWeekStr: I18n.Series.monday, dayOfWeek: .MON),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .TUE), DayOfWeek(dayOfWeekStr: I18n.Series.tuesday, dayOfWeek: .TUE),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .WED), DayOfWeek(dayOfWeekStr: I18n.Series.wednesday, dayOfWeek: .WED),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .THU), DayOfWeek(dayOfWeekStr: I18n.Series.thursday, dayOfWeek: .THU),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .FRI), DayOfWeek(dayOfWeekStr: I18n.Series.friday, dayOfWeek: .FRI),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SAT), DayOfWeek(dayOfWeekStr: I18n.Series.saturday, dayOfWeek: .SAT),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SUN), DayOfWeek(dayOfWeekStr: I18n.Series.sunday, dayOfWeek: .SUN),
DayOfWeek(dayOfWeekStr: "랜덤", dayOfWeek: .RANDOM), DayOfWeek(dayOfWeekStr: I18n.Series.random, dayOfWeek: .RANDOM),
] ]
// //

View File

@@ -62,13 +62,13 @@ final class SeriesMainDayOfWeekViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -24,13 +24,13 @@ struct SeriesMainHomeView: View {
if !viewModel.completedSeriesList.isEmpty { if !viewModel.completedSeriesList.isEmpty {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("완결 시리즈") Text(I18n.Series.completedSectionTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Text("전체보기") Text(I18n.Common.viewAll)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(.init(hex: "78909C")) .foregroundColor(.init(hex: "78909C"))
.onTapGesture { .onTapGesture {
@@ -58,7 +58,7 @@ struct SeriesMainHomeView: View {
if !viewModel.recommendSeriesList.isEmpty { if !viewModel.recommendSeriesList.isEmpty {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("추천 시리즈") Text(I18n.Series.recommendedSectionTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)

View File

@@ -45,13 +45,13 @@ final class SeriesMainHomeViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
@@ -83,13 +83,13 @@ final class SeriesMainHomeViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -27,7 +27,7 @@ struct SeriesMainItemView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
if item.isPopular { if item.isPopular {
Text("인기") Text(I18n.Series.popular)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -37,7 +37,7 @@ struct SeriesMainItemView: View {
} }
if item.isNew { if item.isNew {
Text("신작") Text(I18n.Series.new)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -56,7 +56,7 @@ struct SeriesMainItemView: View {
} }
if item.isComplete { if item.isComplete {
Text("완결") Text(I18n.Series.complete)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -71,7 +71,7 @@ struct SeriesMainItemView: View {
HStack { HStack {
Spacer() Spacer()
Text("\(item.numberOfContent)") Text(I18n.Series.totalEpisodes(item.numberOfContent))
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)

View File

@@ -28,7 +28,7 @@ struct SeriesMainView: View {
Group { Group {
BaseView { BaseView {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "시리즈 전체보기") DetailNavigationBar(title: I18n.Series.title)
// ( / ) // ( / )
HStack(spacing: 0) { HStack(spacing: 0) {
ChatInnerTab( ChatInnerTab(

View File

@@ -27,7 +27,7 @@ struct SeriesItemView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
if item.isPopular { if item.isPopular {
Text("인기") Text(I18n.Series.popular)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -37,7 +37,7 @@ struct SeriesItemView: View {
} }
if item.isNew { if item.isNew {
Text("신작") Text(I18n.Series.new)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -56,7 +56,7 @@ struct SeriesItemView: View {
} }
if item.isComplete { if item.isComplete {
Text("완결") Text(I18n.Series.complete)
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -71,7 +71,7 @@ struct SeriesItemView: View {
HStack { HStack {
Spacer() Spacer()
Text("\(item.numberOfContent)") Text(I18n.Series.totalEpisodes(item.numberOfContent))
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 10) .padding(.horizontal, 10)

View File

@@ -23,9 +23,9 @@ struct SeriesListAllView: View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
if isCompleted { if isCompleted {
DetailNavigationBar(title: "완결 시리즈") DetailNavigationBar(title: I18n.Series.completedSectionTitle)
} else if isOriginal { } else if isOriginal {
DetailNavigationBar(title: "오직 보이스온에서만") DetailNavigationBar(title: I18n.Series.voiceOnOnlyTitle)
} else { } else {
DetailNavigationBar(title: I18n.Series.viewAllByCreator(creatorNickname ?? "")) DetailNavigationBar(title: I18n.Series.viewAllByCreator(creatorNickname ?? ""))
} }

View File

@@ -63,13 +63,13 @@ final class SeriesListAllViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }

View File

@@ -767,6 +767,21 @@ enum I18n {
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") } static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
static func totalCount(_ count: Int) -> String { pick(ko: "\(count)", en: "Total \(count)", ja: "\(count)") } static func totalCount(_ count: Int) -> String { pick(ko: "\(count)", en: "Total \(count)", ja: "\(count)") }
static func itemCount(_ count: Int) -> String { pick(ko: "\(count)", en: "Total \(count)", ja: "\(count)") } static func itemCount(_ count: Int) -> String { pick(ko: "\(count)", en: "Total \(count)", ja: "\(count)") }
static var createTitle: String { pick(ko: "새 재생목록 만들기", en: "Create new playlist", ja: "新しいプレイリストを作成") }
static var createSave: String { pick(ko: "저장", en: "Save", ja: "保存") }
static var modifyTitle: String { pick(ko: "재생목록 수정", en: "Edit playlist", ja: "プレイリストを編集") }
static var modifyAction: String { pick(ko: "수정", en: "Edit", ja: "編集") }
static var titleLabel: String { pick(ko: "재생목록 제목", en: "Playlist title", ja: "プレイリストタイトル") }
static var descriptionLabel: String { pick(ko: "재생목록 설명을 입력해 주세요", en: "Please enter a playlist description", ja: "プレイリストの説明を入力してください") }
static var addContentAction: String { pick(ko: "새로운 콘텐츠 추가/제거", en: "Add/remove content", ja: "新しいコンテンツを追加・削除") }
static var close: String { pick(ko: "닫기", en: "Close", ja: "閉じる") }
static func selectionCount(_ count: Int) -> String { pick(ko: "\(count)", en: "\(count)", ja: "\(count)") }
static func createdDate(_ date: String) -> String { pick(ko: "만든 날짜 \(date)", en: "Created on \(date)", ja: "作成日 \(date)") }
static func contentCount(_ count: Int) -> String { pick(ko: "\(count)", en: "\(count)", ja: "\(count)") }
static var play: String { pick(ko: "재생", en: "Play", ja: "再生") }
static var shuffle: String { pick(ko: "셔플", en: "Shuffle", ja: "シャッフル") }
static var titleValidation: String { pick(ko: "제목을 3자 이상 입력하세요", en: "Enter at least 3 characters for the title.", ja: "タイトルを3文字以上入力してください") }
static var contentValidation: String { pick(ko: "콘텐츠를 1개 이상 추가하세요", en: "Add at least one content item.", ja: "コンテンツを1件以上追加してください") }
} }
} }
@@ -2663,6 +2678,10 @@ enum I18n {
ja: "\(title)を削除しますか?" ja: "\(title)を削除しますか?"
) )
} }
static var deleteCompleted: String {
pick(ko: "삭제되었습니다.", en: "Deleted.", ja: "削除されました。")
}
} }
enum Category { enum Category {
@@ -3670,6 +3689,26 @@ If you block this user, the following features will be restricted.
static var new: String { pick(ko: "신작", en: "New", ja: "新作") } static var new: String { pick(ko: "신작", en: "New", ja: "新作") }
static var complete: String { pick(ko: "완결", en: "Completed", ja: "完結") } static var complete: String { pick(ko: "완결", en: "Completed", ja: "完結") }
static var popular: String { pick(ko: "인기", en: "Popular", ja: "人気") } static var popular: String { pick(ko: "인기", en: "Popular", ja: "人気") }
static var title: String { pick(ko: "시리즈 전체보기", en: "All series", ja: "シリーズ一覧") }
static var completedSectionTitle: String { pick(ko: "완결 시리즈", en: "Completed series", ja: "完結シリーズ") }
static var recommendedSectionTitle: String { pick(ko: "추천 시리즈", en: "Recommended series", ja: "おすすめシリーズ") }
static var byDaySectionTitle: String { pick(ko: "요일별 시리즈", en: "Series by day", ja: "曜日別シリーズ") }
static var voiceOnOnlyTitle: String { pick(ko: "오직 보이스온에서만", en: "Only on VoiceOn", ja: "VoiceOn限定") }
static var allEpisodesListen: String { pick(ko: "전체회차 듣기", en: "Listen to all episodes", ja: "全話を聴く") }
static func allEpisodesTitle(_ seriesTitle: String) -> String { pick(ko: "\(seriesTitle) - 전체회차 듣기", en: "\(seriesTitle) - All episodes", ja: "\(seriesTitle) - 全話を聴く") }
static var registeredOrder: String { pick(ko: "등록순", en: "Oldest", ja: "登録順") }
static var point: String { pick(ko: "포인트", en: "Points", ja: "ポイント") }
static var free: String { pick(ko: "무료", en: "Free", ja: "無料") }
static var monday: String { pick(ko: "", en: "Mon", ja: "") }
static var tuesday: String { pick(ko: "", en: "Tue", ja: "") }
static var wednesday: String { pick(ko: "", en: "Wed", ja: "") }
static var thursday: String { pick(ko: "", en: "Thu", ja: "") }
static var friday: String { pick(ko: "", en: "Fri", ja: "") }
static var saturday: String { pick(ko: "", en: "Sat", ja: "") }
static var sunday: String { pick(ko: "", en: "Sun", ja: "") }
static var random: String { pick(ko: "랜덤", en: "Random", ja: "ランダム") }
static var age19Badge: String { pick(ko: "19세", en: "19+", ja: "19+") }
static func publishing(_ days: String) -> String { pick(ko: "\(days) 연재", en: "\(days)", ja: "\(days)連載") }
static var totalEpisodes: (Int) -> String = { count in static var totalEpisodes: (Int) -> String = { count in
pick(ko: "\(count)", en: "Total \(count) episodes", ja: "\(count)") pick(ko: "\(count)", en: "Total \(count) episodes", ja: "\(count)")
} }

View File

@@ -201,38 +201,38 @@
- [x] `SodaLive/Sources/Content/Playlist/ContentPlaylistListViewModel.swift` - [x] `SodaLive/Sources/Content/Playlist/ContentPlaylistListViewModel.swift`
#### Group 6 (51-60) #### Group 6 (51-60)
- [ ] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateView.swift` - [x] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateViewModel.swift` - [x] `SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateViewModel.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift` - [x] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift` - [x] `SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Create/PlaylistCreateContentView.swift` - [x] `SodaLive/Sources/Content/Playlist/Create/PlaylistCreateContentView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift` - [x] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailViewModel.swift` - [x] `SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailViewModel.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift` - [x] `SodaLive/Sources/Content/Playlist/Detail/PlaylistContentItemView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyView.swift` - [x] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyView.swift`
- [ ] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyViewModel.swift` - [x] `SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyViewModel.swift`
#### Group 7 (61-70) #### Group 7 (61-70)
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentAllView.swift` - [x] `SodaLive/Sources/Content/Series/Content/SeriesContentAllView.swift`
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentAllViewModel.swift` - [x] `SodaLive/Sources/Content/Series/Content/SeriesContentAllViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Content/SeriesContentListItemView.swift` - [x] `SodaLive/Sources/Content/Series/Content/SeriesContentListItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/DayOfWeekSeriesView.swift` - [x] `SodaLive/Sources/Content/Series/DayOfWeekSeriesView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailHomeView.swift` - [x] `SodaLive/Sources/Content/Series/Detail/SeriesDetailHomeView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailIntroductionView.swift` - [x] `SodaLive/Sources/Content/Series/Detail/SeriesDetailIntroductionView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift` - [x] `SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift`
- [ ] `SodaLive/Sources/Content/Series/Detail/SeriesDetailViewModel.swift` - [x] `SodaLive/Sources/Content/Series/Detail/SeriesDetailViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/ByGenre/SeriesMainByGenreViewModel.swift` - [x] `SodaLive/Sources/Content/Series/Main/ByGenre/SeriesMainByGenreViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekView.swift` - [x] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekView.swift`
#### Group 8 (71-78) #### Group 8 (71-78)
- [ ] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekViewModel.swift` - [x] `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeView.swift` - [x] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeView.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeViewModel.swift` - [x] `SodaLive/Sources/Content/Series/Main/Home/SeriesMainHomeViewModel.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/SeriesMainItemView.swift` - [x] `SodaLive/Sources/Content/Series/Main/SeriesMainItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/Main/SeriesMainView.swift` - [x] `SodaLive/Sources/Content/Series/Main/SeriesMainView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesItemView.swift` - [x] `SodaLive/Sources/Content/Series/SeriesItemView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesListAllView.swift` - [x] `SodaLive/Sources/Content/Series/SeriesListAllView.swift`
- [ ] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift` - [x] `SodaLive/Sources/Content/Series/SeriesListAllViewModel.swift`
### CustomView (3) ### CustomView (3)
- [x] `SodaLive/Sources/CustomView/ChatTextFieldView.swift` - [x] `SodaLive/Sources/CustomView/ChatTextFieldView.swift`
@@ -1087,3 +1087,40 @@
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약). - 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약).
- 수동 QA(문구 경로 수동 점검): Group 3~5 대상 파일 재스캔에서 사용자 노출 문자열의 `I18n.*` 경유를 확인했고, 예외(Preview/비노출 비교 문자열)만 잔존함을 확인했다. - 수동 QA(문구 경로 수동 점검): Group 3~5 대상 파일 재스캔에서 사용자 노출 문자열의 `I18n.*` 경유를 확인했고, 예외(Preview/비노출 비교 문자열)만 잔존함을 확인했다.
- LSP 진단 참고: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼(`Kingfisher`, `Moya`, `I18n`, `AppState` 등) 미해결 오류가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증했다. - LSP 진단 참고: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼(`Kingfisher`, `Moya`, `I18n`, `AppState` 등) 미해결 오류가 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증했다.
### 24차 구현 (Content 모듈 Group 6~8, 28개 파일 처리, 2026-04-01)
- 무엇/왜/어떻게:
- 무엇: `변경 대상 파일 전체 목록``Content` Group 6~8(플레이리스트 생성/수정/상세 + 시리즈 목록/상세/메인, 28개 파일)을 전수 점검하고, 런타임 사용자 노출 하드코딩 문구를 `I18n.*` 참조로 전환했다.
- 왜: Playlist/Series 구간에 화면 타이틀, 버튼, 상태 배지, 정렬 라벨, ViewModel 공통 오류 문구가 하드코딩 상태로 남아 있어 `I18n.swift` 단일 접근 원칙과 불일치했기 때문이다.
- 어떻게: `explore`/`librarian` 병렬 탐색 + `grep`/`ast_grep_search`/`read` 직접 점검으로 런타임 문자열과 Preview 샘플 문자열을 분리한 뒤, `I18n.swift``I18n.Content.Playlist`, `I18n.Playlist`, `I18n.Series`를 확장하고 Group 6~8 호출부를 최소 변경으로 치환했다.
- 실행 명령/도구:
- `task(subagent_type="explore", ...)` x2 (`bg_fa2a12ab`, `bg_56444808`)
- `task(subagent_type="librarian", ...)` x1 (`bg_fd44cf0e`)
- `background_output(task_id=...)` x3 (위 3개 task 결과 수집)
- `grep("\"[^\"]*[가-힣][^\"]*\"", include=*.swift, path=SodaLive/Sources/Content/{Playlist,Series})`
- `grep("\\bI18n\\.(Content|Series|Common)", include=*.swift, path=SodaLive/Sources/Content)`
- `ast_grep_search(pattern="Text(\"$TEXT\")", lang=swift, paths=[SodaLive/Sources/Content/Playlist,SodaLive/Sources/Content/Series])`
- `read(SodaLive/Sources/I18n/I18n.swift)` + 대상 파일 병렬 `read`
- `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.Playlist`에 생성/수정/상세 플로우용 키 추가(`createTitle`, `createSave`, `modifyTitle`, `modifyAction`, `titleLabel`, `descriptionLabel`, `addContentAction`, `close`, `selectionCount(_:)`, `createdDate(_:)`, `contentCount(_:)`, `play`, `shuffle`, 검증 문구 2종)
- `I18n.Playlist.deleteCompleted` 추가
- `I18n.Series`에 메인/상세/요일/상태 배지 키 추가(`title`, `completedSectionTitle`, `recommendedSectionTitle`, `byDaySectionTitle`, `voiceOnOnlyTitle`, `allEpisodesListen`, `allEpisodesTitle(_:)`, `registeredOrder`, `point`, `free`, 요일 8종, `age19Badge`, `publishing(_:)`)
- 치환 완료 파일(실치환 20개):
- Playlist: `ContentPlaylistCreateView`, `ContentPlaylistCreateViewModel`, `PlaylistAddContentView`, `ContentPlaylistDetailView`, `ContentPlaylistDetailViewModel`, `ContentPlaylistModifyView`, `ContentPlaylistModifyViewModel`
- Series: `SeriesContentAllView`, `SeriesContentAllViewModel`, `SeriesContentListItemView`, `DayOfWeekSeriesView`, `SeriesDetailHomeView`, `SeriesDetailView`, `SeriesDetailViewModel`, `SeriesMainByGenreViewModel`, `SeriesMainDayOfWeekView`, `SeriesMainDayOfWeekViewModel`, `SeriesMainHomeView`, `SeriesMainHomeViewModel`, `SeriesMainItemView`, `SeriesMainView`, `SeriesItemView`, `SeriesListAllView`, `SeriesListAllViewModel`
- 점검만 수행(실치환 없음, 체크 완료 8개):
- `PlaylistAddContentItemView.swift`, `PlaylistCreateContentView.swift`, `PlaylistContentItemView.swift` (런타임 하드코딩 없음, Preview 샘플만 존재)
- `SeriesDetailIntroductionView.swift` (런타임 사용자 노출 하드코딩 없음; 잔여 한글은 서버값 비교용 `"랜덤"` 분기만 존재)
- Group 6~8 체크박스 28개 `- [x]` 완료 반영.
- 재탐지 결과, 남은 한글 리터럴은 Playlist/Series Preview 샘플 데이터와 `SeriesDetailIntroductionView.swift`의 비노출 비교 문자열(`publishedDaysOfWeek == "랜덤"`)만 존재.
- 빌드 검증:
- `SodaLive-dev` Debug 빌드 성공(`** BUILD SUCCEEDED **`).
- `SodaLive` Debug 빌드는 병렬 실행 시 `XCBuildData/build.db` lock으로 1회 실패했으나, 단독 재실행에서 성공(`** BUILD SUCCEEDED **`).
- 테스트 검증: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 test action 미구성 확인(코드 실패 아님, 스킴 제약).
- 수동 QA(문구 경로 수동 점검): Group 6~8 대상 파일 재스캔에서 사용자 노출 문자열의 `I18n.*` 경유를 확인했고, 예외는 Preview/비노출 비교 문자열만 남김.
- LSP 진단 참고: SourceKit 단독 해석 환경에서 외부 모듈/프로젝트 심볼 미해결 오류(`Kingfisher`, `I18n`, `BaseView`, `AppState` 등)와 `I18n.swift`의 기존 `LanguageHeaderProvider` 미해결이 보고되나, 동일 변경셋은 `xcodebuild` 실컴파일 통과로 검증했다.