feat(creator): 대화하기 액션을 연결한다

This commit is contained in:
Yu Sung
2026-07-03 18:13:04 +09:00
parent 3f24788c29
commit 37c34ae91e
13 changed files with 242 additions and 24 deletions

View File

@@ -61,6 +61,9 @@
CC0101272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; };
CC0102272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; };
CC0103272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; };
CC0101282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; };
CC0102282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; };
CC0103282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; };
CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; };
CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; };
CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; };
@@ -1798,6 +1801,7 @@
CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelIntroduceSection.swift; sourceTree = "<group>"; };
CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelActivitySection.swift; sourceTree = "<group>"; };
CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSnsSection.swift; sourceTree = "<group>"; };
CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTalkActionSection.swift; sourceTree = "<group>"; };
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = "<group>"; };
0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = "<group>"; };
189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = "<group>"; };
@@ -2730,6 +2734,7 @@
CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */,
CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */,
CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */,
CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -5677,6 +5682,7 @@
CC01011E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */,
CC01011F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */,
CC0101272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */,
CC0101282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */,
CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,
@@ -6487,6 +6493,7 @@
CC01021E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */,
CC01021F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */,
CC0102272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */,
CC0102282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */,
CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,
@@ -7313,6 +7320,7 @@
CC01031E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */,
CC01031F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */,
CC0103272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */,
CC0103282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */,
CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_new_create_live.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_new_upload_audio.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_new_upload_community_post.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_new_x_black.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

View File

