diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/Contents.json new file mode 100644 index 0000000..d48b431 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_community_grid.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/ic_community_grid.png b/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/ic_community_grid.png new file mode 100644 index 0000000..eb54f31 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/ic_community_grid.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/Contents.json new file mode 100644 index 0000000..a9241df --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_community_grid_selected.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/ic_community_grid_selected.png b/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/ic_community_grid_selected.png new file mode 100644 index 0000000..f527036 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/ic_community_grid_selected.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/Contents.json new file mode 100644 index 0000000..447f334 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_community_list.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/ic_community_list.png b/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/ic_community_list.png new file mode 100644 index 0000000..7a559d7 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/ic_community_list.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/Contents.json new file mode 100644 index 0000000..e693ea5 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_community_list_selected.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/ic_community_list_selected.png b/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/ic_community_list_selected.png new file mode 100644 index 0000000..9ff6082 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/ic_community_list_selected.png differ diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift index b67120d..883409c 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift @@ -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.. 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.. 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) diff --git a/docs/20260305_크리에이터커뮤니티전체보기그리드리스트전환구현.md b/docs/20260305_크리에이터커뮤니티전체보기그리드리스트전환구현.md new file mode 100644 index 0000000..236a6cf --- /dev/null +++ b/docs/20260305_크리에이터커뮤니티전체보기그리드리스트전환구현.md @@ -0,0 +1,124 @@ +# 20260305 크리에이터 커뮤니티 전체보기 그리드 리스트 전환 구현 + +## 구현 목표 +- `CreatorCommunityAllView`를 인스타그램과 유사하게 기본 `Grid` 화면으로 노출한다. +- 그리드 아이템 탭 시 `List` 화면으로 전환하고, 탭한 아이템 위치에서 리스트를 시작한다. +- 그리드 썸네일 규칙을 반영한다: 이미지 있음(이미지), 이미지 없음(텍스트 일부), 유료 미구매(자물쇠 이미지). +- 리스트 화면에서 뒤로가기 버튼 및 툴바를 제공하고, 뒤로가기 시 다시 그리드로 복귀한다. +- 리스트에서 뒤로가기 시 마지막 리스트 앵커 아이템이 그리드에서 보이도록 스크롤 위치를 복원한다. + +## 체크리스트 +- [x] 기존 `CreatorCommunity` 화면/모델/내비게이션 패턴 탐색 +- [x] `CreatorCommunityAllView` 기본 레이아웃을 Grid 중심으로 전환 +- [x] Grid 아이템 썸네일 규칙(이미지/텍스트/유료 자물쇠) 구현 +- [x] 아이템 탭 시 List 전환 + 탭 아이템 앵커 이동 구현 +- [x] 리스트 상태 툴바/뒤로가기 구현 및 그리드 복귀 동작 연결 +- [x] 리스트 스크롤 중 앵커 인덱스 추적 및 그리드 복귀 시 위치 복원 +- [x] LSP/빌드/테스트 검증 및 결과 기록 + +## 검증 기록 +- 2026-03-05 + - 무엇: 작업 시작 및 요구사항 분석/패턴 탐색 + - 왜: 기존 아키텍처와 충돌 없이 `Grid -> List -> Grid` UX를 정확히 반영하기 위해 + - 어떻게: `CreatorCommunityAllView`, `CreatorCommunityAllItemView`, `DetailNavigationBar`, 응답 모델을 확인해 재사용 가능한 상태/탐색 패턴을 추출 + - 결과: 완료 + +- 2026-03-05 (구현/검증 완료) + - 무엇: `CreatorCommunityAllView`를 기본 Grid(한 줄 3개) 노출로 변경하고, 아이템 탭 시 List 전환/뒤로가기 복귀/앵커 복원을 구현했다. + - 왜: 인스타그램과 유사한 탐색 UX(그리드 진입, 게시물 리스트 탐색, 원위치 복귀)를 요구사항대로 제공하기 위해. + - 어떻게: + - 수정 파일 + - `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift` + - 적용 내용 + - `LazyVGrid` 3열 고정(`GridItem` 3개)으로 그리드 기본 화면 구성 + - 썸네일 규칙 반영: 유료 미구매(`price > 0 && !existOrdered`)는 `ic_lock_bb`, 이미지 없으면 텍스트 앞 18자, 그 외 `AsyncImage` + - `DetailNavigationBar`의 `backAction` 분기로 Grid 모드에서는 화면 뒤로가기, List 모드에서는 Grid 복귀 + - Grid 탭 시 `selectedListIndex`로 List 스크롤 시작점 이동, List 스크롤 중 `listAnchorIndex` 추적 + - List에서 뒤로가기 시 `pendingGridAnchorIndex`를 사용해 Grid에서 해당 앵커 아이템으로 `scrollTo` + - 실행 명령 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - `lsp_diagnostics` (`CreatorCommunityAllView.swift`) + - 결과: + - 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **` 확인. + - 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 실행 불가. + - LSP: SourceKit 단독 진단에서 프로젝트 심볼 미해결 에러가 다수 발생했으나, 실제 xcodebuild 컴파일은 성공. + +- 2026-03-05 (요구사항 보강: 그리드 3열/앵커 복원 타이밍 안정화) + - 무엇: `Grid`를 한 줄 3개 고정 상태로 유지하고, List -> Grid 복귀 시 앵커 스크롤이 상태 전환 타이밍에 영향을 받지 않도록 `onAppear` 복원 로직을 추가했다. + - 왜: "그리드 리스트 한 줄에 3개 표시" 및 "리스트 앵커 아이템 복귀" 요구를 더 안정적으로 충족하기 위해. + - 어떻게: + - `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift` + - `gridColumns` 3개 고정 유지 + - `gridContentView`에 `pendingGridAnchorIndex` 처리용 `.onAppear` 추가 + - 실행 명령 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - 결과: + - 빌드: 두 스킴 모두 `** BUILD SUCCEEDED **`. + - 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`. + +- 2026-03-05 (퍼포먼스 개선: List -> Grid 전환 지연 완화) + - 무엇: 리스트에서 그리드로 돌아갈 때 전환이 느린 문제를 완화하도록 복귀 경로를 무애니메이션/단일 복원으로 최적화했다. + - 왜: 복귀 시 모드 전환 애니메이션 + 앵커 복원 애니메이션 + 중복 복원 트리거가 겹치면 체감 지연이 커질 수 있기 때문. + - 어떻게: + - `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift` + - `returnToGridMode()`에서 `withAnimation` 제거 후 즉시 `isGridMode = true`로 전환 + - `gridContentView`의 `pendingGridAnchorIndex` 복원을 `onAppear` 단일 경로로 통합 + - 앵커 복원 `scrollTo`를 무애니메이션으로 변경해 렌더링 부하 감소 + - 실행 명령 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - `lsp_diagnostics` (`CreatorCommunityAllView.swift`) + - 결과: + - 빌드: `SodaLive`는 `** BUILD SUCCEEDED **`, `SodaLive-dev`는 병렬 빌드 중 `build.db locked` 1회 발생 후 순차 재실행에서 `** BUILD SUCCEEDED **`. + - 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`. + - LSP: SourceKit 단독 진단에서 프로젝트 심볼 미해결 에러가 다수 발생했으나, 실제 xcodebuild 컴파일은 성공. + +- 2026-03-06 (UI 변경: Toolbar 아래 List/Grid 탭 및 기본 List 진입) + - 무엇: Toolbar 아래에 좌측 List/우측 Grid 탭을 추가하고, 기본 표시를 Grid에서 List로 변경했다. + - 왜: 요청된 UI 변경(탭 2개, 기본 List, 뒤로가기 분기)을 정확히 반영하기 위해. + - 어떻게: + - `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift` + - `communityViewTypeTabView` 추가: 좌 `ic_community_list(_selected)`, 우 `ic_community_grid(_selected)`, 각 버튼 `maxWidth: .infinity`로 가로 꽉찬 탭 구성 + - 기본 모드 `isGridMode = false`로 변경 + - `isListFromGridTap` 상태 추가 + - Grid 아이템 탭으로 List 진입 시 `isListFromGridTap = true` + - List 상태 뒤로가기 분기: + - `isListFromGridTap == true`면 Grid 복귀 + - 초기 List(또는 탭 전환 List)는 기본 뒤로가기 수행 + - 실행 명령 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - `lsp_diagnostics` (`CreatorCommunityAllView.swift`) + - 결과: + - 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **`. + - 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`. + - LSP: SourceKit 단독 진단에서는 프로젝트 심볼 미해결 에러가 발생하나, 실제 xcodebuild는 성공. + +- 2026-03-06 (탭 디테일 조정: 배경/하단 라인/선택 인디케이터) + - 무엇: 커뮤니티 전체보기 탭의 시각 디테일을 요청 사양으로 조정했다. + - 왜: 탭 배경색, 하단 구분선, 선택 인디케이터를 명시한 최신 UI 요구를 반영하기 위해. + - 어떻게: + - `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift` + - 탭 배경색을 `#777777`로 변경 + - 탭 하단에 전체 폭 구분선(`height: 1`, `#909090`) 추가 + - 선택 탭 하단 인디케이터(`height: 2`, `#ffffff`) 추가 + - 실행 명령 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - `lsp_diagnostics` (`CreatorCommunityAllView.swift`) + - 결과: + - 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **`. + - 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`. + - LSP: SourceKit 단독 진단에서는 프로젝트 심볼 미해결 및 확장 타입 해석 오류가 발생하나, 실제 xcodebuild는 성공.