feat(i18n): 라이브 모듈 하드코딩 문구를 I18n 키로 통일한다

This commit is contained in:
Yu Sung
2026-04-01 11:58:41 +09:00
parent 201f4c8139
commit 540238eb48
20 changed files with 333 additions and 97 deletions

View File

@@ -1767,17 +1767,59 @@ enum I18n {
static var chatDeleteTitle: String { pick(ko: "채팅 삭제", en: "Delete chat", ja: "チャット削除") } static var chatDeleteTitle: String { pick(ko: "채팅 삭제", en: "Delete chat", ja: "チャット削除") }
} }
enum LiveMain {
static var createLiveButton: String {
pick(ko: "라이브 만들기", en: "Create live", ja: "ライブを作成")
}
static var replaySectionTitle: String {
pick(ko: "라이브 다시 듣기", en: "Live replay", ja: "ライブ再生")
}
}
enum LiveNow { enum LiveNow {
static var allTitle: String { static var allTitle: String {
pick(ko: "지금 라이브 중 전체보기", en: "Live Now - All", ja: "ライブ配信中(全て)") pick(ko: "지금 라이브 중 전체보기", en: "Live Now - All", ja: "ライブ配信中(全て)")
} }
static var sectionTitle: String {
pick(ko: "지금 라이브중", en: "Live now", ja: "ライブ配信中")
}
static var remaining: String { static var remaining: String {
pick(ko: "잔여", en: "Remaining", ja: "残り") pick(ko: "잔여", en: "Remaining", ja: "残り")
} }
static var emptyStateMessage: String {
pick(
ko: "마이페이지에서 본인인증을 하거나\n라이브를 예약하고 참여해보세요.",
en: "Verify your identity in My Page\nor reserve and join a live.",
ja: "マイページで本人認証を行うか、\nライブを予約して参加してください。"
)
}
static var refreshButton: String {
pick(ko: "새로고침", en: "Refresh", ja: "更新")
}
static var followingChannelsTitle: String {
pick(ko: "팔로잉 채널", en: "Following channels", ja: "フォロー中のチャンネル")
}
static var liveBadge: String {
pick(ko: "Live", en: "Live", ja: "Live")
}
static var moreButton: String {
pick(ko: "더보기", en: "More", ja: "もっと見る")
}
} }
enum LiveCancel { enum LiveCancel {
static var title: String {
pick(ko: "예약취소", en: "Cancel reservation", ja: "予約キャンセル")
}
static var reasonPlaceholder: String { static var reasonPlaceholder: String {
pick( pick(
ko: "취소사유를 입력하세요", ko: "취소사유를 입력하세요",
@@ -1786,6 +1828,14 @@ enum I18n {
) )
} }
static var cancelButton: String {
pick(ko: "취소", en: "Cancel", ja: "キャンセル")
}
static var confirmButton: String {
pick(ko: "확인", en: "Confirm", ja: "確認")
}
static var reservationCanceled: String { static var reservationCanceled: String {
pick( pick(
ko: "예약이 취소되었습니다.", ko: "예약이 취소되었습니다.",
@@ -1795,6 +1845,154 @@ enum I18n {
} }
} }
enum LiveReservation {
enum Section {
static var title: String {
pick(ko: "라이브 예약중", en: "Live reservations", ja: "ライブ予約中")
}
static var emptyStateMessage: String {
pick(
ko: "지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.",
en: "There are no live reservations right now.\nFollow channels and receive live notifications.",
ja: "現在予約中のライブはありません。\nチャンネルをフォローしてライブ通知を受け取りましょう。"
)
}
}
enum All {
static var title: String {
pick(ko: "라이브, 예약 캘린더", en: "Live reservation calendar", ja: "ライブ予約カレンダー")
}
static var emptyStateMessage: String {
pick(
ko: "지금 예약중인 라이브가 없습니다.\n다른 날짜의 라이브를 예약하고 참여해 보세요.",
en: "There are no live reservations right now.\nReserve a live on another date and join.",
ja: "現在予約中のライブはありません。\n別の日のライブを予約して参加してみてください。"
)
}
}
enum Item {
static var reservationCompleted: String {
pick(ko: "예약완료", en: "Reserved", ja: "予約完了")
}
static var ownCreatedLive: String {
pick(ko: "내가 개설한 라이브", en: "Live I created", ja: "自分が開設したライブ")
}
static var free: String {
CreateContent.free
}
static func month(_ value: String) -> String {
pick(
ko: "\(value)",
en: "\(value)M",
ja: "\(value)"
)
}
static func priceWithCan(_ can: Int) -> String {
pick(
ko: "\(can)",
en: "\(can) cans",
ja: "\(can)can"
)
}
}
enum Complete {
static var title: String {
pick(ko: "라이브 예약 완료", en: "Live reservation complete", ja: "ライブ予約完了")
}
static var completedMessage: String {
pick(ko: "예약이 완료되었습니다.", en: "Your reservation is complete.", ja: "予約が完了しました。")
}
static var reservationInfoTitle: String {
pick(ko: "라이브 예약정보", en: "Reservation details", ja: "ライブ予約情報")
}
static var channelLabel: String {
pick(ko: "채널", en: "Channel", ja: "チャンネル")
}
static var purchaseDetailLabel: String {
pick(ko: "구매내역", en: "Purchase", ja: "購入内容")
}
static var reservationDateLabel: String {
pick(ko: "예약일자", en: "Reservation date", ja: "予約日時")
}
static var liveCostLabel: String {
pick(ko: "라이브 비용", en: "Live price", ja: "ライブ料金")
}
static var paymentInfoTitle: String {
pick(ko: "결제정보", en: "Payment info", ja: "決済情報")
}
static var ownedCanLabel: String {
pick(ko: "보유캔", en: "Owned cans", ja: "保有can")
}
static var paymentCanLabel: String {
pick(ko: "결제캔", en: "Paid cans", ja: "決済can")
}
static var remainingCanLabel: String {
pick(ko: "잔여캔", en: "Remaining cans", ja: "残りcan")
}
static var canSuffix: String {
pick(ko: "", en: " cans", ja: " can")
}
static var goHome: String {
pick(ko: "홈으로 이동", en: "Go to Home", ja: "ホームへ移動")
}
static var goReservationList: String {
pick(ko: "예약 내역 이동", en: "View reservations", ja: "予約履歴へ移動")
}
}
}
enum LiveChat {
static var staffBadge: String {
pick(ko: "스탭", en: "Staff", ja: "スタッフ")
}
static var donationMemberSuffix: String {
pick(ko: "님이", en: "", ja: "さんが")
}
static func canWithUnit(_ can: Int) -> String {
pick(ko: "\(can)", en: "\(can) cans", ja: "\(can)can")
}
static var secretMissionDonationSuffix: String {
pick(ko: "으로 비밀미션을 보냈습니다.🤫", en: " sent a secret mission.🤫", ja: "で秘密ミッションを送りました。🤫")
}
static var donationSuffix: String {
pick(ko: "을 후원하셨습니다.💰🪙", en: " donated.💰🪙", ja: "を後援しました。💰🪙")
}
static var heartDonationSuffix: String {
pick(ko: "'님이 마음을 전했습니다 : 💕", en: "' sent a heart : 💕", ja: "'さんがハートを送りました : 💕")
}
static var joinSuffix: String {
pick(ko: "'님이 입장하셨습니다.", en: "' joined.", ja: "'さんが入場しました。")
}
}
enum CreateContent { enum CreateContent {
static var selectFile: String { pick(ko: "파일 선택", en: "Select file", ja: "ファイル選択") } static var selectFile: String { pick(ko: "파일 선택", en: "Select file", ja: "ファイル選択") }
static var selectTheme: String { pick(ko: "테마 선택", en: "Select theme", ja: "テーマ選択") } static var selectTheme: String { pick(ko: "테마 선택", en: "Select theme", ja: "テーマ選択") }
@@ -1899,6 +2097,7 @@ enum I18n {
static var cannotReserveOwnLive: String { pick(ko: "내가 만든 라이브는 예약할 수 없습니다.", en: "reserve a live you created is required.", ja: "自分が作ったライブは予約できません。") } static var cannotReserveOwnLive: String { pick(ko: "내가 만든 라이브는 예약할 수 없습니다.", en: "reserve a live you created is required.", ja: "自分が作ったライブは予約できません。") }
static var enterLiveFailed: String { pick(ko: "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", en: "Could not enter the live.\nIf the problem persists, please contact customer support.", ja: "ライブに入室できませんでした。\n問題が続く場合はカスタマーサポートにお問い合わせください。") } static var enterLiveFailed: String { pick(ko: "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.", en: "Could not enter the live.\nIf the problem persists, please contact customer support.", ja: "ライブに入室できませんでした。\n問題が続く場合はカスタマーサポートにお問い合わせください。") }
static var fetchLiveInfoFailed: String { pick(ko: "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요.", en: "Failed to fetch live information.\nPlease try again.", ja: "ライブ情報を取得できませんでした。\nもう一度お試しください。") } static var fetchLiveInfoFailed: String { pick(ko: "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요.", en: "Failed to fetch live information.\nPlease try again.", ja: "ライブ情報を取得できませんでした。\nもう一度お試しください。") }
static var alreadyEndedLive: String { pick(ko: "이미 종료된 라이브 입니다.", en: "This live has already ended.", ja: "このライブはすでに終了しています。") }
static var userBlocked: String { pick(ko: "차단하였습니다.", en: "User has been blocked.", ja: "ブロックしました。") } static var userBlocked: String { pick(ko: "차단하였습니다.", en: "User has been blocked.", ja: "ブロックしました。") }
static var userUnblocked: String { pick(ko: "차단이 해제 되었습니다.", en: "User has been unblocked.", ja: "ブロックを解除しました。") } static var userUnblocked: String { pick(ko: "차단이 해제 되었습니다.", en: "User has been unblocked.", ja: "ブロックを解除しました。") }
static var blockDialogTitle: String { pick(ko: "사용자 차단", en: "Block User", ja: "ユーザーブロック") } static var blockDialogTitle: String { pick(ko: "사용자 차단", en: "Block User", ja: "ユーザーブロック") }

View File

@@ -19,7 +19,7 @@ struct LiveCancelDialog: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Text("예약취소") Text(I18n.LiveCancel.title)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.padding(.top, 40) .padding(.top, 40)
@@ -39,7 +39,7 @@ struct LiveCancelDialog: View {
.padding(.top, 13.3) .padding(.top, 13.3)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("취소") Text(I18n.LiveCancel.cancelButton)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -54,7 +54,7 @@ struct LiveCancelDialog: View {
isShowCancelPopup = false isShowCancelPopup = false
} }
Text("확인") Text(I18n.LiveCancel.confirmButton)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.vertical, 16) .padding(.vertical, 16)

View File

@@ -19,7 +19,7 @@ struct LiveReplayListView: View {
var body: some View { var body: some View {
VStack(spacing: 14) { VStack(spacing: 14) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("라이브 다시 듣기") Text(I18n.LiveMain.replaySectionTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)

View File

@@ -141,7 +141,7 @@ struct LiveView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("라이브 만들기") Text(I18n.LiveMain.createLiveButton)
.appFont(size: 13.3, weight: .bold) .appFont(size: 13.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
} }

View File

@@ -42,7 +42,7 @@ final class LiveViewModel: ObservableObject {
@Published var liveStartDate: String? = nil @Published var liveStartDate: String? = nil
@Published var nowDate: String? = nil @Published var nowDate: String? = nil
let paymentDialogCancelTitle = "취소" let paymentDialogCancelTitle = I18n.Common.cancel
var page = 1 var page = 1
var isLast = false var isLast = false
@@ -104,13 +104,13 @@ final class LiveViewModel: 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.MemberChannel.enterLiveFailed
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.MemberChannel.enterLiveFailed
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -151,13 +151,13 @@ final class LiveViewModel: 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.MemberChannel.enterLiveFailed
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.MemberChannel.enterLiveFailed
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -189,13 +189,13 @@ final class LiveViewModel: 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
} }
} }
@@ -245,13 +245,13 @@ final class LiveViewModel: 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
} }
} }
@@ -297,13 +297,13 @@ final class LiveViewModel: 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
} }
} }
@@ -314,7 +314,7 @@ final class LiveViewModel: ObservableObject {
func reservationLiveRoom(roomId: Int) { func reservationLiveRoom(roomId: Int) {
getRoomDetail(roomId: roomId) { [unowned self] in getRoomDetail(roomId: roomId) { [unowned self] in
if ($0.manager.id == UserDefaults.int(forKey: .userId)) { if ($0.manager.id == UserDefaults.int(forKey: .userId)) {
self.errorMessage = "내가 만든 라이브는 예약할 수 없습니다." self.errorMessage = I18n.MemberChannel.cannotReserveOwnLive
self.isShowPopup = true self.isShowPopup = true
} else { } else {
if $0.isPrivateRoom { if $0.isPrivateRoom {
@@ -326,9 +326,9 @@ final class LiveViewModel: ObservableObject {
if ($0.price == 0 || $0.isPaid) { if ($0.price == 0 || $0.isPaid) {
self.reservation(roomId: roomId) self.reservation(roomId: roomId)
} else { } else {
self.paymentDialogTitle = "\($0.price)캔으로 예약" self.paymentDialogTitle = I18n.MemberChannel.reserveWithCansTitle($0.price)
self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다." self.paymentDialogDesc = I18n.MemberChannel.reservePaymentDesc($0.title)
self.paymentDialogConfirmTitle = "결제 후 예약하기" self.paymentDialogConfirmTitle = I18n.MemberChannel.reservePaymentConfirmTitle
self.paymentDialogConfirmAction = { [unowned self] in self.paymentDialogConfirmAction = { [unowned self] in
hidePopup() hidePopup()
reservation(roomId: roomId) reservation(roomId: roomId)
@@ -375,13 +375,13 @@ final class LiveViewModel: ObservableObject {
if hours >= 1 { if hours >= 1 {
self.liveStartDate = beginDate.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm") self.liveStartDate = beginDate.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm")
self.nowDate = now.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm") self.nowDate = now.convertDateFormat(dateFormat: "yyyy-MM-dd, HH:mm")
self.paymentDialogDesc2 = "라이브를 시작한 지 \(hours)시간 \(minutes)분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다." self.paymentDialogDesc2 = I18n.MemberChannel.elapsedLiveWarning(hours: hours, minutes: minutes)
} }
} }
self.paymentDialogTitle = "유료 라이브 입장" self.paymentDialogTitle = I18n.MemberChannel.paidLiveEnterTitle
self.paymentDialogDesc = "\($0.price)캔을 차감하고\n라이브에 입장 하시겠습니까?" self.paymentDialogDesc = I18n.MemberChannel.paidLiveEnterDesc($0.price)
self.paymentDialogConfirmTitle = "결제 후 참여하기" self.paymentDialogConfirmTitle = I18n.MemberChannel.paidLiveConfirmTitle
self.paymentDialogConfirmAction = { [unowned self] in self.paymentDialogConfirmAction = { [unowned self] in
hidePopup() hidePopup()
self.enterRoom(roomId: roomId) self.enterRoom(roomId: roomId)
@@ -425,18 +425,18 @@ final class LiveViewModel: ObservableObject {
} else { } else {
if let message = decoded.message { if let message = decoded.message {
if message.contains("종료") { if message.contains("종료") {
self.errorMessage = "이미 종료된 라이브 입니다." self.errorMessage = I18n.MemberChannel.alreadyEndedLive
} else { } else {
self.errorMessage = message self.errorMessage = message
} }
} else { } else {
self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요." self.errorMessage = I18n.MemberChannel.fetchLiveInfoFailed
} }
self.isShowPopup = true self.isShowPopup = true
} }
} catch { } catch {
self.errorMessage = "라이브 정보를 가져오지 못했습니다.\n다시 시도해 주세요." self.errorMessage = I18n.MemberChannel.fetchLiveInfoFailed
self.isShowPopup = true self.isShowPopup = true
} }
@@ -471,13 +471,13 @@ final class LiveViewModel: 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

@@ -105,15 +105,14 @@ struct LiveNowAllView: View {
if isShowAuthConfirmView { if isShowAuthConfirmView {
SodaDialog( SodaDialog(
title: "본인인증", title: I18n.Main.Auth.dialogTitle,
desc: "청소년 보호를 위해\n본인인증을 완료한\n성인만 라이브 입장이 가능합니다.\n" + desc: I18n.Main.Auth.liveEntryVerificationDescription,
"라이브 입장을 위해\n본인인증을 진행해 주세요.", confirmButtonTitle: I18n.Main.Auth.goToVerification,
confirmButtonTitle: "본인인증 하러가기",
confirmButtonAction: { confirmButtonAction: {
isShowAuthConfirmView = false isShowAuthConfirmView = false
isShowAuthView = true isShowAuthView = true
}, },
cancelButtonTitle: "취소", cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: { cancelButtonAction: {
isShowAuthConfirmView = false isShowAuthConfirmView = false
pendingAction = nil pendingAction = nil
@@ -142,7 +141,7 @@ struct LiveNowAllView: View {
isShowAuthView = false isShowAuthView = false
} }
.onError { _ in .onError { _ in
AppState.shared.errorMessage = "본인인증 중 오류가 발생했습니다." AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
AppState.shared.isShowErrorPopup = true AppState.shared.isShowErrorPopup = true
isShowAuthView = false isShowAuthView = false
} }

View File

@@ -118,7 +118,7 @@ struct LiveNowItemView: View {
.padding(.horizontal, 2) .padding(.horizontal, 2)
.padding(.bottom, 2) .padding(.bottom, 2)
} else { } else {
Text("무료") Text(I18n.LiveReservation.Item.free)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "#263238")) .foregroundColor(Color(hex: "#263238"))
.padding(.vertical, 4) .padding(.vertical, 4)

View File

@@ -19,14 +19,14 @@ struct SectionLiveNowView: View {
var body: some View { var body: some View {
LazyVStack(spacing: 13.3) { LazyVStack(spacing: 13.3) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("지금 라이브중") Text(I18n.LiveNow.sectionTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
if items.count > 0 { if items.count > 0 {
Text("전체보기") Text(I18n.Common.viewAll)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
.onTapGesture { AppState.shared.setAppStep(step: .liveNowAll(onClickParticipant: onClickParticipant)) } .onTapGesture { AppState.shared.setAppStep(step: .liveNowAll(onClickParticipant: onClickParticipant)) }
@@ -54,7 +54,7 @@ struct SectionLiveNowView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("마이페이지에서 본인인증을 하거나\n라이브를 예약하고 참여해보세요.") Text(I18n.LiveNow.emptyStateMessage)
.appFont(size: 13, weight: .medium) .appFont(size: 13, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
@@ -72,7 +72,7 @@ struct SectionLiveNowView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image("ic_refresh") Image("ic_refresh")
Text("새로고침") Text(I18n.LiveNow.refreshButton)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.grayd2) .foregroundColor(Color.grayd2)
} }

View File

@@ -17,7 +17,7 @@ struct SectionRecommendChannelView: View {
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("팔로잉 채널") Text(I18n.LiveNow.followingChannelsTitle)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
@@ -55,7 +55,7 @@ struct SectionRecommendChannelView: View {
) )
if item.isOnAir { if item.isOnAir {
Text("Live") Text(I18n.LiveNow.liveBadge)
.appFont(size: 8.7, weight: .bold) .appFont(size: 8.7, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.vertical, 2.7) .padding(.vertical, 2.7)
@@ -87,7 +87,7 @@ struct SectionRecommendChannelView: View {
.resizable() .resizable()
.frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .center) .frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .center)
Text("더보기") Text(I18n.LiveNow.moreButton)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.frame(width: screenSize().width * 0.18) .frame(width: screenSize().width * 0.18)

View File

@@ -55,7 +55,7 @@ struct LiveReservationAllItemView: View {
Spacer() Spacer()
if item.isReservation { if item.isReservation {
Text("예약완료") Text(I18n.LiveReservation.Item.reservationCompleted)
.appFont(size: 11.3, weight: .medium) .appFont(size: 11.3, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2")) .foregroundColor(Color(hex: "d2d2d2"))
.padding(.horizontal, 7) .padding(.horizontal, 7)
@@ -63,7 +63,7 @@ struct LiveReservationAllItemView: View {
.background(Color(hex: "533d89")) .background(Color(hex: "533d89"))
.cornerRadius(10) .cornerRadius(10)
} else { } else {
Text(item.price > 0 ? "\(item.price)" : "무료") Text(item.price > 0 ? I18n.LiveReservation.Item.priceWithCan(item.price) : I18n.LiveReservation.Item.free)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "e2e2e2").opacity(0.49)) .foregroundColor(Color(hex: "e2e2e2").opacity(0.49))
.padding(.bottom, 6.7) .padding(.bottom, 6.7)

View File

@@ -21,7 +21,7 @@ struct LiveReservationAllView: View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
GeometryReader { proxy in GeometryReader { proxy in
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "라이브, 예약 캘린더") DetailNavigationBar(title: I18n.LiveReservation.All.title)
WeekCalendarView { date in WeekCalendarView { date in
viewModel.selectedDateString = date viewModel.selectedDateString = date
@@ -57,7 +57,7 @@ struct LiveReservationAllView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("지금 예약중인 라이브가 없습니다.\n다른 날짜의 라이브를 예약하고 참여해 보세요.") Text(I18n.LiveReservation.All.emptyStateMessage)
.appFont(size: 13, weight: .medium) .appFont(size: 13, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -14,13 +14,13 @@ struct LiveReservationCompleteView: View {
var body: some View { var body: some View {
BaseView { BaseView {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "라이브 예약 완료") { DetailNavigationBar(title: I18n.LiveReservation.Complete.title) {
AppState.shared.setAppStep(step: .main) AppState.shared.setAppStep(step: .main)
} }
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) { VStack(spacing: 0) {
Text("예약이 완료되었습니다.") Text(I18n.LiveReservation.Complete.completedMessage)
.appFont(size: 20, weight: .bold) .appFont(size: 20, weight: .bold)
.foregroundColor(Color(hex: "a285eb")) .foregroundColor(Color(hex: "a285eb"))
.frame(width: screenSize().width - 26.7, alignment: .leading) .frame(width: screenSize().width - 26.7, alignment: .leading)
@@ -33,14 +33,14 @@ struct LiveReservationCompleteView: View {
.padding(.top, 16.7) .padding(.top, 16.7)
.padding(.bottom, 26.7) .padding(.bottom, 26.7)
Text("라이브 예약정보") Text(I18n.LiveReservation.Complete.reservationInfoTitle)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
VStack(spacing: 6.7) { VStack(spacing: 6.7) {
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Text("채널") Text(I18n.LiveReservation.Complete.channelLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -51,7 +51,7 @@ struct LiveReservationCompleteView: View {
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Text("구매내역") Text(I18n.LiveReservation.Complete.purchaseDetailLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -62,7 +62,7 @@ struct LiveReservationCompleteView: View {
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Text("예약일자") Text(I18n.LiveReservation.Complete.reservationDateLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -77,7 +77,7 @@ struct LiveReservationCompleteView: View {
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
HStack(spacing: 26.7) { HStack(spacing: 26.7) {
Text("라이브 비용") Text(I18n.LiveReservation.Complete.liveCostLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -94,14 +94,14 @@ struct LiveReservationCompleteView: View {
.foregroundColor(Color(hex: "232323")) .foregroundColor(Color(hex: "232323"))
.padding(.vertical, 20) .padding(.vertical, 20)
Text("결제정보") Text(I18n.LiveReservation.Complete.paymentInfoTitle)
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("보유캔") Text(I18n.LiveReservation.Complete.ownedCanLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -111,14 +111,14 @@ struct LiveReservationCompleteView: View {
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Text("") Text(I18n.LiveReservation.Complete.canSuffix)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("결제캔") Text(I18n.LiveReservation.Complete.paymentCanLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -128,14 +128,14 @@ struct LiveReservationCompleteView: View {
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Text("") Text(I18n.LiveReservation.Complete.canSuffix)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
.frame(width: screenSize().width - 53.4, alignment: .leading) .frame(width: screenSize().width - 53.4, alignment: .leading)
HStack(spacing: 0) { HStack(spacing: 0) {
Text("잔여캔") Text(I18n.LiveReservation.Complete.remainingCanLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
@@ -145,7 +145,7 @@ struct LiveReservationCompleteView: View {
.appFont(size: 15.3, weight: .bold) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Text("") Text(I18n.LiveReservation.Complete.canSuffix)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
@@ -154,7 +154,7 @@ struct LiveReservationCompleteView: View {
.padding(.top, 20) .padding(.top, 20)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("홈으로 이동") Text(I18n.LiveReservation.Complete.goHome)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "9970ff"))
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -170,7 +170,7 @@ struct LiveReservationCompleteView: View {
AppState.shared.setAppStep(step: .main) AppState.shared.setAppStep(step: .main)
} }
Text("예약 내역 이동") Text(I18n.LiveReservation.Complete.goReservationList)
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.vertical, 16) .padding(.vertical, 16)

View File

@@ -67,7 +67,7 @@ struct LiveReservationItemView: View {
VStack(alignment: .trailing, spacing: 8) { VStack(alignment: .trailing, spacing: 8) {
VStack(spacing: 0) { VStack(spacing: 0) {
Text("\(dateDic["month"] ?? "")") Text(I18n.LiveReservation.Item.month(dateDic["month"] ?? ""))
.appFont(size: 14, weight: .bold) .appFont(size: 14, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.vertical, 6) .padding(.vertical, 6)
@@ -100,7 +100,7 @@ struct LiveReservationItemView: View {
.background(Color(hex: "3b5ff1")) .background(Color(hex: "3b5ff1"))
.cornerRadius(4) .cornerRadius(4)
} else { } else {
Text("무료") Text(I18n.LiveReservation.Item.free)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "#263238")) .foregroundColor(Color(hex: "#263238"))
.padding(4) .padding(4)

View File

@@ -21,7 +21,7 @@ struct MyLiveReservationItemView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
Image("ic_mic_colored") Image("ic_mic_colored")
Text("내가 개설한 라이브") Text(I18n.LiveReservation.Item.ownCreatedLive)
.appFont(size: 18, weight: .bold) .appFont(size: 18, weight: .bold)
.foregroundColor(Color(hex: "80D8FF")) .foregroundColor(Color(hex: "80D8FF"))
} }
@@ -79,7 +79,7 @@ struct MyLiveReservationItemView: View {
VStack(alignment: .trailing, spacing: 8) { VStack(alignment: .trailing, spacing: 8) {
VStack(spacing: 4) { VStack(spacing: 4) {
Text("\(dateDic["month"] ?? "")") Text(I18n.LiveReservation.Item.month(dateDic["month"] ?? ""))
.appFont(size: 14, weight: .bold) .appFont(size: 14, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.vertical, 6) .padding(.vertical, 6)
@@ -112,7 +112,7 @@ struct MyLiveReservationItemView: View {
.background(Color(hex: "3b5ff1")) .background(Color(hex: "3b5ff1"))
.cornerRadius(4) .cornerRadius(4)
} else { } else {
Text("무료") Text(I18n.LiveReservation.Item.free)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "#263238")) .foregroundColor(Color(hex: "#263238"))
.padding(4) .padding(4)

View File

@@ -20,14 +20,14 @@ struct SectionLiveReservationView: View {
var body: some View { var body: some View {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("라이브 예약중") Text(I18n.LiveReservation.Section.title)
.appFont(size: 24, weight: .bold) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
if items.count > 0 { if items.count > 0 {
Text("전체보기") Text(I18n.Common.viewAll)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
.onTapGesture { .onTapGesture {
@@ -101,7 +101,7 @@ struct SectionLiveReservationView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.") Text(I18n.LiveReservation.Section.emptyStateMessage)
.appFont(size: 13, weight: .medium) .appFont(size: 13, weight: .medium)
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)

View File

@@ -104,7 +104,7 @@ struct LiveRoomChatItemView: View {
VStack(alignment: .leading, spacing: 6.7) { VStack(alignment: .leading, spacing: 6.7) {
HStack(spacing: 5) { HStack(spacing: 5) {
if chatMessage.rank == -3 { if chatMessage.rank == -3 {
Text("스탭") Text(I18n.LiveChat.staffBadge)
.appFont(size: 10) .appFont(size: 10)
.foregroundColor(.white) .foregroundColor(.white)
.padding(2) .padding(2)

View File

@@ -40,17 +40,17 @@ struct LiveRoomDonationChatItemView: View {
.appFont(size: 12) .appFont(size: 12)
.foregroundColor(.white) .foregroundColor(.white)
Text("님이") Text(I18n.LiveChat.donationMemberSuffix)
.appFont(size: 12, weight: .light) .appFont(size: 12, weight: .light)
.foregroundColor(.white) .foregroundColor(.white)
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text("\(chatMessage.can)") Text(I18n.LiveChat.canWithUnit(chatMessage.can))
.appFont(size: 15) .appFont(size: 15)
.foregroundColor(Color(hex: "fdca2f")) .foregroundColor(Color(hex: "fdca2f"))
Text(chatMessage.chat.contains("비밀") ? "으로 비밀미션을 보냈습니다.🤫" : "을 후원하셨습니다.💰🪙") Text(chatMessage.chat.contains("비밀") ? I18n.LiveChat.secretMissionDonationSuffix : I18n.LiveChat.donationSuffix)
.appFont(size: 15) .appFont(size: 15)
.foregroundColor(.white) .foregroundColor(.white)
} }

View File

@@ -21,7 +21,7 @@ struct LiveRoomHeartDonationChatItemView: View {
.appFont(size: 12, weight: .bold) .appFont(size: 12, weight: .bold)
.foregroundColor(Color(hex: "ec3aa6")) .foregroundColor(Color(hex: "ec3aa6"))
Text("'님이 마음을 전했습니다 : 💕") Text(I18n.LiveChat.heartDonationSuffix)
.appFont(size: 12) .appFont(size: 12)
.foregroundColor(Color.gray11) .foregroundColor(Color.gray11)
} }

View File

@@ -28,7 +28,7 @@ struct LiveRoomJoinChatItemView: View {
.appFont(size: 12, weight: .bold) .appFont(size: 12, weight: .bold)
.foregroundColor(Color.mainYellow) .foregroundColor(Color.mainYellow)
Text("'님이 입장하셨습니다.") Text(I18n.LiveChat.joinSuffix)
.appFont(size: 12) .appFont(size: 12)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
} }

View File

@@ -319,28 +319,28 @@
### Live (56) ### Live (56)
#### Group 1 (1-10) #### Group 1 (1-10)
- [ ] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift` - [x] `SodaLive/Sources/Live/Cancel/LiveCancelDialog.swift`
- [ ] `SodaLive/Sources/Live/LatestFinishedLiveItemView.swift` - [x] `SodaLive/Sources/Live/LatestFinishedLiveItemView.swift`
- [ ] `SodaLive/Sources/Live/LiveReplayListView.swift` - [x] `SodaLive/Sources/Live/LiveReplayListView.swift`
- [ ] `SodaLive/Sources/Live/LiveView.swift` - [x] `SodaLive/Sources/Live/LiveView.swift`
- [ ] `SodaLive/Sources/Live/LiveViewModel.swift` - [x] `SodaLive/Sources/Live/LiveViewModel.swift`
- [ ] `SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift` - [x] `SodaLive/Sources/Live/Now/All/LiveNowAllItemView.swift`
- [ ] `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` - [x] `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift`
- [ ] `SodaLive/Sources/Live/Now/LiveNowItemView.swift` - [x] `SodaLive/Sources/Live/Now/LiveNowItemView.swift`
- [ ] `SodaLive/Sources/Live/Now/SectionLiveNowView.swift` - [x] `SodaLive/Sources/Live/Now/SectionLiveNowView.swift`
- [ ] `SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift` - [x] `SodaLive/Sources/Live/RecommendChannel/SectionRecommendChannelView.swift`
#### Group 2 (11-20) #### Group 2 (11-20)
- [ ] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift` - [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllItemView.swift`
- [ ] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift` - [x] `SodaLive/Sources/Live/Reservation/All/LiveReservationAllView.swift`
- [ ] `SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift` - [x] `SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift`
- [ ] `SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift` - [x] `SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift`
- [ ] `SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift` - [x] `SodaLive/Sources/Live/Reservation/MyLiveReservationItemView.swift`
- [ ] `SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift` - [x] `SodaLive/Sources/Live/Reservation/SectionLiveReservationView.swift`
- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift` - [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomChatItemView.swift`
- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift` - [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomDonationChatItemView.swift`
- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift` - [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomHeartDonationChatItemView.swift`
- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift` - [x] `SodaLive/Sources/Live/Room/Chat/LiveRoomJoinChatItemView.swift`
#### Group 3 (21-30) #### Group 3 (21-30)
- [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift` - [ ] `SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift`
@@ -892,3 +892,41 @@
- 공통 키 재사용 정리: `I18n.Common.viewAll`, `I18n.Common.latestContent`, `I18n.Settings.companyInfo`, `I18n.Chat.Auth.*`, `I18n.LiveRoom.follow/following` 적용. - 공통 키 재사용 정리: `I18n.Common.viewAll`, `I18n.Common.latestContent`, `I18n.Settings.companyInfo`, `I18n.Chat.Auth.*`, `I18n.LiveRoom.follow/following` 적용.
- Oracle 후속 보정: 홈 FAB 버튼 문구를 제목형 키(`uploadTitle`)에서 CTA 전용 키(`I18n.CreateContent.uploadAction`)로 분리해 영문/일문 의미를 버튼 행동과 일치시킴. - Oracle 후속 보정: 홈 FAB 버튼 문구를 제목형 키(`uploadTitle`)에서 CTA 전용 키(`I18n.CreateContent.uploadAction`)로 분리해 영문/일문 의미를 버튼 행동과 일치시킴.
- Home Group 1 체크박스 9개 `- [x]` 완료 반영. - 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` 실컴파일 통과로 검증 완료.