feat(explorer): 크리에이터 상세정보 다이얼로그와 SNS 링크를 추가한다
This commit is contained in:
21
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_sns_fancimm.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/ic_sns_fancimm.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/ic_sns_fancimm.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_sns_instagram.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/ic_sns_instagram.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/ic_sns_instagram.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_sns_kakao.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/ic_sns_kakao.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/ic_sns_kakao.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_sns_x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/ic_sns_x.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/ic_sns_x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_sns_youtube.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/ic_sns_youtube.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/ic_sns_youtube.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -13,6 +13,7 @@ enum ExplorerApi {
|
||||
case getExplorer
|
||||
case searchChannel(channel: String)
|
||||
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
|
||||
case getCreatorDetail(userId: Int)
|
||||
case getFollowerList(userId: Int, page: Int, size: Int)
|
||||
case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
|
||||
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
|
||||
@@ -40,6 +41,9 @@ extension ExplorerApi: TargetType {
|
||||
case .getCreatorProfile(let userId, _):
|
||||
return "/explorer/profile/\(userId)"
|
||||
|
||||
case .getCreatorDetail(let userId):
|
||||
return "/explorer/profile/\(userId)/detail"
|
||||
|
||||
case .getCreatorProfileDonationRanking(let userId, _, _, _):
|
||||
return "/explorer/profile/\(userId)/donation-rank"
|
||||
|
||||
@@ -62,7 +66,7 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank:
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank:
|
||||
return .get
|
||||
|
||||
case .writeCheers, .writeCreatorNotice:
|
||||
@@ -75,7 +79,7 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
var task: Task {
|
||||
switch self {
|
||||
case .getExplorer, .getCreatorRank:
|
||||
case .getExplorer, .getCreatorRank, .getCreatorDetail:
|
||||
return .requestPlain
|
||||
|
||||
case .searchChannel(let channel):
|
||||
|
||||
@@ -30,6 +30,10 @@ final class ExplorerRepository {
|
||||
)
|
||||
}
|
||||
|
||||
func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getCreatorDetail(userId: id))
|
||||
}
|
||||
|
||||
func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -21,6 +21,7 @@ struct UserProfileView: View {
|
||||
@State private var isShowFollowNotifyDialog: Bool = false
|
||||
@State private var isShowRouletteSettings: Bool = false
|
||||
@State private var isShowMenuSettings: Bool = false
|
||||
@State private var isShowCreatorDetailDialog: Bool = false
|
||||
|
||||
@State private var maxCommunityPostHeight: CGFloat? = nil
|
||||
|
||||
@@ -79,10 +80,12 @@ struct UserProfileView: View {
|
||||
AppState.shared.setAppStep(step: .followerList(userId: creatorProfile.creator.creatorId))
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 9.3) {
|
||||
Text(I18n.MemberChannel.followerCount(creatorProfile.creator.notificationRecipientCount.comma()))
|
||||
Text(I18n.MemberChannel.followerCountWithDetail(creatorProfile.creator.notificationRecipientCount.comma()))
|
||||
.appFont(size: 16, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
.onTapGesture {
|
||||
isShowCreatorDetailDialog = true
|
||||
viewModel.getCreatorDetail(userId: creatorProfile.creator.creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,6 +520,14 @@ struct UserProfileView: View {
|
||||
if isShowMemberProfilePopup {
|
||||
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
|
||||
}
|
||||
|
||||
if isShowCreatorDetailDialog {
|
||||
CreatorDetailDialogView(
|
||||
isShowing: $isShowCreatorDetailDialog,
|
||||
creatorDetail: viewModel.creatorDetail,
|
||||
isLoading: viewModel.isCreatorDetailLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
|
||||
@@ -35,6 +35,8 @@ final class UserProfileViewModel: ObservableObject {
|
||||
@Published var navigationTitle = "채널"
|
||||
|
||||
@Published private(set) var creatorProfile: GetCreatorProfileResponse?
|
||||
@Published private(set) var creatorDetail: GetCreatorDetailResponse?
|
||||
@Published var isCreatorDetailLoading = false
|
||||
@Published private(set) var communityPostList = [GetCommunityPostListResponse]()
|
||||
|
||||
@Published var isShowShareView = false
|
||||
@@ -98,6 +100,47 @@ final class UserProfileViewModel: ObservableObject {
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getCreatorDetail(userId: Int) {
|
||||
creatorDetail = nil
|
||||
isCreatorDetailLoading = true
|
||||
|
||||
repository.getCreatorDetail(id: userId)
|
||||
.sink { [weak self] result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self?.isCreatorDetailLoading = false
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetCreatorDetailResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.creatorDetail = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isCreatorDetailLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func hidePaymentPopup() {
|
||||
isShowPaymentDialog = false
|
||||
isShowPasswordDialog = false
|
||||
|
||||
@@ -884,6 +884,16 @@ enum I18n {
|
||||
static var followerCount: (String) -> String = { count in
|
||||
pick(ko: "팔로워 \(count)명", en: "\(count) followers", ja: "フォロワー\(count)人")
|
||||
}
|
||||
static var followerCountWithDetail: (String) -> String = { count in
|
||||
pick(ko: "팔로워 \(count)명 · 상세정보 >", en: "\(count) followers · Details >", ja: "フォロワー\(count)人 ・ 詳細情報 >")
|
||||
}
|
||||
static var creatorDetailDebut: String { pick(ko: "데뷔", en: "Debut", ja: "デビュー") }
|
||||
static var creatorDetailTotalLiveCount: String { pick(ko: "라이브 총 횟수", en: "Total live sessions", ja: "ライブ総回数") }
|
||||
static var creatorDetailAccumulatedLiveTime: String { pick(ko: "라이브 누적 시간", en: "Total live time", ja: "ライブ累積時間") }
|
||||
static var creatorDetailAccumulatedParticipants: String { pick(ko: "라이브 누적 참여자", en: "Total live participants", ja: "ライブ累積参加者") }
|
||||
static var creatorDetailRegisteredContentCount: String { pick(ko: "등록 콘텐츠 수", en: "Registered contents", ja: "登録コンテンツ数") }
|
||||
static var creatorDetailSns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
||||
static var preDebut: String { pick(ko: "데뷔전", en: "Pre-debut", ja: "デビュー前") }
|
||||
|
||||
static func channelTitle(_ nickname: String) -> String {
|
||||
pick(ko: "\(nickname)님의 채널", en: "\(nickname)'s channel", ja: "\(nickname)のチャンネル")
|
||||
|
||||
22
docs/20260225_크리에이터상세정보다이얼로그추가.md
Normal file
22
docs/20260225_크리에이터상세정보다이얼로그추가.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 2026-02-25 크리에이터 상세정보 다이얼로그 추가
|
||||
|
||||
## 구현 체크리스트
|
||||
- [x] 크리에이터 상세정보 조회 API 연결 (`/explorer/profile/{id}/detail`)
|
||||
- [x] 크리에이터 상세정보 다이얼로그 UI 구현 (`Sources/Explorer/Profile/Detail`)
|
||||
- [x] `UserProfileView`의 팔로워 문구를 `팔로워 OO명 · 상세정보 >`로 변경
|
||||
- [x] 팔로워 문구 탭 시 상세정보 다이얼로그 표시 연결
|
||||
- [x] 국제화(I18n) 키 추가 및 적용
|
||||
- [x] 진단/빌드 검증 수행 및 결과 기록
|
||||
|
||||
## 검증 기록
|
||||
- 무엇/왜/어떻게: `UserProfileView`에서 팔로워 문구 탭 시 크리에이터 상세정보를 보여주도록 API/ViewModel/다이얼로그 UI를 연결했다. `Sources/Explorer/Profile/Detail`에 다이얼로그 UI와 상세 응답 모델을 배치하고, 숫자값 comma 표기/데뷔전 처리/SNS 조건부 노출을 반영했다.
|
||||
- 실행 명령: `pod install`
|
||||
- 결과: 성공 (의존성 설치 및 workspace 통합 완료)
|
||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build -quiet`
|
||||
- 결과: 성공 (빌드 완료, 스크립트/대상 선택 관련 경고만 존재)
|
||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build -quiet`
|
||||
- 결과: 성공 (빌드 완료, 경고만 존재)
|
||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -destination "platform=iOS Simulator,name=iPhone 16" test -quiet`
|
||||
- 결과: 실패 (`Scheme SodaLive is not currently configured for the test action.` — 스킴 테스트 액션 미구성)
|
||||
- 실행 명령: `lsp_diagnostics` (수정 파일 대상)
|
||||
- 결과: 개발 환경 인덱싱에서 외부 모듈(`Kingfisher`, `Moya`, `CombineMoya`) 해석 오류가 발생해 신뢰 가능한 진단을 제공하지 못함. 실제 검증은 `xcodebuild` 빌드 성공으로 대체 확인.
|
||||
Reference in New Issue
Block a user