@@ -3,6 +3,7 @@ import Kingfisher
struct CreatorChannelHeaderSection: View {
let creator: CreatorChannelCreatorResponse
let onTapTalk: () -> Void
var body: some View {
ZStack(alignment: .bottom) {
@@ -49,7 +50,10 @@ struct CreatorChannelHeaderSection: View {
.foregroundColor(.white)
.multilineTextAlignment(.center)
talkButton
CreatorChannelTalkActionSection(
isAiChatAvailable: creator.isAiChatAvailable,
onTapTalk: onTapTalk
)
}
.frame(maxWidth: .infinity)
.padding(.horizontal, SodaSpacing.s24)
@@ -57,24 +61,4 @@ struct CreatorChannelHeaderSection: View {
}
.frame(width: screenSize().width, height: screenSize().width)
}
private var talkButton: some View {
Button(action: {}) {
HStack(spacing: SodaSpacing.s6) {
Image("ic_new_talk")
.resizable()
.frame(width: 20, height: 20)
Text(I18n.CreatorChannelHome.talk)
.appFont(size: 16, weight: .medium)
.foregroundColor(.white)
}
.padding(SodaSpacing.s12)
.overlay(
Capsule()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}

View File

@@ -1,13 +1,22 @@
import SwiftUI
import Bootpay
import BootpayUI
struct CreatorChannelView: View {
let creatorId: Int
@StateObject private var viewModel = CreatorChannelViewModel()
@StateObject private var channelDonationViewModel = ChannelDonationViewModel()
@StateObject private var mypageViewModel = MyPageViewModel()
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
@Environment(\.dismiss) private var dismiss
@State private var tabBarMinY: CGFloat = .greatestFiniteMagnitude
@State private var isShowChannelDonationDialog = false
@State private var isShowAuthView = false
@State private var isShowAuthConfirmView = false
@State private var pendingAction: (() -> Void)? = nil
@State private var payload = Payload()
private let titleBarHeight: CGFloat = 56
private let tabBarHeight: CGFloat = 52
@@ -68,6 +77,10 @@ struct CreatorChannelView: View {
LoadingView()
}
if isShowAuthConfirmView {
authConfirmDialog
}
if isShowChannelDonationDialog {
LiveRoomDonationDialogView(
isShowing: $isShowChannelDonationDialog,
@@ -92,7 +105,11 @@ struct CreatorChannelView: View {
}
}
.navigationBarBackButtonHidden(true)
.fullScreenCover(isPresented: $isShowAuthView) {
authView
}
.onAppear {
configurePayload()
if viewModel.hasLoaded == false {
viewModel.fetchHome(creatorId: creatorId)
}
@@ -129,7 +146,7 @@ struct CreatorChannelView: View {
@ViewBuilder
private var headerSection: some View {
if let creator = viewModel.response?.creator {
CreatorChannelHeaderSection(creator: creator)
CreatorChannelHeaderSection(creator: creator, onTapTalk: handleTalkTap)
}
}
@@ -168,6 +185,57 @@ struct CreatorChannelView: View {
}
}
private var authConfirmDialog: some View {
SodaDialog(
title: I18n.Chat.Auth.dialogTitle,
desc: I18n.Chat.Auth.dialogDescription,
confirmButtonTitle: I18n.Chat.Auth.goToVerification,
confirmButtonAction: {
isShowAuthConfirmView = false
isShowAuthView = true
},
cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: {
isShowAuthConfirmView = false
pendingAction = nil
},
textAlignment: .center
)
}
private var authView: some View {
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
.onConfirm { _ in true }
.onCancel { _ in isShowAuthView = false }
.onError { _ in
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
AppState.shared.isShowErrorPopup = true
isShowAuthView = false
}
.onDone {
DEBUG_LOG("onDone: \($0)")
mypageViewModel.authVerify($0) {
auth = true
isShowAuthView = false
if let action = pendingAction {
pendingAction = nil
action()
}
}
}
.onClose { isShowAuthView = false }
}
private func configurePayload() {
payload.applicationId = BOOTPAY_APP_ID
payload.price = 0
payload.pg = "다날"
payload.method = "본인인증"
payload.orderName = "본인인증"
payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))"
}
private func selectTab(_ tab: CreatorChannelTab) {
switch tab {
case .audio:
@@ -228,6 +296,43 @@ struct CreatorChannelView: View {
}
}
private func handleTalkTap() {
guard let characterId = viewModel.response?.creator.characterId, characterId > 0 else { return }
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
AppState.shared.setAppStep(step: .login)
return
}
let normalizedCountryCode = UserDefaults
.string(forKey: .countryCode)
.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
if isKoreanCountry && auth == false {
pendingAction = {
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
}
isShowAuthConfirmView = true
return
}
if !UserDefaults.isAdultContentVisible() {
pendingAction = nil
moveToContentSettingsWithGuideToast()
return
}
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
}
private func moveToContentSettingsWithGuideToast() {
AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)
AppState.shared.setAppStep(step: .contentViewSettings)
}
private func showDonationDialog() {
guard let creatorId = viewModel.response?.creator.creatorId else { return }
channelDonationViewModel.setCreatorId(creatorId, shouldFetch: false)

View File

@@ -0,0 +1,37 @@
import SwiftUI
struct CreatorChannelTalkActionSection: View {
let isAiChatAvailable: Bool
let onTapTalk: () -> Void
var body: some View {
HStack {
Spacer()
if isAiChatAvailable {
Button(action: onTapTalk) {
HStack(spacing: SodaSpacing.s6) {
Image("ic_new_talk")
.resizable()
.frame(width: 20, height: 20)
Text(I18n.CreatorChannelHome.talk)
.appFont(size: 16, weight: .medium)
.foregroundColor(.white)
}
.padding(SodaSpacing.s12)
.overlay(
Capsule()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
Spacer()
}
.frame(height: 44)
.frame(maxWidth: .infinity)
.padding(.bottom, 14)
}
}

View File

@@ -541,7 +541,7 @@
### Phase 17: 대화하기 액션 영역
- [ ] **Task 17.1: 대화하기 액션 섹션 생성**
- [x] **Task 17.1: 대화하기 액션 섹션 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift`
@@ -556,7 +556,7 @@
- 실행 명령: `rg "struct CreatorChannelTalkActionSection|isAiChatAvailable|ic_new_talk|ic_new_dm|frame\\(height|onTapTalk" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift`
- 기대 결과: 대화하기만 조건부 표시되고 DM 버튼 구현이 없다.
- [ ] **Task 17.2: 기존 AI 채팅방 진입 guard 연결**
- [x] **Task 17.2: 기존 AI 채팅방 진입 guard 연결**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift`