feat(explorer): 크리에이터 상세정보 다이얼로그와 SNS 링크를 추가한다
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// CreatorDetailDialogView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/25/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
import Kingfisher
|
||||
|
||||
struct CreatorDetailDialogView: View {
|
||||
|
||||
@Binding var isShowing: Bool
|
||||
let creatorDetail: GetCreatorDetailResponse?
|
||||
let isLoading: Bool
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
private var closeIconAssetName: String {
|
||||
UIImage(named: "ic_x_white") != nil ? "ic_x_white" : "ic_close_white"
|
||||
}
|
||||
|
||||
private var profileImageSize: CGFloat {
|
||||
screenSize().width - 26.7 - 48
|
||||
}
|
||||
|
||||
private var snsItems: [CreatorDetailSnsItem] {
|
||||
guard let creatorDetail else { return [] }
|
||||
|
||||
var items = [CreatorDetailSnsItem]()
|
||||
appendSnsItem(items: &items, iconName: "ic_sns_instagram", url: creatorDetail.instagramUrl)
|
||||
appendSnsItem(items: &items, iconName: "ic_sns_fancimm", url: creatorDetail.fancimmUrl)
|
||||
appendSnsItem(items: &items, iconName: "ic_sns_x", url: creatorDetail.xurl)
|
||||
appendSnsItem(items: &items, iconName: "ic_sns_youtube", url: creatorDetail.youtubeUrl)
|
||||
appendSnsItem(items: &items, iconName: "ic_sns_kakao", url: creatorDetail.kakaoOpenChatUrl)
|
||||
return items
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black
|
||||
.opacity(0.7)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
isShowing = false
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
Image(closeIconAssetName)
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
isShowing = false
|
||||
}
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.padding(.trailing, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 240)
|
||||
} else if let creatorDetail {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
KFImage(URL(string: creatorDetail.profileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: profileImageSize, height: profileImageSize))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: profileImageSize, height: profileImageSize)
|
||||
.clipped()
|
||||
.cornerRadius(16)
|
||||
|
||||
Text(creatorDetail.nickname)
|
||||
.appFont(size: 36, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.top, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 30) {
|
||||
detailSection(
|
||||
title: I18n.MemberChannel.creatorDetailDebut,
|
||||
value: debutDisplayValue(creatorDetail: creatorDetail)
|
||||
)
|
||||
|
||||
detailSection(
|
||||
title: I18n.MemberChannel.creatorDetailTotalLiveCount,
|
||||
value: creatorDetail.activitySummary.liveCount.comma()
|
||||
)
|
||||
|
||||
detailSection(
|
||||
title: I18n.MemberChannel.creatorDetailAccumulatedLiveTime,
|
||||
value: creatorDetail.activitySummary.liveTime.comma()
|
||||
)
|
||||
|
||||
detailSection(
|
||||
title: I18n.MemberChannel.creatorDetailAccumulatedParticipants,
|
||||
value: creatorDetail.activitySummary.liveContributorCount.comma()
|
||||
)
|
||||
|
||||
detailSection(
|
||||
title: I18n.MemberChannel.creatorDetailRegisteredContentCount,
|
||||
value: creatorDetail.activitySummary.contentCount.comma()
|
||||
)
|
||||
|
||||
if !snsItems.isEmpty {
|
||||
snsSection(items: snsItems)
|
||||
}
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: screenSize().height * 0.72)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(8)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func detailSection(title: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.appFont(size: 16, weight: .medium)
|
||||
.foregroundColor(Color(hex: "B0BEC5"))
|
||||
|
||||
Text(value)
|
||||
.appFont(size: 20, weight: .medium)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func snsSection(items: [CreatorDetailSnsItem]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(I18n.MemberChannel.creatorDetailSns)
|
||||
.appFont(size: 16, weight: .medium)
|
||||
.foregroundColor(Color(hex: "B0BEC5"))
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ForEach(items) { item in
|
||||
Image(item.iconName)
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
openSnsLink(item.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func debutDisplayValue(creatorDetail: GetCreatorDetailResponse) -> String {
|
||||
let debutDate = creatorDetail.debutDate.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let dday = creatorDetail.dday.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !debutDate.isEmpty, !dday.isEmpty else {
|
||||
return I18n.MemberChannel.preDebut
|
||||
}
|
||||
|
||||
return "\(debutDate) (\(dday))"
|
||||
}
|
||||
|
||||
private func appendSnsItem(items: inout [CreatorDetailSnsItem], iconName: String, url: String) {
|
||||
let trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !trimmed.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
items.append(CreatorDetailSnsItem(iconName: iconName, url: trimmed))
|
||||
}
|
||||
|
||||
private func openSnsLink(_ urlString: String) {
|
||||
guard let url = normalizedUrl(urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
private func normalizedUrl(_ urlString: String) -> URL? {
|
||||
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !trimmed.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") {
|
||||
return URL(string: trimmed)
|
||||
}
|
||||
|
||||
return URL(string: "https://\(trimmed)")
|
||||
}
|
||||
}
|
||||
|
||||
private struct CreatorDetailSnsItem: Identifiable {
|
||||
let iconName: String
|
||||
let url: String
|
||||
|
||||
var id: String { iconName }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// GetCreatorDetailResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/25/26.
|
||||
//
|
||||
|
||||
struct GetCreatorDetailResponse: Decodable {
|
||||
let nickname: String
|
||||
let profileImageUrl: String
|
||||
let debutDate: String
|
||||
let dday: String
|
||||
let activitySummary: CreatorDetailActivitySummary
|
||||
let instagramUrl: String
|
||||
let fancimmUrl: String
|
||||
let xurl: String
|
||||
let youtubeUrl: String
|
||||
let kakaoOpenChatUrl: String
|
||||
}
|
||||
|
||||
struct CreatorDetailActivitySummary: Decodable {
|
||||
let liveCount: Int
|
||||
let liveTime: Int
|
||||
let liveContributorCount: Int
|
||||
let contentCount: Int
|
||||
}
|
||||
Reference in New Issue
Block a user