feat(creator): 공지 섹션을 연결한다
This commit is contained in:
@@ -31,6 +31,9 @@
|
||||
CC0101162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; };
|
||||
CC0102162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; };
|
||||
CC0103162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; };
|
||||
CC0101172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */; };
|
||||
CC0102172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */; };
|
||||
CC0103172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.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 */; };
|
||||
@@ -1758,6 +1761,7 @@
|
||||
CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCurrentLiveSection.swift; sourceTree = "<group>"; };
|
||||
CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLatestAudioSection.swift; sourceTree = "<group>"; };
|
||||
CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelDonationSection.swift; sourceTree = "<group>"; };
|
||||
CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelNoticeSection.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>"; };
|
||||
@@ -2680,6 +2684,7 @@
|
||||
CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */,
|
||||
CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */,
|
||||
CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */,
|
||||
CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -5617,6 +5622,7 @@
|
||||
CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */,
|
||||
CC0101152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */,
|
||||
CC0101162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */,
|
||||
CC0101172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */,
|
||||
CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
|
||||
CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
|
||||
CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,
|
||||
@@ -6417,6 +6423,7 @@
|
||||
CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */,
|
||||
CC0102152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */,
|
||||
CC0102162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */,
|
||||
CC0102172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */,
|
||||
CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
|
||||
CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
|
||||
CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,
|
||||
@@ -7233,6 +7240,7 @@
|
||||
CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */,
|
||||
CC0103152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */,
|
||||
CC0103162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */,
|
||||
CC0103172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */,
|
||||
CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */,
|
||||
CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */,
|
||||
CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatorChannelNoticeSection: View {
|
||||
let notices: [CreatorChannelCommunityPostResponse]
|
||||
let onTapNotice: (Int) -> Void
|
||||
let onTapComment: (Int) -> Void
|
||||
let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void
|
||||
|
||||
init(
|
||||
notices: [CreatorChannelCommunityPostResponse],
|
||||
onTapNotice: @escaping (Int) -> Void = { _ in },
|
||||
onTapComment: @escaping (Int) -> Void = { _ in },
|
||||
onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in }
|
||||
) {
|
||||
self.notices = notices
|
||||
self.onTapNotice = onTapNotice
|
||||
self.onTapComment = onTapComment
|
||||
self.onTapPurchase = onTapPurchase
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !notices.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
SectionTitle(title: I18n.CreatorChannelHome.notice)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s4) {
|
||||
ForEach(notices) { notice in
|
||||
CreatorChannelNoticeCard(
|
||||
notice: notice,
|
||||
onTapNotice: {
|
||||
onTapNotice(notice.postId)
|
||||
},
|
||||
onTapComment: {
|
||||
onTapComment(notice.postId)
|
||||
},
|
||||
onTapPurchase: {
|
||||
onTapPurchase(notice)
|
||||
}
|
||||
)
|
||||
.frame(width: 374)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s14)
|
||||
}
|
||||
}
|
||||
.padding(.top, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CreatorChannelNoticeCard: View {
|
||||
let notice: CreatorChannelCommunityPostResponse
|
||||
let onTapNotice: () -> Void
|
||||
let onTapComment: () -> Void
|
||||
let onTapPurchase: () -> Void
|
||||
|
||||
@StateObject private var playManager = CreatorCommunityMediaPlayerManager.shared
|
||||
@StateObject private var contentPlayManager = ContentPlayManager.shared
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
noticeLabel
|
||||
header
|
||||
bodyContent
|
||||
reactionBar
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onTapNotice)
|
||||
}
|
||||
|
||||
private var noticeLabel: some View {
|
||||
HStack(spacing: 2) {
|
||||
Image("ic_pin")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(Color(hex: "73FF01"))
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
Text("Notice")
|
||||
.appFont(size: 14, weight: .medium)
|
||||
.foregroundColor(Color(hex: "73FF01"))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(spacing: SodaSpacing.s8) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: notice.creatorProfileUrl),
|
||||
size: CGSize(width: 42, height: 42)
|
||||
)
|
||||
.background(Color.white.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(notice.creatorNickname)
|
||||
.appFont(size: 14, weight: .medium)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text(notice.relativeTimeText())
|
||||
.appFont(size: 14, weight: .regular)
|
||||
.foregroundColor(Color.gray500)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private var bodyContent: some View {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||
Text(notice.content)
|
||||
.appFont(size: 16, weight: .regular)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(4)
|
||||
.truncationMode(.tail)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if notice.hasImage {
|
||||
thumbnail
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 92, alignment: .center)
|
||||
}
|
||||
|
||||
private var thumbnail: some View {
|
||||
ZStack {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: notice.imageUrl ?? ""),
|
||||
size: CGSize(width: 92, height: 92)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
.frame(width: 92, height: 92)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
.blur(radius: notice.isLocked ? 12 : 0)
|
||||
|
||||
if notice.isLocked {
|
||||
paidLockOverlay
|
||||
} else if notice.hasAudio {
|
||||
audioPlayButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var audioPlayButton: some View {
|
||||
Button {
|
||||
guard let audioUrl = notice.audioUrl, !audioUrl.isEmpty else { return }
|
||||
|
||||
contentPlayManager.pauseAudio()
|
||||
playManager.toggleContent(item: CreatorCommunityContentItem(contentId: notice.postId, url: audioUrl))
|
||||
} label: {
|
||||
Image(playManager.isPlaying && playManager.currentPlayingContentId == notice.postId ? "btn_audio_content_pause" : "btn_audio_content_play")
|
||||
.resizable()
|
||||
.frame(width: 48, height: 48)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var paidLockOverlay: some View {
|
||||
Button(action: onTapPurchase) {
|
||||
ZStack {
|
||||
Color.gray700.opacity(0.35)
|
||||
|
||||
VStack(spacing: SodaSpacing.s4) {
|
||||
Image("ic_new_community_lock")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
HStack(spacing: SodaSpacing.s6) {
|
||||
Image("ic_bar_cash")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("\(notice.price)")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding(SodaSpacing.s8)
|
||||
.background(Color.white)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 92, height: 92)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
|
||||
private var reactionBar: some View {
|
||||
HStack(spacing: 15) {
|
||||
Button(action: onTapComment) {
|
||||
HStack(spacing: SodaSpacing.s4) {
|
||||
Image("ic_feed_community_reply")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
Text("\(notice.commentCount)")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(Color.gray400)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
HStack(spacing: SodaSpacing.s4) {
|
||||
Image(notice.isLiked ? "ic_feed_community_heart_fill" : "ic_feed_community_heart")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
Text("\(notice.likeCount)")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(Color.gray400)
|
||||
}
|
||||
}
|
||||
.frame(height: 24)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CreatorChannelCommunityPostResponse {
|
||||
func relativeTimeText(now: Date = Date()) -> String {
|
||||
DateParser.relativeTimeText(fromUTC: dateUtc, fallback: dateUtc, now: now)
|
||||
}
|
||||
|
||||
var hasImage: Bool {
|
||||
!(imageUrl ?? "").isEmpty
|
||||
}
|
||||
|
||||
var hasAudio: Bool {
|
||||
!(audioUrl ?? "").isEmpty
|
||||
}
|
||||
|
||||
var isLocked: Bool {
|
||||
price > 0 && !existOrdered
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatorChannelNoticeSection_Previews: PreviewProvider {
|
||||
private static let previewImageUrl = "https://picsum.photos/300/300"
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: SodaSpacing.s20) {
|
||||
CreatorChannelNoticeSection(notices: [])
|
||||
|
||||
CreatorChannelNoticeSection(
|
||||
notices: [
|
||||
CreatorChannelCommunityPostResponse(
|
||||
postId: 1,
|
||||
creatorId: 10,
|
||||
creatorNickname: "크리에이터 이름",
|
||||
creatorProfileUrl: previewImageUrl,
|
||||
imageUrl: previewImageUrl,
|
||||
audioUrl: nil,
|
||||
content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분크리에이터가 커뮤니티에 올린 글이 보이는 부분 말줄임...",
|
||||
price: 0,
|
||||
dateUtc: "2026-07-03T03:44:00Z",
|
||||
existOrdered: true,
|
||||
likeCount: 5,
|
||||
commentCount: 2,
|
||||
isLiked: false
|
||||
),
|
||||
CreatorChannelCommunityPostResponse(
|
||||
postId: 2,
|
||||
creatorId: 10,
|
||||
creatorNickname: "크리에이터 이름",
|
||||
creatorProfileUrl: previewImageUrl,
|
||||
imageUrl: nil,
|
||||
audioUrl: nil,
|
||||
content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분",
|
||||
price: 10,
|
||||
dateUtc: "2026-07-03T03:42:00Z",
|
||||
existOrdered: false,
|
||||
likeCount: 3,
|
||||
commentCount: 1,
|
||||
isLiked: false
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ struct CreatorChannelHomeView: View {
|
||||
onSelectTab: onSelectTab,
|
||||
onTapDonate: onTapDonate
|
||||
)
|
||||
|
||||
CreatorChannelNoticeSection(notices: response.notices)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.black)
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
|
||||
### Phase 8: 공지 섹션
|
||||
|
||||
- [ ] **Task 8.1: 공지 섹션 구현**
|
||||
- [x] **Task 8.1: 공지 섹션 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift`
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift`
|
||||
|
||||
Reference in New Issue
Block a user