feat(i18n): 오디션 화면 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-03-31 15:39:57 +09:00
parent 136bfc8eee
commit 222520d5e9
13 changed files with 406 additions and 87 deletions

View File

@@ -30,7 +30,7 @@ struct ApplyMethodView: View {
} }
} }
Text("오디션 지원방식") Text(I18n.Audition.ApplyMethod.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.top, 33.3) .padding(.top, 33.3)
@@ -39,7 +39,7 @@ struct ApplyMethodView: View {
HStack(spacing: 3) { HStack(spacing: 3) {
Image("ic_upload") Image("ic_upload")
Text("파일 업로드") Text(I18n.Audition.ApplyMethod.fileUpload)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
@@ -60,7 +60,7 @@ struct ApplyMethodView: View {
HStack(spacing: 3) { HStack(spacing: 3) {
Image("ic_mic_color_button") Image("ic_mic_color_button")
Text("바로 녹음") Text(I18n.Audition.ApplyMethod.recordNow)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
@@ -81,7 +81,7 @@ struct ApplyMethodView: View {
.padding(.top, 21.3) .padding(.top, 21.3)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("※ 파일은 mp3, aac만 업로드 가능") Text(I18n.Audition.ApplyMethod.fileFormatNotice)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
.padding(.top, 13.3) .padding(.top, 13.3)

View File

@@ -30,7 +30,7 @@ struct AuditionApplicantRecordingView: View {
VStack { VStack {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션 녹음") Text(I18n.Audition.Recording.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
@@ -76,7 +76,7 @@ struct AuditionApplicantRecordingView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Spacer() Spacer()
Text("삭제") Text(I18n.Common.delete)
.appFont(size: 15.3, weight: .medium) .appFont(size: 15.3, weight: .medium)
.foregroundColor(Color.graybb.opacity(0)) .foregroundColor(Color.graybb.opacity(0))
@@ -99,7 +99,7 @@ struct AuditionApplicantRecordingView: View {
Spacer() Spacer()
Text("삭제") Text(I18n.Common.delete)
.appFont(size: 15.3, weight: .medium) .appFont(size: 15.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.onTapGesture { .onTapGesture {
@@ -113,7 +113,7 @@ struct AuditionApplicantRecordingView: View {
.padding(.vertical, 52.3) .padding(.vertical, 52.3)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("다시 녹음") Text(I18n.Audition.Recording.recordAgain)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.frame(width: (proxy.size.width - 40) / 3, height: 50) .frame(width: (proxy.size.width - 40) / 3, height: 50)
@@ -129,7 +129,7 @@ struct AuditionApplicantRecordingView: View {
soundManager.recordMode = .RECORD soundManager.recordMode = .RECORD
} }
Text("녹음완료") Text(I18n.Audition.Recording.recordComplete)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.frame(width: (proxy.size.width - 40) * 2 / 3, height: 50) .frame(width: (proxy.size.width - 40) * 2 / 3, height: 50)
@@ -140,7 +140,7 @@ struct AuditionApplicantRecordingView: View {
let soundData = try Data(contentsOf: soundManager.getAudioFileURL()) let soundData = try Data(contentsOf: soundManager.getAudioFileURL())
onClickCompleteRecording(tempFileName, soundData) onClickCompleteRecording(tempFileName, soundData)
} catch { } catch {
errorMessage = "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." errorMessage = I18n.Audition.Recording.createFileFailed
isShowPopup = true isShowPopup = true
} }
} }

View File

@@ -33,7 +33,7 @@ struct AuditionApplyView: View {
if isShow { if isShow {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션 지원") Text(I18n.Audition.Apply.title)
.appFont(size: 18.3, weight: .medium) .appFont(size: 18.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
@@ -45,7 +45,7 @@ struct AuditionApplyView: View {
} }
} }
Text("녹음파일") Text(I18n.Audition.Apply.recordingFile)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(.grayee) .foregroundColor(.grayee)
.padding(.top, 20) .padding(.top, 20)
@@ -53,7 +53,7 @@ struct AuditionApplyView: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Image("ic_note_square") Image("ic_note_square")
Text(filename) Text(displayFileName)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.grayd2) .foregroundColor(.grayd2)
@@ -66,12 +66,12 @@ struct AuditionApplyView: View {
.cornerRadius(5.3) .cornerRadius(5.3)
.padding(.top, 10) .padding(.top, 10)
Text("연락처") Text(I18n.Audition.Apply.contact)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(.grayee) .foregroundColor(.grayee)
.padding(.top, 15) .padding(.top, 15)
TextField("합격시 받을 연락처를 남겨주세요", text: $phoneNumber) TextField(I18n.Audition.Apply.contactPlaceholder, text: $phoneNumber)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
@@ -89,7 +89,7 @@ struct AuditionApplyView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.") Text(I18n.Audition.Apply.privacyAgreement)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.lineSpacing(3) .lineSpacing(3)
@@ -100,7 +100,7 @@ struct AuditionApplyView: View {
isAgree.toggle() isAgree.toggle()
} }
Text("오디션 지원하기") Text(I18n.Audition.Apply.submit)
.appFont(size: 13.3, weight: .bold) .appFont(size: 13.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
@@ -110,7 +110,7 @@ struct AuditionApplyView: View {
.padding(.top, 35) .padding(.top, 35)
.onTapGesture { .onTapGesture {
if !isAgree { if !isAgree {
errorMessage = "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다." errorMessage = I18n.Audition.Apply.requireAgreement
isShowPopup = true isShowPopup = true
return return
} }
@@ -137,6 +137,14 @@ struct AuditionApplyView: View {
} }
} }
} }
private var displayFileName: String {
if filename.hasPrefix("voiceon_now_voice_") {
return I18n.Audition.Apply.recordedVoiceFileName
}
return filename
}
} }
#Preview { #Preview {

View File

@@ -24,7 +24,7 @@ struct AuditionView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("오디션") Text(I18n.Audition.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
@@ -43,13 +43,13 @@ struct AuditionView: View {
.background(Color.black) .background(Color.black)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("보이스온 오디션 이용방법") Text(I18n.Audition.List.usageGuide)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Text("자세히>") Text(I18n.Audition.List.detail)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -73,17 +73,17 @@ struct AuditionView: View {
if $0 == 0 && !item.isOff { if $0 == 0 && !item.isOff {
VStack(alignment: .leading, spacing: 25) { VStack(alignment: .leading, spacing: 25) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션") Text(I18n.Audition.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(" ON") Text(I18n.Audition.List.onStatus)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.mainRed) .foregroundColor(Color.mainRed)
Spacer() Spacer()
Text("\(viewModel.inProgressCount)") Text(I18n.Audition.List.totalCount(viewModel.inProgressCount))
.appFont(size: 11.3, weight: .medium) .appFont(size: 11.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
} }
@@ -111,17 +111,17 @@ struct AuditionView: View {
.padding(.top, 5) .padding(.top, 5)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션") Text(I18n.Audition.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(" OFF") Text(I18n.Audition.List.offStatus)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Spacer() Spacer()
Text("\(viewModel.completedCount)") Text(I18n.Audition.List.totalCount(viewModel.completedCount))
.appFont(size: 11.3, weight: .medium) .appFont(size: 11.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
} }

View File

@@ -66,13 +66,13 @@ final class AuditionViewModel: 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

@@ -30,7 +30,7 @@ struct AuditionDetailView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.cornerRadius(6.7) .cornerRadius(6.7)
Text("오디션 정보") Text(I18n.Audition.Detail.informationTitle)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)
@@ -38,7 +38,7 @@ struct AuditionDetailView: View {
ExpandableTextView(text: response.information) ExpandableTextView(text: response.information)
.padding(.top, 13.3) .padding(.top, 13.3)
Text("오디션 캐릭터") Text(I18n.Audition.Detail.characterTitle)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)

View File

@@ -18,7 +18,7 @@ final class AuditionDetailViewModel: ObservableObject {
@Published var isLoading = false @Published var isLoading = false
@Published var response: GetAuditionDetailResponse? = nil @Published var response: GetAuditionDetailResponse? = nil
@Published var title: String = "보이스온" @Published var title: String = I18n.Audition.defaultTitle
func getAuditionDetail(auditionId: Int, onFailure: @escaping () -> Void) { func getAuditionDetail(auditionId: Int, onFailure: @escaping () -> Void) {
isLoading = true isLoading = true
@@ -45,7 +45,7 @@ final class AuditionDetailViewModel: 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
@@ -54,7 +54,7 @@ final class AuditionDetailViewModel: ObservableObject {
} }
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
onFailure() onFailure()

View File

@@ -64,7 +64,7 @@ class AuditionSoundManager: NSObject, ObservableObject {
private func setupPlayer(with url: String) { private func setupPlayer(with url: String) {
guard let url = URL(string: url) else { guard let url = URL(string: url) else {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." self.errorMessage = I18n.Audition.Sound.playbackFailed
self.isShowPopup = true self.isShowPopup = true
return return
} }
@@ -92,7 +92,7 @@ class AuditionSoundManager: NSObject, ObservableObject {
} }
} catch { } catch {
DispatchQueue.main.async { DispatchQueue.main.async {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." self.errorMessage = I18n.Audition.Sound.playbackFailed
self.isShowPopup = true self.isShowPopup = true
self.isLoading = false self.isLoading = false
} }

View File

@@ -28,7 +28,7 @@ struct AuditionDetailRoleItemView: View {
.opacity(item.isComplete ? 0.7 : 0.0) .opacity(item.isComplete ? 0.7 : 0.0)
) )
Text(item.isComplete ? "모집완료" : "모집중") Text(item.isComplete ? I18n.Audition.Detail.recruitmentClosed : I18n.Audition.Detail.recruiting)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.horizontal, 9) .padding(.horizontal, 9)

View File

@@ -45,7 +45,7 @@ struct AuditionRoleDetailView: View {
HStack(spacing: 14) { HStack(spacing: 14) {
if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) { if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) {
Text("원작 보러가기") Text(I18n.Audition.Detail.viewOriginalWork)
.appFont(size: 16, weight: .bold) .appFont(size: 16, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 12) .padding(.vertical, 12)
@@ -59,7 +59,7 @@ struct AuditionRoleDetailView: View {
} }
if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) { if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) {
Text("오디션 대본 확인") Text(I18n.Audition.Detail.checkScript)
.appFont(size: 16, weight: .bold) .appFont(size: 16, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 12) .padding(.vertical, 12)
@@ -74,7 +74,7 @@ struct AuditionRoleDetailView: View {
} }
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
Text("오디션 캐릭터 정보") Text(I18n.Audition.Detail.characterInfo)
.appFont(size: 14.7, weight: .bold) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@@ -83,13 +83,13 @@ struct AuditionRoleDetailView: View {
} }
if viewModel.applicantList.isEmpty { if viewModel.applicantList.isEmpty {
Text("지원자가 없습니다.") Text(I18n.Audition.Detail.noApplicants)
.appFont(size: 13, weight: .medium) .appFont(size: 13, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)
} else { } else {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("참여자") Text(I18n.Audition.Detail.participants)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
@@ -98,13 +98,13 @@ struct AuditionRoleDetailView: View {
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.leading, 2.3) .padding(.leading, 2.3)
Text("") Text(I18n.Audition.Detail.personUnit)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Spacer() Spacer()
Text("최신순") Text(I18n.Audition.Detail.sortNewest)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
viewModel.sortType == .NEWEST ? Color.button : Color.graybb viewModel.sortType == .NEWEST ? Color.button : Color.graybb
@@ -113,7 +113,7 @@ struct AuditionRoleDetailView: View {
viewModel.setSortType(sortType: .NEWEST) viewModel.setSortType(sortType: .NEWEST)
} }
Text("좋아요순") Text(I18n.Audition.Detail.sortLikes)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
viewModel.sortType == .LIKES ? Color.button : Color.graybb viewModel.sortType == .LIKES ? Color.button : Color.graybb
@@ -161,7 +161,7 @@ struct AuditionRoleDetailView: View {
} }
if let roleDetail = viewModel.auditionRoleDetail { if let roleDetail = viewModel.auditionRoleDetail {
Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원") Text(roleDetail.isAlreadyApplicant ? I18n.Audition.Apply.reapply : I18n.Audition.Apply.apply)
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(14) .padding(14)
@@ -241,9 +241,9 @@ struct AuditionRoleDetailView: View {
if isShowNoticeReapply { if isShowNoticeReapply {
SodaDialog( SodaDialog(
title: "재지원 안내", title: I18n.Audition.Apply.reapplyNoticeTitle,
desc: "재지원 시 이전 지원 내역은 삭제되며 받은 투표수는 무효 처리됩니다.", desc: I18n.Audition.Apply.reapplyNoticeDesc,
confirmButtonTitle: "확인" confirmButtonTitle: I18n.Common.confirm
) { ) {
isShowNoticeReapply = false isShowNoticeReapply = false
isShowApplyMethodView = true isShowApplyMethodView = true
@@ -252,9 +252,9 @@ struct AuditionRoleDetailView: View {
if isShowNoticeAuthView { if isShowNoticeAuthView {
SodaDialog( SodaDialog(
title: "- 본인인증 -", title: I18n.Audition.Apply.authRequiredTitle,
desc: "마이페이지에서 '본인인증'을 하고 다시 오디션에 지원해 주세요.", desc: I18n.Audition.Apply.authRequiredDesc,
confirmButtonTitle: "확인" confirmButtonTitle: I18n.Common.confirm
) { ) {
isShowNoticeAuthView = false isShowNoticeAuthView = false
} }
@@ -264,7 +264,7 @@ struct AuditionRoleDetailView: View {
SodaDialog( SodaDialog(
title: viewModel.dialogTitle, title: viewModel.dialogTitle,
desc: viewModel.dialogDesc, desc: viewModel.dialogDesc,
confirmButtonTitle: "확인" confirmButtonTitle: I18n.Common.confirm
) { ) {
viewModel.isShowVoteCompleteView = false viewModel.isShowVoteCompleteView = false
viewModel.isShowNotifyVote = false viewModel.isShowNotifyVote = false
@@ -300,17 +300,17 @@ struct AuditionRoleDetailView: View {
viewModel.fileName = fileUrl.lastPathComponent viewModel.fileName = fileUrl.lastPathComponent
isShowApplyView = true isShowApplyView = true
} else { } else {
viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed
viewModel.isShowPopup = true viewModel.isShowPopup = true
} }
} else { } else {
viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed
viewModel.isShowPopup = true viewModel.isShowPopup = true
} }
case .failure(let error): case .failure(let error):
DEBUG_LOG("error: \(error.localizedDescription)") DEBUG_LOG("error: \(error.localizedDescription)")
viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.errorMessage = I18n.Audition.Apply.fileLoadFailed
viewModel.isShowPopup = true viewModel.isShowPopup = true
} }
} }

View File

@@ -21,7 +21,7 @@ final class AuditionRoleDetailViewModel: ObservableObject {
@Published var totalCount = 0 @Published var totalCount = 0
@Published var applicantList = [GetAuditionRoleApplicantItem]() @Published var applicantList = [GetAuditionRoleApplicantItem]()
@Published var name = "보이스온" @Published var name = I18n.Audition.defaultTitle
@Published var auditionRoleDetail: GetAuditionRoleDetailResponse? = nil @Published var auditionRoleDetail: GetAuditionRoleDetailResponse? = nil
@Published private(set) var sortType = AuditionApplicantSortType.NEWEST { @Published private(set) var sortType = AuditionApplicantSortType.NEWEST {
@@ -93,13 +93,13 @@ final class AuditionRoleDetailViewModel: ObservableObject {
if let message = roleDetailDecoded.message { if let message = roleDetailDecoded.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
} }
@@ -118,7 +118,7 @@ final class AuditionRoleDetailViewModel: ObservableObject {
if let message = applicantListDecoded.message { if let message = applicantListDecoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
@@ -127,7 +127,7 @@ final class AuditionRoleDetailViewModel: ObservableObject {
} }
} }
} catch { } catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.onFailure() self.onFailure()
@@ -172,13 +172,13 @@ final class AuditionRoleDetailViewModel: 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
} }
} }
@@ -188,13 +188,13 @@ final class AuditionRoleDetailViewModel: ObservableObject {
func applyAudition(onSuccess: @escaping () -> Void) { func applyAudition(onSuccess: @escaping () -> Void) {
if phoneNumber.count != 11 { if phoneNumber.count != 11 {
errorMessage = "잘못된 연락처 입니다.\n다시 입력해 주세요." errorMessage = I18n.Audition.Apply.invalidContact
isShowPopup = true isShowPopup = true
return return
} }
guard let soundData = soundData else { guard let soundData = soundData else {
errorMessage = "잘못된 녹음 파일 입니다.\n다시 선택해 주세요." errorMessage = I18n.Audition.Apply.invalidRecordingFile
isShowPopup = true isShowPopup = true
return return
} }
@@ -248,19 +248,19 @@ final class AuditionRoleDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Audition.Apply.applyFailed
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Audition.Apply.applyFailed
self.isShowPopup = true self.isShowPopup = true
} }
} }
.store(in: &subscription) .store(in: &subscription)
} else { } else {
self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Audition.Apply.applyFailed
self.isShowPopup = true self.isShowPopup = true
self.isLoading = false self.isLoading = false
} }
@@ -287,8 +287,8 @@ final class AuditionRoleDetailViewModel: ObservableObject {
if decoded.success { if decoded.success {
if self.isShowNotifyVote { if self.isShowNotifyVote {
self.dialogTitle = "[오디션 응원]" self.dialogTitle = I18n.Audition.Vote.cheerTitle
self.dialogDesc = "오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요." self.dialogDesc = I18n.Audition.Vote.cheerDescription
self.isShowVoteCompleteView = true self.isShowVoteCompleteView = true
} }
@@ -302,20 +302,20 @@ final class AuditionRoleDetailViewModel: ObservableObject {
} else { } else {
if let message = decoded.message { if let message = decoded.message {
if message.contains("오늘 응원은 여기까지") { if message.contains("오늘 응원은 여기까지") {
self.dialogTitle = "[오늘 응원 제한]" self.dialogTitle = I18n.Audition.Vote.limitTitle
self.dialogDesc = "오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요." self.dialogDesc = I18n.Audition.Vote.limitDescription
self.isShowVoteCompleteView = true self.isShowVoteCompleteView = true
} else { } else {
self.errorMessage = message self.errorMessage = message
self.isShowPopup = true self.isShowPopup = true
} }
} else { } else {
self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." self.errorMessage = I18n.Audition.Vote.unknownError
self.isShowPopup = true self.isShowPopup = true
} }
} }
} catch { } catch {
self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." self.errorMessage = I18n.Audition.Vote.unknownError
self.isShowPopup = true self.isShowPopup = true
} }
} }

View File

@@ -281,6 +281,294 @@ enum I18n {
static var points: String { pick(ko: "포인트", en: "Points", ja: "ポイント") } static var points: String { pick(ko: "포인트", en: "Points", ja: "ポイント") }
} }
enum Audition {
static var title: String {
pick(ko: "오디션", en: "Audition", ja: "オーディション")
}
static var defaultTitle: String {
pick(ko: "보이스온", en: "VoiceOn", ja: "ボイスオン")
}
enum List {
static var usageGuide: String {
pick(
ko: "보이스온 오디션 이용방법",
en: "How to use VoiceOn Audition",
ja: "ボイスオンオーディション利用方法"
)
}
static var detail: String {
pick(ko: "자세히>", en: "Details >", ja: "詳しく >")
}
static var onStatus: String {
pick(ko: " ON", en: " ON", ja: " ON")
}
static var offStatus: String {
pick(ko: " OFF", en: " OFF", ja: " OFF")
}
static func totalCount(_ count: Int) -> String {
pick(
ko: "\(count)",
en: "Total \(count)",
ja: "\(count)"
)
}
}
enum ApplyMethod {
static var title: String {
pick(ko: "오디션 지원방식", en: "Audition application method", ja: "オーディション応募方法")
}
static var fileUpload: String {
pick(ko: "파일 업로드", en: "Upload file", ja: "ファイルアップロード")
}
static var recordNow: String {
pick(ko: "바로 녹음", en: "Record now", ja: "今すぐ録音")
}
static var fileFormatNotice: String {
pick(
ko: "※ 파일은 mp3, aac만 업로드 가능",
en: "※ Only mp3 and aac files can be uploaded",
ja: "※ ファイルはmp3、aacのみアップロード可能"
)
}
}
enum Apply {
static var title: String {
pick(ko: "오디션 지원", en: "Apply for audition", ja: "オーディション応募")
}
static var recordingFile: String {
pick(ko: "녹음파일", en: "Recorded file", ja: "録音ファイル")
}
static var contact: String {
pick(ko: "연락처", en: "Contact", ja: "連絡先")
}
static var contactPlaceholder: String {
pick(
ko: "합격시 받을 연락처를 남겨주세요",
en: "Enter contact information to receive if selected",
ja: "合格時に受け取る連絡先を入力してください"
)
}
static var recordedVoiceFileName: String {
pick(ko: "내 녹음파일", en: "My recording", ja: "自分の録音ファイル")
}
static var privacyAgreement: String {
pick(
ko: "보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.",
en: "I agree to the collection and use of personal information (contact) for personal contact if selected for the VoiceOn audio drama audition.\nApplicants have the right to refuse consent to the collection and use of personal information, and if they do not consent, the audition application will be canceled.",
ja: "ボイスオンオーディオドラマオーディション合格時の個別連絡のため、個人情報(連絡先)の収集および利用に同意します。\n応募者は個人情報の収集および利用への同意を拒否する権利があり、同意しない場合はオーディション応募が取り消されます。"
)
}
static var submit: String {
pick(ko: "오디션 지원하기", en: "Submit audition application", ja: "オーディションに応募する")
}
static var apply: String {
pick(ko: "오디션 지원", en: "Apply for audition", ja: "オーディション応募")
}
static var reapply: String {
pick(ko: "오디션 재지원", en: "Reapply for audition", ja: "オーディション再応募")
}
static var requireAgreement: String {
pick(
ko: "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.",
en: "You must agree to the collection and use of contact information to apply for the audition.",
ja: "連絡先の収集および利用に同意すると、オーディションに応募できます。"
)
}
static var reapplyNoticeTitle: String {
pick(ko: "재지원 안내", en: "Reapply notice", ja: "再応募の案内")
}
static var reapplyNoticeDesc: String {
pick(
ko: "재지원 시 이전 지원 내역은 삭제되며 받은 투표수는 무효 처리됩니다.",
en: "When you reapply, your previous application history is deleted and received votes are invalidated.",
ja: "再応募すると、以前の応募履歴は削除され、受け取った投票数は無効になります。"
)
}
static var authRequiredTitle: String {
pick(ko: "- 본인인증 -", en: "- Identity verification -", ja: "- 本人認証 -")
}
static var authRequiredDesc: String {
pick(
ko: "마이페이지에서 '본인인증'을 하고 다시 오디션에 지원해 주세요.",
en: "Please complete 'Identity Verification' in My Page and apply for the audition again.",
ja: "マイページで「本人認証」を行ってから、もう一度オーディションに応募してください。"
)
}
static var invalidContact: String {
pick(
ko: "잘못된 연락처 입니다.\n다시 입력해 주세요.",
en: "Invalid contact information.\nPlease enter it again.",
ja: "連絡先が正しくありません。\nもう一度入力してください。"
)
}
static var invalidRecordingFile: String {
pick(
ko: "잘못된 녹음 파일 입니다.\n다시 선택해 주세요.",
en: "Invalid recording file.\nPlease select it again.",
ja: "録音ファイルが正しくありません。\nもう一度選択してください。"
)
}
static var fileLoadFailed: String {
pick(
ko: "오디오 파일을 불러오지 못했습니다.\n다시 선택해 주세요",
en: "Could not load the audio file.\nPlease select it again.",
ja: "オーディオファイルを読み込めませんでした。\nもう一度選択してください。"
)
}
static var applyFailed: String {
pick(
ko: "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.",
en: "Could not complete the audition application.\nPlease try again.\nIf the problem persists, please contact customer support.",
ja: "オーディション応募を完了できませんでした。\nもう一度お試しください。\n問題が続く場合はカスタマーサポートにお問い合わせください。"
)
}
}
enum Recording {
static var title: String {
pick(ko: "오디션 녹음", en: "Audition recording", ja: "オーディション録音")
}
static var recordAgain: String {
pick(ko: "다시 녹음", en: "Record again", ja: "再録音")
}
static var recordComplete: String {
pick(ko: "녹음완료", en: "Recording complete", ja: "録音完了")
}
static var createFileFailed: String {
pick(
ko: "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.",
en: "Could not create the recording file.\nPlease try again.\nIf the problem persists, please contact customer support.",
ja: "録音ファイルを作成できませんでした。\nもう一度お試しください。\n問題が続く場合はカスタマーサポートにお問い合わせください。"
)
}
}
enum Detail {
static var informationTitle: String {
pick(ko: "오디션 정보", en: "Audition information", ja: "オーディション情報")
}
static var characterTitle: String {
pick(ko: "오디션 캐릭터", en: "Audition character", ja: "オーディションキャラクター")
}
static var viewOriginalWork: String {
pick(ko: "원작 보러가기", en: "View original work", ja: "原作を見る")
}
static var checkScript: String {
pick(ko: "오디션 대본 확인", en: "Check audition script", ja: "オーディション台本を確認")
}
static var characterInfo: String {
pick(ko: "오디션 캐릭터 정보", en: "Audition character info", ja: "オーディションキャラクター情報")
}
static var noApplicants: String {
pick(ko: "지원자가 없습니다.", en: "No applicants.", ja: "応募者がいません。")
}
static var participants: String {
pick(ko: "참여자", en: "Participants", ja: "参加者")
}
static var personUnit: String {
pick(ko: "", en: "people", ja: "")
}
static var sortNewest: String {
pick(ko: "최신순", en: "Newest", ja: "新着順")
}
static var sortLikes: String {
pick(ko: "좋아요순", en: "Most liked", ja: "いいね順")
}
static var recruiting: String {
pick(ko: "모집중", en: "Open", ja: "募集中")
}
static var recruitmentClosed: String {
pick(ko: "모집완료", en: "Closed", ja: "募集終了")
}
}
enum Vote {
static var cheerTitle: String {
pick(ko: "[오디션 응원]", en: "[Audition cheer]", ja: "[オーディション応援]")
}
static var cheerDescription: String {
pick(
ko: "오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요.",
en: "You cheered for this audition.\n(Free cheer: once per account per day)\nTry sending an additional cheer with 1 can.",
ja: "オーディションを応援しました。\n無料応援1アカウント1日1回\n1canで追加応援してみましょう。"
)
}
static var limitTitle: String {
pick(ko: "[오늘 응원 제한]", en: "[Today's cheer limit]", ja: "[本日の応援制限]")
}
static var limitDescription: String {
pick(
ko: "오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요.",
en: "That's all for today's cheers!\nYou can use it up to 10 times a day.\nPlease try again tomorrow.",
ja: "本日の応援はここまでです!\n1日最大10回まで利用できます。\n明日またご利用ください。"
)
}
static var unknownError: String {
pick(
ko: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.",
en: "An unknown error occurred. Please try again.",
ja: "不明なエラーが発生しました。もう一度お試しください。"
)
}
}
enum Sound {
static var playbackFailed: String {
pick(
ko: "오류가 발생했습니다. 다시 시도해 주세요.",
en: "An error occurred. Please try again.",
ja: "エラーが発生しました。もう一度お試しください。"
)
}
}
}
enum Report { enum Report {
static var postReportTitle: String { static var postReportTitle: String {
pick(ko: "게시물 신고", en: "Report post", ja: "投稿通報") pick(ko: "게시물 신고", en: "Report post", ja: "投稿通報")

View File

@@ -95,19 +95,19 @@
## 변경 대상 파일 전체 목록 ## 변경 대상 파일 전체 목록
### Audition (13) ### Audition (13)
- [ ] `SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift` - [x] `SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift`
- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplicantItemView.swift` - [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantItemView.swift`
- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift` - [x] `SodaLive/Sources/Audition/Applicant/AuditionApplicantRecordingView.swift`
- [ ] `SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift` - [x] `SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift`
- [ ] `SodaLive/Sources/Audition/AuditionItemView.swift` - [x] `SodaLive/Sources/Audition/AuditionItemView.swift`
- [ ] `SodaLive/Sources/Audition/AuditionView.swift` - [x] `SodaLive/Sources/Audition/AuditionView.swift`
- [ ] `SodaLive/Sources/Audition/AuditionViewModel.swift` - [x] `SodaLive/Sources/Audition/AuditionViewModel.swift`
- [ ] `SodaLive/Sources/Audition/Detail/AuditionDetailView.swift` - [x] `SodaLive/Sources/Audition/Detail/AuditionDetailView.swift`
- [ ] `SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift` - [x] `SodaLive/Sources/Audition/Detail/AuditionDetailViewModel.swift`
- [ ] `SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift` - [x] `SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift`
- [ ] `SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift` - [x] `SodaLive/Sources/Audition/Role/AuditionDetailRoleItemView.swift`
- [ ] `SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift` - [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift`
- [ ] `SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift` - [x] `SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift`
### Chat (28) ### Chat (28)
- [ ] `SodaLive/Sources/Chat/Character/CharacterItemView.swift` - [ ] `SodaLive/Sources/Chat/Character/CharacterItemView.swift`
@@ -536,3 +536,26 @@
- 내부 로그/채널 prefix/API path/아이콘명/색상코드/Preview 샘플만 가진 29개 파일 추가 제외 완료. - 내부 로그/채널 prefix/API path/아이콘명/색상코드/Preview 샘플만 가진 29개 파일 추가 제외 완료.
- 변경 대상 파일 수 `359 → 330`, 상위 모듈 수 `24 → 21`으로 갱신. - 변경 대상 파일 수 `359 → 330`, 상위 모듈 수 `24 → 21`으로 갱신.
- 예외 유지: `Content/Detail/ContentDetailPurchaseButton.swift``"원으로"/"캔으로"` 사용자 노출 텍스트가 있어 유지. - 예외 유지: `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.`로 테스트 액션 미구성 확인(코드 실패 아님, 스킴 제약).