feat(community): 커뮤니티 보기 전환 탭과 아이콘을 추가한다

This commit is contained in:
Yu Sung
2026-03-06 14:25:04 +09:00
parent d29e23b9cf
commit f145de87aa
10 changed files with 452 additions and 42 deletions

View File

@@ -8,57 +8,49 @@
import SwiftUI
struct CreatorCommunityAllView: View {
let creatorId: Int
@StateObject var viewModel = CreatorCommunityAllViewModel()
@StateObject var playerManager = CreatorCommunityMediaPlayerManager.shared
@State private var isGridMode = false
@State private var isListFromGridTap = false
@State private var selectedListIndex = 0
@State private var listAnchorIndex = 0
@State private var pendingGridAnchorIndex: Int?
private let gridColumns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
var body: some View {
GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "커뮤니티")
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 26.7) {
ForEach(0..<viewModel.communityPostList.count, id: \.self) { index in
let item = viewModel.communityPostList[index]
CreatorCommunityAllItemView(
item: item,
onClickLike: {
viewModel.communityPostLike(postId: item.postId)
},
onClickComment: {
viewModel.postId = item.postId
viewModel.isShowSecret = item.price > 0 && item.existOrdered && item.creatorId != UserDefaults.int(forKey: .userId)
viewModel.isShowCommentListView = true
},
onClickWriteComment: { comment, isSecret in
viewModel.createCommunityPostComment(
comment: comment,
postId: item.postId,
isSecret: isSecret
)
},
onClickShowReportMenu: {
viewModel.postId = item.postId
viewModel.isShowReportMenu = true
},
onClickPurchaseContent: {
viewModel.postId = item.postId
viewModel.postPrice = item.price
viewModel.postIndex = index
viewModel.isShowPostPurchaseView = true
}
)
.onAppear {
if index == viewModel.communityPostList.count - 1 {
viewModel.getCommunityPostList()
}
DetailNavigationBar(
title: "커뮤니티",
backAction: {
if isGridMode {
AppState.shared.back()
} else {
if isListFromGridTap {
returnToGridMode()
} else {
AppState.shared.back()
}
}
}
)
communityViewTypeTabView
if isGridMode {
gridContentView
} else {
listContentView
}
}
.sheet(
@@ -188,12 +180,222 @@ struct CreatorCommunityAllView: View {
}
}
}
private var gridContentView: some View {
ScrollViewReader { scrollProxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: gridColumns, spacing: 1) {
ForEach(0..<viewModel.communityPostList.count, id: \.self) { index in
let item = viewModel.communityPostList[index]
CreatorCommunityAllGridItemView(item: item)
.id(index)
.onTapGesture {
selectedListIndex = index
listAnchorIndex = index
isListFromGridTap = true
withAnimation(.easeInOut(duration: 0.2)) {
isGridMode = false
}
}
.onAppear {
if index == viewModel.communityPostList.count - 1 {
viewModel.getCommunityPostList()
}
}
}
}
}
.onAppear {
guard let index = pendingGridAnchorIndex else { return }
DispatchQueue.main.async {
scrollProxy.scrollTo(index, anchor: .center)
pendingGridAnchorIndex = nil
}
}
}
}
private var listContentView: some View {
ScrollViewReader { scrollProxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 13.3) {
ForEach(0..<viewModel.communityPostList.count, id: \.self) { index in
let item = viewModel.communityPostList[index]
makeCommunityItemView(item: item, index: index)
.id(index)
.onAppear {
listAnchorIndex = index
if index == viewModel.communityPostList.count - 1 {
viewModel.getCommunityPostList()
}
}
}
}
.padding(.vertical, 13.3)
}
.onAppear {
let targetIndex = min(max(selectedListIndex, 0), max(viewModel.communityPostList.count - 1, 0))
DispatchQueue.main.async {
scrollProxy.scrollTo(targetIndex, anchor: .top)
}
}
.onChange(of: isGridMode) { mode in
guard !mode else { return }
let targetIndex = min(max(selectedListIndex, 0), max(viewModel.communityPostList.count - 1, 0))
DispatchQueue.main.async {
scrollProxy.scrollTo(targetIndex, anchor: .top)
}
}
}
}
private func makeCommunityItemView(item: GetCommunityPostListResponse, index: Int) -> some View {
CreatorCommunityAllItemView(
item: item,
onClickLike: {
viewModel.communityPostLike(postId: item.postId)
},
onClickComment: {
viewModel.postId = item.postId
viewModel.isShowSecret = item.price > 0 && item.existOrdered && item.creatorId != UserDefaults.int(forKey: .userId)
viewModel.isShowCommentListView = true
},
onClickWriteComment: { comment, isSecret in
viewModel.createCommunityPostComment(
comment: comment,
postId: item.postId,
isSecret: isSecret
)
},
onClickShowReportMenu: {
viewModel.postId = item.postId
viewModel.isShowReportMenu = true
},
onClickPurchaseContent: {
viewModel.postId = item.postId
viewModel.postPrice = item.price
viewModel.postIndex = index
viewModel.isShowPostPurchaseView = true
}
)
}
private func returnToGridMode() {
pendingGridAnchorIndex = listAnchorIndex
isListFromGridTap = false
isGridMode = true
}
private var communityViewTypeTabView: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Button {
isGridMode = false
isListFromGridTap = false
} label: {
Image(isGridMode ? "ic_community_list" : "ic_community_list_selected")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.frame(maxWidth: .infinity)
.frame(height: 44)
}
Button {
isGridMode = true
isListFromGridTap = false
} label: {
Image(isGridMode ? "ic_community_grid_selected" : "ic_community_grid")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.frame(maxWidth: .infinity)
.frame(height: 44)
}
}
.background(Color.black)
ZStack(alignment: .bottomLeading) {
Rectangle()
.foregroundColor(Color(hex: "909090"))
.frame(height: 1)
HStack(spacing: 0) {
Rectangle()
.foregroundColor(isGridMode ? Color.clear : Color.white)
.frame(maxWidth: .infinity)
Rectangle()
.foregroundColor(isGridMode ? Color.white : Color.clear)
.frame(maxWidth: .infinity)
}
.frame(height: 2)
}
.frame(height: 2)
}
}
private func creatorCommunityModifySuccess() {
viewModel.getCommunityPostList()
}
}
private struct CreatorCommunityAllGridItemView: View {
let item: GetCommunityPostListResponse
var body: some View {
ZStack {
if isPaidLocked {
Color.gray33
Image("ic_lock_bb")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
} else if let imageUrl = item.imageUrl, !imageUrl.isEmpty {
AsyncImage(url: URL(string: imageUrl)) { phase in
switch phase {
case .empty:
Color.gray33
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
Color(hex: "263238")
@unknown default:
Color.gray33
}
}
} else {
Color(hex: "263238")
Text(fallbackText)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color.grayee)
.multilineTextAlignment(.center)
.lineLimit(3)
.padding(8)
}
}
.aspectRatio(1, contentMode: .fit)
.clipped()
}
private var isPaidLocked: Bool {
item.price > 0 && !item.existOrdered
}
private var fallbackText: String {
let content = item.content.trimmingCharacters(in: .whitespacesAndNewlines)
if content.isEmpty {
return "-"
}
return String(content.prefix(18))
}
}
struct CreatorCommunityAllView_Previews: PreviewProvider {
static var previews: some View {
CreatorCommunityAllView(creatorId: 0)