From 2bc26c6173f8d098921dab17c03dceebd2a1f4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Thu, 25 Dec 2025 15:46:22 +0900 Subject: [PATCH 01/15] =?UTF-8?q?[feat]=20=EC=95=8C=EB=A6=BC=20=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0=20=EB=B0=94=ED=85=80=20=EC=8B=9C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/MainTab/MainTabPath.swift | 2 +- .../alert_explain.imageset/Contents.json | 23 +++++ .../alert_explain.imageset/alert_explain.png | Bin 0 -> 7705 bytes .../alert_explain@2x.png | Bin 0 -> 20738 bytes .../alert_explain@3x.png | Bin 0 -> 36748 bytes .../DSKit/Sources/Foundation/PokitImage.swift | 3 + .../PokitCategorySetting.swift | 7 +- .../Sources/PokitCategorySettingFeature.swift | 52 ++++++++++- .../Sources/PokitCategorySettingView.swift | 36 ++++++++ .../Sources/Sheet/PokitAlertBottomSheet.swift | 83 ++++++++++++++++++ .../FeatureCategorySettingDemoApp.swift | 20 ++++- 11 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/Contents.json create mode 100644 Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/alert_explain.png create mode 100644 Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/alert_explain@2x.png create mode 100644 Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/alert_explain@3x.png create mode 100644 Projects/Feature/FeatureCategorySetting/Sources/Sheet/PokitAlertBottomSheet.swift diff --git a/Projects/App/Sources/MainTab/MainTabPath.swift b/Projects/App/Sources/MainTab/MainTabPath.swift index fbd1782d..c1354d9a 100644 --- a/Projects/App/Sources/MainTab/MainTabPath.swift +++ b/Projects/App/Sources/MainTab/MainTabPath.swift @@ -261,7 +261,7 @@ public extension MainTabFeature { ))) return .none case .path(.element(_, action: .알림함(.delegate(.alertBoxDismiss)))): - state.path.popLast() + let _ = state.path.popLast() return .none default: return .none } diff --git a/Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/Contents.json new file mode 100644 index 00000000..7a032e19 --- /dev/null +++ b/Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "alert_explain.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "alert_explain@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "alert_explain@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/alert_explain.png b/Projects/DSKit/Resources/Assets.xcassets/alert_explain.imageset/alert_explain.png new file mode 100644 index 0000000000000000000000000000000000000000..b9bf3f2f530a4e9c4472b633a0767b17ee1163f6 GIT binary patch literal 7705 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

V0MTQ+N%7Fw3!O`H6!Ry?4?x_z;+Z6dbw~<} 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 V=E%L8gi~oCC7!N+F6*2UngEDV$eaKG literal 0 HcmV?d00001 diff --git a/Projects/DSKit/Sources/Foundation/PokitImage.swift b/Projects/DSKit/Sources/Foundation/PokitImage.swift index b89a404b..79f7e712 100644 --- a/Projects/DSKit/Sources/Foundation/PokitImage.swift +++ b/Projects/DSKit/Sources/Foundation/PokitImage.swift @@ -130,6 +130,8 @@ public enum PokitImage { return DSKitAsset.imageProfile.swiftUIImage case .unpokited: return DSKitAsset.unpokited.swiftUIImage + case .alertExplain: + return DSKitAsset.alertExplain.swiftUIImage } } } @@ -198,5 +200,6 @@ public extension PokitImage { case firecracker case profile case unpokited + case alertExplain } } diff --git a/Projects/Domain/Sources/PokitCategorySetting/PokitCategorySetting.swift b/Projects/Domain/Sources/PokitCategorySetting/PokitCategorySetting.swift index 5d24f29f..8cd549ca 100644 --- a/Projects/Domain/Sources/PokitCategorySetting/PokitCategorySetting.swift +++ b/Projects/Domain/Sources/PokitCategorySetting/PokitCategorySetting.swift @@ -27,12 +27,16 @@ public struct PokitCategorySetting: Equatable { /// 유저가 선택한 카테고리 키워드(기본값: default - 미선택) public var keywordType: BaseInterestType + /// 카테고리 참여자 수 + public let userCount: Int? + public init( categoryId: Int?, categoryName: String?, categoryImage: BaseCategoryImage?, openType: BaseOpenType?, - keywordType: BaseInterestType? + keywordType: BaseInterestType?, + userCount: Int? = nil ) { self.imageList = [] self.pageable = .init( @@ -45,5 +49,6 @@ public struct PokitCategorySetting: Equatable { self.categoryImage = categoryImage self.openType = openType ?? .공개 self.keywordType = keywordType ?? .default + self.userCount = userCount } } diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift index 6206d9f5..925d9241 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift @@ -4,7 +4,7 @@ // // Created by 김민호 on 7/25/24. -import Foundation +import SwiftUI import ComposableArchitecture import DSKit @@ -25,6 +25,10 @@ public struct PokitCategorySettingFeature { var userClient @Dependency(KeyboardClient.self) var keyboardClient + @Dependency(UserNotificationClient.self) + var userNotificationClient + @Dependency(\.openSettings) + var openSetting /// - State @ObservableState public struct State: Equatable { @@ -56,6 +60,13 @@ public struct PokitCategorySettingFeature { && selectedProfile != nil && (domain.openType == .공개 ? keywordSelectType != .normal : true) } + var isCoEditing: Bool { + let userCount = domain.userCount ?? 0 + return userCount > 0 + } + var alertEnable: Bool { + isNotificationAuthorization && isAlert + } let type: SettingType var keywordSelectType: KeywordSelectType = .normal @@ -63,6 +74,9 @@ public struct PokitCategorySettingFeature { var isKeywordSheetPresented: Bool = false var pokitNameTextInpuState: PokitInputStyle.State = .default var isKeyboardVisible: Bool = false + var isNotificationAuthorization: Bool = false + var isAlert = true + var showAlertSheet = false @Shared(.inMemory("SelectCategory")) var categoryId: Int? /// - 포킷 수정 API / 추가 API /// categoryName @@ -79,7 +93,8 @@ public struct PokitCategorySettingFeature { categoryName: category?.categoryName, categoryImage: category?.categoryImage, openType: category?.openType, - keywordType: category?.keywordType + keywordType: category?.keywordType, + userCount: category?.userCount ) } } @@ -102,6 +117,10 @@ public struct PokitCategorySettingFeature { case 포킷명지우기_버튼_눌렀을때 case 키워드_바텀시트_활성화(Bool) case 키워드_선택_버튼_눌렀을때(BaseInterestType) + case 알림_권한_바인딩(Bool) + case 알림_바텀시트_알림켜기_버튼_눌렀을떼 + case 알림_바텀시트_다음에_버튼_눌렀을떼 + case scenePhase_바꼈을때(ScenePhase) } public enum InnerAction: Equatable { @@ -109,12 +128,14 @@ public struct PokitCategorySettingFeature { case 포킷_오류_핸들링(BaseError) case 카테고리_인메모리_저장(BaseCategoryItem) case 키보드_감지_반영(Bool) + case 알림_권한_감지_반영(Bool) } public enum AsyncAction: Equatable { case 프로필_목록_조회_API case 클립보드_감지 case 키보드_감지 + case 알림_권한_감지 } public enum ScopeAction { @@ -279,6 +300,22 @@ private extension PokitCategorySettingFeature { return .run { send in await send(.view(.키워드_바텀시트_활성화(false))) } + + case let .알림_권한_바인딩(isAlert): + state.isAlert = isAlert + guard isAlert && !state.isNotificationAuthorization else { return .none } + state.showAlertSheet = true + return .none + + case .알림_바텀시트_알림켜기_버튼_눌렀을떼: + return .run { _ in await openSetting() } + + case .알림_바텀시트_다음에_버튼_눌렀을떼: + state.showAlertSheet = false + return .none + case let .scenePhase_바꼈을때(scenePhase): + guard scenePhase == .active else { return .none } + return .send(.async(.알림_권한_감지)) } } @@ -305,6 +342,12 @@ private extension PokitCategorySettingFeature { case let .키보드_감지_반영(isVisible): state.isKeyboardVisible = isVisible return .none + case let .알림_권한_감지_반영(authorization): + state.isNotificationAuthorization = authorization + if state.isNotificationAuthorization { + state.showAlertSheet = false + } + return .none } } @@ -332,6 +375,11 @@ private extension PokitCategorySettingFeature { await send(.inner(.키보드_감지_반영(detect)), animation: .pokitSpring) } } + case .알림_권한_감지: + return .run { send in + let authorization = await userNotificationClient.getNotificationSettings() + await send(.inner(.알림_권한_감지_반영(authorization.authorizationStatus == .authorized))) + } } } diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift index b75cc834..81644e39 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift @@ -14,6 +14,9 @@ import NukeUI @ViewAction(for: PokitCategorySettingFeature.self) public struct PokitCategorySettingView: View { + @Environment(\.scenePhase) + private var scenePhase + /// - Properties @Perception.Bindable public var store: StoreOf @@ -34,6 +37,9 @@ public extension PokitCategorySettingView { PokitDivider() .padding(.horizontal, -20) .padding(.top, 28) + + if store.isCoEditing { alarmSettingSection } + openTypeSettingSection keywordSection Spacer() @@ -59,9 +65,17 @@ public extension PokitCategorySettingView { action: { send(.키워드_선택_버튼_눌렀을때($0)) } ) } + .sheet(isPresented: $store.showAlertSheet) { + WithPerceptionTracking { + PokitAlertBottomSheet(store: store) + } + } .ignoresSafeArea(.container, edges: .bottom) .dismissKeyboard(focused: $isFocused) .task { await send(.뷰가_나타났을때).finish() } + .onChange(of: scenePhase) { newValue in + send(.scenePhase_바꼈을때(newValue)) + } } } } @@ -156,6 +170,28 @@ private extension PokitCategorySettingView { ) } } + + /// 알림 받기 + var alarmSettingSection: some View { + VStack(alignment: .leading, spacing: 4) { + Toggle(isOn: .init( + get: { store.alertEnable }, + set: { send(.알림_권한_바인딩($0)) } + )) { + Text("알림 받기") + .pokitFont(.b1(.b)) + .foregroundStyle(.pokit(.text(.primary))) + } + .tint(.pokit(.icon(.brand))) + Text("포킷이 수정되면 알림을 보내드립니다.") + .pokitFont(.detail1) + .foregroundStyle(.pokit(.text(.tertiary))) + } + .padding(.vertical, 12) + .padding(.leading, 8) + .padding(.top, 16) + } + /// 공개 여부 설정 var openTypeSettingSection: some View { VStack(alignment: .leading, spacing: 4) { diff --git a/Projects/Feature/FeatureCategorySetting/Sources/Sheet/PokitAlertBottomSheet.swift b/Projects/Feature/FeatureCategorySetting/Sources/Sheet/PokitAlertBottomSheet.swift new file mode 100644 index 00000000..fedc207e --- /dev/null +++ b/Projects/Feature/FeatureCategorySetting/Sources/Sheet/PokitAlertBottomSheet.swift @@ -0,0 +1,83 @@ +// +// PokitAlertBottomSheet.swift +// FeatureCategorySetting +// +// Created by 김도형 on 12/16/25. +// + +import SwiftUI + +import ComposableArchitecture +import DSKit + +struct PokitAlertBottomSheet: View { + @State + private var height: CGFloat = 439 + + private let store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + var body: some View { + GeometryReader { proxy in + let bottomSafeArea = proxy.safeAreaInsets.bottom + let topSafeArea = proxy.safeAreaInsets.top + + VStack(spacing: 20) { + title + .padding(.top, 36) + + Image(.image(.alertExplain)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + + buttons + } + .padding(.horizontal, 20) + .frame(maxWidth: .infinity) + .padding(.bottom, 36 - bottomSafeArea) + .padding(.top, 12 - topSafeArea) + .ignoresSafeArea(edges: [.bottom, .top]) + .readHeight() + .onPreferenceChange(HeightPreferenceKey.self) { height in + if let height { self.height = height } + } + } + .presentationDragIndicator(.visible) + .pokitPresentationCornerRadius() + .pokitPresentationBackground() + .presentationDetents([.height(height)]) + + } +} + +// MARK: - Configure Views +private extension PokitAlertBottomSheet { + var title: some View { + VStack(spacing: 8) { + Text("포킷 앱 알림을 켜주세요!") + .pokitFont(.title2) + .foregroundStyle(.pokit(.text(.primary))) + + Text("시스템 알림이 꺼져 있어요.\n포킷 앱 알림을 허용해 주시면 알림을 보내드려요.") + .pokitFont(.b2(.m)) + .foregroundStyle(.pokit(.text(.secondary))) + } + } + + var buttons: some View { + HStack(spacing: 8) { + PokitBottomButton("다음에", state: .default(.primary)) { + store.send(.view(.알림_바텀시트_다음에_버튼_눌렀을떼)) + } + + PokitBottomButton("알림 켜기", state: .filled(.primary)) { + store.send(.view(.알림_바텀시트_알림켜기_버튼_눌렀을떼)) + } + } + .padding(.vertical, 16) + } +} diff --git a/Projects/Feature/FeatureCategorySettingDemo/Sources/FeatureCategorySettingDemoApp.swift b/Projects/Feature/FeatureCategorySettingDemo/Sources/FeatureCategorySettingDemoApp.swift index 54729a4d..19d49f36 100644 --- a/Projects/Feature/FeatureCategorySettingDemo/Sources/FeatureCategorySettingDemoApp.swift +++ b/Projects/Feature/FeatureCategorySettingDemo/Sources/FeatureCategorySettingDemoApp.swift @@ -11,6 +11,7 @@ import ComposableArchitecture import FeatureCategorySetting import FeatureIntro import Util +import Domain @main struct FeatureCategorySettingDemoApp: App { @@ -23,7 +24,24 @@ struct FeatureCategorySettingDemoApp: App { NavigationStack { PokitCategorySettingView( store: Store( - initialState: .init(type: .추가), + initialState: .init( + type: .수정, + category: BaseCategoryItem( + id: 764, + userId: 213, + categoryName: "playlist", + categoryImage: BaseCategoryImage( + imageId: 13, + imageURL: "https://pokit-s3.s3.ap-northeast-2.amazonaws.com/category-image/music.png" + ), + contentCount: 3, + createdAt: "2024.12.03", + openType: .공개, + keywordType: .음악, + userCount: 2, + isFavorite: false + ) + ), reducer: { PokitCategorySettingFeature() } ) ) From 153b4f1ee303bd5b8d6c0c4acc39c75304650a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Thu, 25 Dec 2025 16:08:47 +0900 Subject: [PATCH 02/15] =?UTF-8?q?[feat]=20=EC=B4=88=EB=8C=80=EB=90=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EB=AA=A9=EB=A1=9D=20api=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Category/InvitedUserResponse.swift | 22 +++++++++++++++++++ .../Category/CategoryClient+LiveKey.swift | 3 +++ .../Category/CategoryClient+TestKey.swift | 3 ++- .../Network/Category/CategoryClient.swift | 4 +++- .../Network/Category/CategoryEndpoint.swift | 8 ++++++- .../Domain/Sources/Base/InvitedUser.swift | 20 +++++++++++++++++ .../CategoryDetail/CategoryDetail.swift | 3 +++ .../InvitedUserResponse+Extension.swift | 20 +++++++++++++++++ .../Sources/CategoryDetailFeature.swift | 14 ++++++++++++ .../FeatureCategoryDetailDemoApp.swift | 16 +++++++++++++- .../Sources/PokitCategorySettingView.swift | 3 +++ 11 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 Projects/CoreKit/Sources/Data/DTO/Category/InvitedUserResponse.swift create mode 100644 Projects/Domain/Sources/Base/InvitedUser.swift create mode 100644 Projects/Domain/Sources/DTO/Category/InvitedUserResponse+Extension.swift diff --git a/Projects/CoreKit/Sources/Data/DTO/Category/InvitedUserResponse.swift b/Projects/CoreKit/Sources/Data/DTO/Category/InvitedUserResponse.swift new file mode 100644 index 00000000..8e8e8199 --- /dev/null +++ b/Projects/CoreKit/Sources/Data/DTO/Category/InvitedUserResponse.swift @@ -0,0 +1,22 @@ +// +// InvitedUserResponse.swift +// CoreKit +// +// Created by 김도형 on 12/25/25. +// + +import Foundation + +public struct InvitedUserResponse: Decodable { + public let userId: Int + public let nickname: String + public let profileImage: BaseProfileImageResponse? +} + +extension InvitedUserResponse { + public static var mock: Self = Self( + userId: 1, + nickname: "PokitUser", + profileImage: .mock + ) +} diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift index d6fe994a..ded81e40 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift @@ -52,6 +52,9 @@ extension CategoryClient: DependencyKey { }, 공유받은_카테고리_저장: { model in try await provider.requestNoBody(.공유받은_카테고리_저장(model: model)) + }, + 포킷_초대된_유저_목록_조회: { id in + try await provider.request(.포킷_초대된_유저_목록_조회(categoryId: id)) } ) }() diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift index bcf23adf..2d9f4fb7 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift @@ -18,7 +18,8 @@ extension CategoryClient: TestDependencyKey { 유저_카테고리_개수_조회: { .mock }, 카테고리_상세_조회: { _ in .mock }, 공유받은_카테고리_조회: { _, _ in .mock }, - 공유받은_카테고리_저장: { _ in } + 공유받은_카테고리_저장: { _ in }, + 포킷_초대된_유저_목록_조회: { _ in [.mock] } ) }() } diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift index a147ba68..1fe8c67d 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift @@ -36,5 +36,7 @@ public struct CategoryClient { public var 공유받은_카테고리_저장: @Sendable ( _ model: CopiedCategoryRequest ) async throws -> Void + public var 포킷_초대된_유저_목록_조회: @Sendable ( + _ categoryId: Int + ) async throws -> [InvitedUserResponse] } - diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift index dbfe8dc7..91eeaed8 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift @@ -20,6 +20,7 @@ public enum CategoryEndpoint { case 카테고리_상세_조회(categoryId: String) case 공유받은_카테고리_조회(categoryId: String, model: BasePageableRequest) case 공유받은_카테고리_저장(model: CopiedCategoryRequest) + case 포킷_초대된_유저_목록_조회(categoryId: Int) } @@ -53,6 +54,8 @@ extension CategoryEndpoint: TargetType { return "/share/\(categoryId)" case .공유받은_카테고리_저장: return "/share" + case let .포킷_초대된_유저_목록_조회(categoryId): + return "/\(categoryId)/invited" } } @@ -68,7 +71,8 @@ extension CategoryEndpoint: TargetType { .카테고리_프로필_목록_조회, .유저_카테고리_개수_조회, .카테고리_상세_조회, - .공유받은_카테고리_조회: + .공유받은_카테고리_조회, + .포킷_초대된_유저_목록_조회: return .get case .카테고리생성, @@ -113,6 +117,8 @@ extension CategoryEndpoint: TargetType { ) case let .공유받은_카테고리_저장(model): return .requestJSONEncodable(model) + case .포킷_초대된_유저_목록_조회: + return .requestPlain } } public var headers: [String: String]? { diff --git a/Projects/Domain/Sources/Base/InvitedUser.swift b/Projects/Domain/Sources/Base/InvitedUser.swift new file mode 100644 index 00000000..41671355 --- /dev/null +++ b/Projects/Domain/Sources/Base/InvitedUser.swift @@ -0,0 +1,20 @@ +// +// InvitedUser.swift +// Domain +// +// Created by 김도형 on 12/25/25. +// + +import Foundation + +public struct InvitedUser: Equatable, Identifiable { + public let id: Int + public let nickname: String + public let profile: BaseProfile? + + public init(id: Int, nickname: String, profile: BaseProfile?) { + self.id = id + self.nickname = nickname + self.profile = profile + } +} diff --git a/Projects/Domain/Sources/CategoryDetail/CategoryDetail.swift b/Projects/Domain/Sources/CategoryDetail/CategoryDetail.swift index 3bc7a5d4..3939e473 100644 --- a/Projects/Domain/Sources/CategoryDetail/CategoryDetail.swift +++ b/Projects/Domain/Sources/CategoryDetail/CategoryDetail.swift @@ -15,6 +15,8 @@ public struct CategoryDetail: Equatable { public var categoryListInQuiry: BaseCategoryListInquiry /// 카테고리(포킷) 내 콘텐츠(링크) 리스트 public var contentList: BaseContentListInquiry + /// 초대된 유저 목록 + public var invitedUsers: [InvitedUser] // - MARK: Request /// 조회할 페이징 정보 public var pageable: BasePageable @@ -36,6 +38,7 @@ public struct CategoryDetail: Equatable { sort: [], hasNext: false ) + self.invitedUsers = [] self.pageable = .init( page: 0, size: 10, diff --git a/Projects/Domain/Sources/DTO/Category/InvitedUserResponse+Extension.swift b/Projects/Domain/Sources/DTO/Category/InvitedUserResponse+Extension.swift new file mode 100644 index 00000000..e8f9227b --- /dev/null +++ b/Projects/Domain/Sources/DTO/Category/InvitedUserResponse+Extension.swift @@ -0,0 +1,20 @@ +// +// InvitedUserResponse+Extension.swift +// Domain +// +// Created by 김도형 on 12/25/25. +// + +import Foundation + +import CoreKit + +public extension InvitedUserResponse { + func toDomain() -> InvitedUser { + return .init( + id: self.userId, + nickname: self.nickname, + profile: self.profileImage?.toDomain() + ) + } +} diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index ba1239d5..5d3f01d4 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -109,6 +109,7 @@ public struct CategoryDetailFeature { case 카테고리_내_컨텐츠_목록_조회_API_반영(BaseContentListInquiry) case pagenation_API_반영(BaseContentListInquiry) case pagenation_초기화 + case 포킷_초대된_유저_목록_조회_API_반영([InvitedUser]) } public enum AsyncAction: Equatable { @@ -116,6 +117,7 @@ public struct CategoryDetailFeature { case 카테고리_목록_조회_API case 페이징_재조회 case 클립보드_감지 + case 포킷_초대된_유저_목록_조회_API } public enum ScopeAction { @@ -254,6 +256,7 @@ private extension CategoryDetailFeature { return .merge( .send(.async(.카테고리_내_컨텐츠_목록_조회_API)), .send(.async(.카테고리_목록_조회_API)), + .send(.async(.포킷_초대된_유저_목록_조회_API)), .send(.async(.클립보드_감지)) ) case .pagenation: @@ -312,6 +315,10 @@ private extension CategoryDetailFeature { state.isLoading = true state.contents.removeAll() return .none + + case .포킷_초대된_유저_목록_조회_API_반영(let users): + state.domain.invitedUsers = users + return .none } } @@ -392,6 +399,13 @@ private extension CategoryDetailFeature { await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring) } } + + case .포킷_초대된_유저_목록_조회_API: + return .run { [id = state.domain.category.id] send in + let response = try await categoryClient.포킷_초대된_유저_목록_조회(id) + let users = response.map { $0.toDomain() } + await send(.inner(.포킷_초대된_유저_목록_조회_API_반영(users))) + } } } diff --git a/Projects/Feature/FeatureCategoryDetailDemo/Sources/FeatureCategoryDetailDemoApp.swift b/Projects/Feature/FeatureCategoryDetailDemo/Sources/FeatureCategoryDetailDemoApp.swift index 39d93f7c..9424c7a4 100644 --- a/Projects/Feature/FeatureCategoryDetailDemo/Sources/FeatureCategoryDetailDemoApp.swift +++ b/Projects/Feature/FeatureCategoryDetailDemo/Sources/FeatureCategoryDetailDemoApp.swift @@ -21,7 +21,21 @@ struct FeatureCategoryDetailDemoApp: App { CategoryDetailView( store: Store( initialState: .init( - category: CategoryItemInquiryResponse.mock.toDomain() + category: BaseCategoryItem( + id: 764, + userId: 213, + categoryName: "playlist", + categoryImage: BaseCategoryImage( + imageId: 13, + imageURL: "https://pokit-s3.s3.ap-northeast-2.amazonaws.com/category-image/music.png" + ), + contentCount: 3, + createdAt: "2024.12.03", + openType: .공개, + keywordType: .음악, + userCount: 0, + isFavorite: false + ) ), reducer: { CategoryDetailFeature() } ) diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift index 81644e39..5f2d4f03 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift @@ -219,10 +219,13 @@ private extension PokitCategorySettingView { Text("포킷 키워드") .pokitFont(.b1(.b)) .foregroundStyle(.pokit(.text(.primary))) + Spacer() + Image(.icon(.arrowRight)) } .buttonStyle(.plain) + Text(store.keywordSelectType.label) .pokitFont(.detail1) .foregroundStyle(store.keywordSelectType.fontColor) From 3d83219b9d34db2b643335129b27c4921b1ff4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:09:06 +0900 Subject: [PATCH 03/15] =?UTF-8?q?[feat]=20#211=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20api=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Category/CategoryClient+LiveKey.swift | 11 +++++++++++ .../Category/CategoryClient+TestKey.swift | 5 ++++- .../Data/Network/Category/CategoryClient.swift | 10 ++++++++++ .../Network/Category/CategoryEndpoint.swift | 18 +++++++++++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift index ded81e40..622500e4 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift @@ -55,6 +55,17 @@ extension CategoryClient: DependencyKey { }, 포킷_초대된_유저_목록_조회: { id in try await provider.request(.포킷_초대된_유저_목록_조회(categoryId: id)) + }, + 포킷_내보내기: { categoryId, resignUserId in + try await provider.requestNoBody( + .포킷_내보내기(categoryId: categoryId, resignUserId: resignUserId) + ) + }, + 포킷_나가기: { categoryId in + try await provider.requestNoBody(.포킷_나가기(categoryId: categoryId)) + }, + 포킷_초대_수락: { categoryId in + try await provider.requestNoBody(.포킷_초대_수락(categoryId: categoryId)) } ) }() diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift index 2d9f4fb7..9c339d2a 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift @@ -19,7 +19,10 @@ extension CategoryClient: TestDependencyKey { 카테고리_상세_조회: { _ in .mock }, 공유받은_카테고리_조회: { _, _ in .mock }, 공유받은_카테고리_저장: { _ in }, - 포킷_초대된_유저_목록_조회: { _ in [.mock] } + 포킷_초대된_유저_목록_조회: { _ in [.mock] }, + 포킷_내보내기: { _, _ in }, + 포킷_나가기: { _ in }, + 포킷_초대_수락: { _ in } ) }() } diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift index 1fe8c67d..bce62c40 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift @@ -39,4 +39,14 @@ public struct CategoryClient { public var 포킷_초대된_유저_목록_조회: @Sendable ( _ categoryId: Int ) async throws -> [InvitedUserResponse] + public var 포킷_내보내기: @Sendable ( + _ categoryId: Int, + _ resignUserId: Int + ) async throws -> Void + public var 포킷_나가기: @Sendable ( + _ categoryId: Int + ) async throws -> Void + public var 포킷_초대_수락: @Sendable ( + _ categoryId: Int + ) async throws -> Void } diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift index 91eeaed8..34fcd2c8 100644 --- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryEndpoint.swift @@ -21,6 +21,9 @@ public enum CategoryEndpoint { case 공유받은_카테고리_조회(categoryId: String, model: BasePageableRequest) case 공유받은_카테고리_저장(model: CopiedCategoryRequest) case 포킷_초대된_유저_목록_조회(categoryId: Int) + case 포킷_내보내기(categoryId: Int, resignUserId: Int) + case 포킷_나가기(categoryId: Int) + case 포킷_초대_수락(categoryId: Int) } @@ -56,6 +59,12 @@ extension CategoryEndpoint: TargetType { return "/share" case let .포킷_초대된_유저_목록_조회(categoryId): return "/\(categoryId)/invited" + case let .포킷_내보내기(categoryId, resignUserId): + return "/share/resign/\(categoryId)/\(resignUserId)" + case let .포킷_나가기(categoryId): + return "/share/out/\(categoryId)" + case let .포킷_초대_수락(categoryId): + return "/share/accept/\(categoryId)" } } @@ -76,7 +85,10 @@ extension CategoryEndpoint: TargetType { return .get case .카테고리생성, - .공유받은_카테고리_저장: + .공유받은_카테고리_저장, + .포킷_내보내기, + .포킷_나가기, + .포킷_초대_수락: return .post } } @@ -119,6 +131,10 @@ extension CategoryEndpoint: TargetType { return .requestJSONEncodable(model) case .포킷_초대된_유저_목록_조회: return .requestPlain + case .포킷_내보내기, + .포킷_나가기, + .포킷_초대_수락: + return .requestPlain } } public var headers: [String: String]? { From b4efda23bbe3625ddf93f5e6c45d88a1aa97d9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:09:25 +0900 Subject: [PATCH 04/15] =?UTF-8?q?[feat]=20#211=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=ED=86=A1=20=EA=B3=B5=EC=9C=A0=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=ED=83=AC=ED=94=8C=EB=A6=BF=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/CategoryKaKaoShareModel.swift | 8 +++++ .../Share/KakaoShareClient+LiveKey.swift | 30 ++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Model/CategoryKaKaoShareModel.swift b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Model/CategoryKaKaoShareModel.swift index 5ede98ca..018d9210 100644 --- a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Model/CategoryKaKaoShareModel.swift +++ b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Model/CategoryKaKaoShareModel.swift @@ -8,15 +8,23 @@ import Foundation public struct CategoryKaKaoShareModel { + public enum ShareType: String { + case 공유 = "share" + case 초대 = "invite" + } + + let shareType: ShareType let categoryName: String let categoryId: Int let imageURL: String public init( + shareType: ShareType, categoryName: String, categoryId: Int, imageURL: String ) { + self.shareType = shareType self.categoryName = categoryName self.categoryId = categoryId self.imageURL = imageURL diff --git a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift index 925454c3..a35e43df 100644 --- a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift @@ -19,8 +19,10 @@ extension KakaoShareClient: DependencyKey { /// 딥링크 let appLink = Link( androidExecutionParams: [ + "shareType": model.shareType.rawValue, "categoryId": "\(model.categoryId)" ], iosExecutionParams: [ + "shareType": model.shareType.rawValue, "categoryId": "\(model.categoryId)" ] ) @@ -32,12 +34,24 @@ extension KakaoShareClient: DependencyKey { ) /// 카카오톡 메세지 내용 - let content = Content( - title: "\(model.categoryName) 포킷을 공유받았어요!", - imageUrl: URL(string: model.imageURL), - description: "소중한 링크들이 담긴 포킷을 Pokit 앱에서 지금 바로 확인해보세요!", - link: appLink - ) + let content = { + switch model.shareType { + case .공유: + Content( + title: "\(model.categoryName) 포킷을 공유받았어요!", + imageUrl: URL(string: model.imageURL), + description: "소중한 링크들이 담긴 포킷을 Pokit 앱에서 지금 바로 확인해보세요!", + link: appLink + ) + case .초대: + Content( + title: "\(model.categoryName) 포킷 초대장이 왔어요!", + imageUrl: URL(string: model.imageURL), + description: "소중한 링크들이 담긴 포킷을 Pokit 앱에서 지금 바로 침여해보세요!", + link: appLink + ) + } + }() /// 피드 템플릿 let template = FeedTemplate( @@ -60,7 +74,9 @@ extension KakaoShareClient: DependencyKey { return } - let serverCallbackArgs = ["categoryId": "\(model.categoryId)"] + let serverCallbackArgs = [ + "categoryId": "\(model.categoryId)" + ] ShareApi.shared.shareDefault( templateObject: templateJsonObject, From d8a3f05fd6bf6a297fac7d749590440e8befadcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:09:58 +0900 Subject: [PATCH 05/15] =?UTF-8?q?[feat]=20#211=20=ED=8F=AC=ED=82=B7=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20-=20=EC=B4=88=EB=8C=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/MainTab/MainTabFeature.swift | 40 +- .../Sources/MainTab/MainTabFeatureView.swift | 5 - .../App/Sources/MainTab/MainTabPath.swift | 79 +--- .../Sources/Components/PokitBottomSheet.swift | 13 +- .../Components/PokitDeleteBottomSheet.swift | 46 +- .../Sources/CategoryDetailFeature.swift | 420 +++++++++++++++--- .../Sources/CategoryDetailView.swift | 322 ++++++++++---- .../Sources/CategoryType.swift | 14 + .../PokitParticipantsBottomSheet.swift | 183 ++++++++ .../Sources/ContentCard/ContentCardView.swift | 18 +- .../ContentSettingFeature.swift | 97 ++-- .../Sources/PokitRootFeature.swift | 1 + 12 files changed, 925 insertions(+), 313 deletions(-) create mode 100644 Projects/Feature/FeatureCategoryDetail/Sources/CategoryType.swift create mode 100644 Projects/Feature/FeatureCategoryDetail/Sources/PokitParticipantsBottomSheet.swift diff --git a/Projects/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift index b6096ec8..57efe647 100644 --- a/Projects/App/Sources/MainTab/MainTabFeature.swift +++ b/Projects/App/Sources/MainTab/MainTabFeature.swift @@ -10,6 +10,7 @@ import ComposableArchitecture import FeaturePokit import FeatureRecommend import FeatureContentDetail +import FeatureCategoryDetail import Domain import DSKit import Util @@ -79,14 +80,14 @@ public struct MainTabFeature { public enum InnerAction: Equatable { case 링크추가및수정이동(contentId: Int) case linkCopySuccess(URL?) - case 공유포킷_이동(sharedCategory: CategorySharing.SharedCategory) + case 공유받은_카테고리_이동(category: BaseCategoryItem, type: CategoryType) case 경고_띄움(BaseError) case errorSheetPresented(Bool) case 링크팝업_활성화(PokitLinkPopup.PopupType) case 카테고리상세_이동(category: BaseCategoryItem) } public enum AsyncAction: Equatable { - case 공유받은_카테고리_조회(categoryId: Int) + case 공유받은_카테고리_조회(categoryId: Int, shareType: String?) } public enum ScopeAction: Equatable { case doNothing } public enum DelegateAction: Equatable { @@ -208,7 +209,9 @@ private extension MainTabFeature { let categoryIdString = queryItems.first(where: { $0.name == "categoryId" })?.value, let categoryId = Int(categoryIdString) else { return .none } - + + let shareType = queryItems.first(where: { $0.name == "shareType" })?.value + switch state.selectedTab { case .pokit: amplitudeTrack(.view_home_pokit(entryPoint: "deeplink")) @@ -216,7 +219,7 @@ private extension MainTabFeature { amplitudeTrack(.view_home_recommend(entryPoint: "deeplink")) } - return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId))) + return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId, shareType: shareType))) case .경고_확인버튼_클릭: state.error = nil return .run { send in await send(.inner(.errorSheetPresented(false))) } @@ -275,18 +278,39 @@ private extension MainTabFeature { } state.path.append(.카테고리상세(.init(category: category))) return .none + + case let .공유받은_카테고리_이동(category, type): + state.path.append(.카테고리상세(.init(type: type, category: category))) + return .none + default: return .none } } /// - Async Effect func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect { switch action { - case let .공유받은_카테고리_조회(categoryId: categoryId): + case let .공유받은_카테고리_조회(categoryId: categoryId, shareType: shareType): return .run { send in do { - let request = BasePageableRequest(page: 0, size: 10, sort: ["createdAt", "desc"]) - let sharedCategory = try await categoryClient.공유받은_카테고리_조회("\(categoryId)", request).toDomain() - await send(.inner(.공유포킷_이동(sharedCategory: sharedCategory)), animation: .smooth) + let request = BasePageableRequest(page: 0, size: 10, sort: ["createdAt,desc"]) + let response = try await categoryClient.공유받은_카테고리_조회("\(categoryId)", request) + let category = BaseCategoryItem( + id: response.category.categoryId, + userId: 0, + categoryName: response.category.categoryName, + categoryImage: BaseCategoryImage( + imageId: response.category.categoryImageId, + imageURL: response.category.categoryImageUrl + ), + contentCount: response.category.contentCount, + createdAt: "", + openType: .공개, + keywordType: .default, + userCount: 0, + isFavorite: false + ) + let type: CategoryType = shareType == "invite" ? .초대 : .공유 + await send(.inner(.공유받은_카테고리_이동(category: category, type: type)), animation: .smooth) } catch { guard let errorResponse = error as? ErrorResponse else { return } let errorDomain = BaseError(response: errorResponse) diff --git a/Projects/App/Sources/MainTab/MainTabFeatureView.swift b/Projects/App/Sources/MainTab/MainTabFeatureView.swift index 6388ef08..bc98f4c8 100644 --- a/Projects/App/Sources/MainTab/MainTabFeatureView.swift +++ b/Projects/App/Sources/MainTab/MainTabFeatureView.swift @@ -16,7 +16,6 @@ import FeatureContentDetail import FeatureContentSetting import FeatureCategoryDetail import FeatureContentList -import FeatureCategorySharing @ViewAction(for: MainTabFeature.self) public struct MainTabView: View { @@ -67,10 +66,6 @@ public extension MainTabView { if let store = store.scope(state: \.링크목록, action: \.링크목록) { ContentListView(store: store) } - case .링크공유: - if let store = store.scope(state: \.링크공유, action: \.링크공유) { - CategorySharingView(store: store) - } } if self.store.linkPopup != nil { diff --git a/Projects/App/Sources/MainTab/MainTabPath.swift b/Projects/App/Sources/MainTab/MainTabPath.swift index c1354d9a..98c31724 100644 --- a/Projects/App/Sources/MainTab/MainTabPath.swift +++ b/Projects/App/Sources/MainTab/MainTabPath.swift @@ -14,7 +14,6 @@ import FeatureCategorySetting import FeatureContentDetail import FeatureContentSetting import FeatureContentList -import FeatureCategorySharing import Domain import Util @@ -29,7 +28,6 @@ public struct MainTabPath { case 링크추가및수정(ContentSettingFeature.State) case 카테고리상세(CategoryDetailFeature.State) case 링크목록(ContentListFeature.State) - case 링크공유(CategorySharingFeature.State) } public enum Action { @@ -40,7 +38,6 @@ public struct MainTabPath { case 링크추가및수정(ContentSettingFeature.Action) case 카테고리상세(CategoryDetailFeature.Action) case 링크목록(ContentListFeature.Action) - case 링크공유(CategorySharingFeature.Action) } public var body: some Reducer { @@ -51,7 +48,6 @@ public struct MainTabPath { Scope(state: \.링크추가및수정, action: \.링크추가및수정) { ContentSettingFeature() } Scope(state: \.카테고리상세, action: \.카테고리상세) { CategoryDetailFeature() } Scope(state: \.링크목록, action: \.링크목록) { ContentListFeature() } - Scope(state: \.링크공유, action: \.링크공유) { CategorySharingFeature() } } } @@ -101,18 +97,10 @@ public extension MainTabFeature { /// - 포킷 `추가` or `수정`이 성공적으로 `완료`되었을 때 case .path(.element(_, action: .포킷추가및수정(.delegate(.settingSuccess)))): state.path.removeLast() - guard let lastPath = state.path.last else { - switch state.selectedTab { - case .pokit: return .none - case .recommend: - return .send(.recommend(.delegate(.포킷_추가하기_완료))) - } - } - switch lastPath { - case .링크공유: - state.path.removeLast() - return .none - default: return .none + switch state.selectedTab { + case .pokit: return .none + case .recommend: + return .send(.recommend(.delegate(.포킷_추가하기_완료))) } /// - 포킷 카테고리 아이템 눌렀을 때 @@ -218,51 +206,26 @@ public extension MainTabFeature { return .send(.delegate(.로그아웃)) case .path(.element(_, action: .설정(.delegate(.회원탈퇴)))): return .send(.delegate(.회원탈퇴)) - case let .inner(.공유포킷_이동(sharedCategory: sharedCategory)): - state.path.append(.링크공유(CategorySharingFeature.State(sharedCategory: sharedCategory))) - return .none - - /// 링크 공유에서 컨텐츠 상세보기 - case let .path(.element(_, action: .링크공유(.delegate(.컨텐츠_아이템_클릭(categoryId: categoryId, content: content))))): - state.contentDetail = ContentDetailFeature.State(content: BaseContentDetail( - id: content.id, - category: BaseCategoryInfo( - categoryId: categoryId, - categoryName: content.categoryName - ), - title: content.title, - data: content.data, - memo: content.memo ?? "", - createdAt: content.createdAt, - favorites: nil, - alertYn: .no - )) - return .none - - case let .path(.element(_, action: .링크공유(.delegate(.공유받은_카테고리_추가(sharedCategory))))): - let category = BaseCategoryItem( - id: sharedCategory.categoryId, - userId: 0, - categoryName: sharedCategory.categoryName, - categoryImage: BaseCategoryImage( - imageId: sharedCategory.categoryImageId, - imageURL: sharedCategory.categoryImageUrl - ), - contentCount: sharedCategory.contentCount, - createdAt: "", - openType: .공개, - keywordType: .default, - userCount: 0, - isFavorite: false - ) - state.path.append(.포킷추가및수정(PokitCategorySettingFeature.State( - type: .공유추가, - category: category - ))) - return .none + case .path(.element(_, action: .알림함(.delegate(.alertBoxDismiss)))): let _ = state.path.popLast() return .none + + /// - 초대 수락 완료 + case .path(.element(_, action: .카테고리상세(.delegate(.초대_수락_완료)))): + state.path.removeLast() + return .send(.inner(.링크팝업_활성화(.success(title: "초대를 수락했습니다", until: 2))), animation: .pokitSpring) + + /// - 공유 포킷 저장 완료 + case .path(.element(_, action: .카테고리상세(.delegate(.저장_완료)))): + state.path.removeLast() + return .send(.inner(.링크팝업_활성화(.success(title: "포킷을 저장했습니다", until: 2))), animation: .pokitSpring) + + /// - 포킷 나가기 완료 + case .path(.element(_, action: .카테고리상세(.delegate(.포킷나가기)))): + state.path.removeLast() + return .send(.inner(.링크팝업_활성화(.success(title: "포킷에서 나갔습니다", until: 2))), animation: .pokitSpring) + default: return .none } } diff --git a/Projects/DSKit/Sources/Components/PokitBottomSheet.swift b/Projects/DSKit/Sources/Components/PokitBottomSheet.swift index b3295556..08499532 100644 --- a/Projects/DSKit/Sources/Components/PokitBottomSheet.swift +++ b/Projects/DSKit/Sources/Components/PokitBottomSheet.swift @@ -93,6 +93,9 @@ public struct PokitBottomSheet: View { case .delete: delegateSend?(.deleteCellButtonTapped) return + case .leave: + delegateSend?(.leaveCellButtonTapped) + return } } } @@ -103,31 +106,35 @@ public extension PokitBottomSheet { case share case edit case delete - + case leave + var name: String { switch self { case .favorite: return "즐겨찾기" case .share: return "공유하기" case .edit: return "수정하기" case .delete: return "삭제하기" + case .leave: return "나가기" } } - + var icon: PokitImage { switch self { case .favorite: return .icon(.star) case .share: return .icon(.share) case .edit: return .icon(.edit) case .delete: return .icon(.trash) + case .leave: return .icon(.arrowRight) } } } - + enum Delegate { case favoriteCellButtonTapped case shareCellButtonTapped case editCellButtonTapped case deleteCellButtonTapped + case leaveCellButtonTapped } } diff --git a/Projects/DSKit/Sources/Components/PokitDeleteBottomSheet.swift b/Projects/DSKit/Sources/Components/PokitDeleteBottomSheet.swift index 0a7f368d..fdcc4096 100644 --- a/Projects/DSKit/Sources/Components/PokitDeleteBottomSheet.swift +++ b/Projects/DSKit/Sources/Components/PokitDeleteBottomSheet.swift @@ -9,10 +9,10 @@ import SwiftUI public struct PokitDeleteBottomSheet: View { @State - private var height: CGFloat = 0 + private var height: CGFloat = 246 private let type: SheetType private let delegateSend: ((PokitDeleteBottomSheet.Delegate) -> Void)? - + public init( type: SheetType, delegateSend: ((PokitDeleteBottomSheet.Delegate) -> Void)? @@ -20,19 +20,20 @@ public struct PokitDeleteBottomSheet: View { self.type = type self.delegateSend = delegateSend } - + public var body: some View { GeometryReader { proxy in let bottomSafeArea = proxy.safeAreaInsets.bottom - + let topSafeArea = proxy.safeAreaInsets.top + VStack(spacing: 0) { /// - 텍스트 영역 VStack(spacing: 8) { Text(type.sheetTitle) .foregroundStyle(.pokit(.text(.primary))) .pokitFont(.title2) + Text(type.sheetContents) - .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) .foregroundStyle(.pokit(.text(.secondary))) .pokitFont(.b2(.m)) @@ -46,9 +47,9 @@ public struct PokitDeleteBottomSheet: View { state: .default(.primary), action: { delegateSend?(.cancelButtonTapped) } ) - + PokitBottomButton( - "삭제", + type.confirmButtonTitle, state: .filled(.primary), action: { delegateSend?(.deleteButtonTapped) } ) @@ -56,22 +57,23 @@ public struct PokitDeleteBottomSheet: View { } .padding(.horizontal, 20) .padding(.bottom, 36 - bottomSafeArea) - .background(.white) - .pokitPresentationCornerRadius() - .pokitPresentationBackground() - .presentationDragIndicator(.visible) + .padding(.top, 12 - topSafeArea) + .ignoresSafeArea(edges: [.bottom, .top]) .readHeight() .onPreferenceChange(HeightPreferenceKey.self) { height in if let height { self.height = height } } - .presentationDetents([.height(self.height)]) .onAppear { UINotificationFeedbackGenerator() .notificationOccurred(.warning) } } + .pokitPresentationCornerRadius() + .pokitPresentationBackground() + .presentationDragIndicator(.visible) + .presentationDetents([.height(height)]) } } //MARK: - Delegate @@ -79,20 +81,36 @@ public extension PokitDeleteBottomSheet { enum SheetType { case 링크삭제 case 포킷삭제 - + case 유저내보내기(String) + case 포킷나가기 + var sheetTitle: String { switch self { case .링크삭제: "링크를 정말 삭제하시겠습니까?" case .포킷삭제: "포킷을 정말 삭제하시겠습니까?" + case .유저내보내기: "유저를 내보내시겠습니까?" + case .포킷나가기: "포킷을 나가시겠습니까?" } } - + var sheetContents: String { switch self { case .링크삭제: return "함께 저장한 모든 정보가 삭제되며,\n복구하실 수 없습니다." case .포킷삭제: return "함께 저장한 모든 링크가 삭제되며,\n복구하실 수 없습니다." + case .유저내보내기(let nickname): + return "선택한 유저를 함께 공유 중인 포킷에서\n내보내시겠습니까?" + case .포킷나가기: + return "포킷을 나가면 해당 포킷이\n목록에서 사라집니다." + } + } + + var confirmButtonTitle: String { + switch self { + case .링크삭제, .포킷삭제: "삭제" + case .유저내보내기: "내보내기" + case .포킷나가기: "나가기" } } } diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 5d3f01d4..52715ddc 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -26,6 +26,8 @@ public struct CategoryDetailFeature { private var contentClient @Dependency(KakaoShareClient.self) private var kakaoShareClient + @Dependency(UserDefaultsClient.self) + private var userDefaults @Dependency(\.amplitude.track) private var amplitudeTrack @@ -63,13 +65,33 @@ public struct CategoryDetailFeature { var isCategorySheetPresented: Bool = false var isCategorySelectSheetPresented: Bool = false var isPokitDeleteSheetPresented: Bool = false + var isParticipantsSheetPresented: Bool = false + var isRemoveParticipantSheetPresented: Bool = false + var isLeaveSheetPresented: Bool = false + /// selected user + var selectedUserToRemove: InvitedUser? + var type: CategoryType /// pagenation var hasNext: Bool { domain.contentList.hasNext } var isLoading: Bool = true + /// computed properties + var invitedUsers: [InvitedUser] { + domain.invitedUsers + } + var isSharedCategory: Bool { + domain.invitedUsers.count >= 2 + } + /// 현재 로그인한 사용자의 ID + var currentUserId: Int? + var isCreator: Bool { + guard let currentUserId else { return false } + return domain.category.userId == currentUserId + } - public init(category: BaseCategoryItem) { + public init(type: CategoryType = .참여, category: BaseCategoryItem) { + self.type = type self.domain = .init(categpry: category) } } @@ -88,28 +110,37 @@ public struct CategoryDetailFeature { case binding(BindingAction) case dismiss case pagenation - + /// 즐겨찾기 or 안읽음 버튼 눌렀을 때 case 분류_버튼_눌렀을때(SortCollectType) case 정렬_버튼_눌렀을때 - case 공유_버튼_눌렀을때 + case 공유_버튼_눌렀을때(CategoryKaKaoShareModel.ShareType) case 카테고리_케밥_버튼_눌렀을때 case 카테고리_선택_버튼_눌렀을때 case 카테고리_선택했을때(BaseCategoryItem) case 뷰가_나타났을때 case 링크_추가_버튼_눌렀을때 + case 참여인원_버튼_눌렀을때 + case 초대_수락하기_버튼_눌렀을때 + case 저장하기_버튼_눌렀을때 } public enum InnerAction: Equatable { case 카테고리_시트_활성화(Bool) case 카테고리_선택_시트_활성화(Bool) case 카테고리_삭제_시트_활성화(Bool) - + case 참여인원_시트_활성화(Bool) + case 내보내기_확인_시트_활성화(Bool) + case 나가기_확인_시트_활성화(Bool) + case 카카오톡_공유(CategoryKaKaoShareModel.ShareType) + case 타입_변경(CategoryType) + case 카테고리_목록_조회_API_반영(BaseCategoryListInquiry) case 카테고리_내_컨텐츠_목록_조회_API_반영(BaseContentListInquiry) case pagenation_API_반영(BaseContentListInquiry) case pagenation_초기화 case 포킷_초대된_유저_목록_조회_API_반영([InvitedUser]) + case 내보낼_유저_선택(InvitedUser) } public enum AsyncAction: Equatable { @@ -118,14 +149,25 @@ public struct CategoryDetailFeature { case 페이징_재조회 case 클립보드_감지 case 포킷_초대된_유저_목록_조회_API + case 포킷_내보내기_API(categoryId: Int, resignUserId: Int) + case 포킷_나가기_API(categoryId: Int) + case 포킷_초대_수락_API(categoryId: Int) + case 공유받은_포킷_저장_API } public enum ScopeAction { case categoryBottomSheet(PokitBottomSheet.Delegate) case categoryDeleteBottomSheet(PokitDeleteBottomSheet.Delegate) + case participantsBottomSheet(ParticipantsBottomSheetDelegate) + case removeParticipantBottomSheet(PokitDeleteBottomSheet.Delegate) + case leaveBottomSheet(PokitDeleteBottomSheet.Delegate) case contents(IdentifiedActionOf) } - + + public enum ParticipantsBottomSheetDelegate: Equatable { + case removeParticipant(InvitedUser) + } + public enum DelegateAction: Equatable { case contentItemTapped(BaseContentItem) case linkCopyDetected(URL?) @@ -134,7 +176,10 @@ public struct CategoryDetailFeature { case 포킷삭제 case 포킷수정(BaseCategoryItem) case 포킷공유 + case 포킷나가기 case 카테고리_내_컨텐츠_목록_조회 + case 초대_수락_완료 + case 저장_완료 } } @@ -216,19 +261,8 @@ private extension CategoryDetailFeature { .send(.async(.카테고리_내_컨텐츠_목록_조회_API)) ) - case .공유_버튼_눌렀을때: - amplitudeTrack(.share_link( - linkId: "\(state.domain.category.id)", - shareTarget: "kakaotalk" - )) - kakaoShareClient.카테고리_카카오톡_공유( - CategoryKaKaoShareModel( - categoryName: state.domain.category.categoryName, - categoryId: state.domain.category.id, - imageURL: state.domain.category.categoryImage.imageURL - ) - ) - return .none + case let .공유_버튼_눌렀을때(shareType): + return .send(.inner(.카카오톡_공유(shareType))) case .링크_추가_버튼_눌렀을때: let id = state.category.id @@ -252,6 +286,12 @@ private extension CategoryDetailFeature { return .run { _ in await dismiss() } case .뷰가_나타났을때: + /// 현재 로그인한 사용자 ID 가져오기 + if let userIdString = userDefaults.stringKey(.userId), + let userId = Int(userIdString) { + state.currentUserId = userId + } + /// 단순 조회 액션들의 나열이기 때문에 merge로 우선 처리 return .merge( .send(.async(.카테고리_내_컨텐츠_목록_조회_API)), @@ -262,6 +302,16 @@ private extension CategoryDetailFeature { case .pagenation: state.domain.pageable.page += 1 return .send(.async(.카테고리_내_컨텐츠_목록_조회_API)) + + case .참여인원_버튼_눌렀을때: + return .send(.inner(.참여인원_시트_활성화(true))) + + case .초대_수락하기_버튼_눌렀을때: + let categoryId = state.domain.category.id + return .send(.async(.포킷_초대_수락_API(categoryId: categoryId))) + + case .저장하기_버튼_눌렀을때: + return .send(.async(.공유받은_포킷_저장_API)) } } @@ -319,6 +369,41 @@ private extension CategoryDetailFeature { case .포킷_초대된_유저_목록_조회_API_반영(let users): state.domain.invitedUsers = users return .none + + case let .참여인원_시트_활성화(presented): + state.isParticipantsSheetPresented = presented + return .none + + case let .내보내기_확인_시트_활성화(presented): + state.isRemoveParticipantSheetPresented = presented + return .none + + case let .나가기_확인_시트_활성화(presented): + state.isLeaveSheetPresented = presented + return .none + + case let .내보낼_유저_선택(user): + state.selectedUserToRemove = user + return .send(.inner(.내보내기_확인_시트_활성화(true))) + + case let .타입_변경(type): + state.type = type + return .none + + case let .카카오톡_공유(shareType): + amplitudeTrack(.share_link( + linkId: "\(state.domain.category.id)", + shareTarget: "kakaotalk" + )) + kakaoShareClient.카테고리_카카오톡_공유( + CategoryKaKaoShareModel( + shareType: shareType, + categoryName: state.domain.category.categoryName, + categoryId: state.domain.category.id, + imageURL: state.domain.category.categoryImage.imageURL + ) + ) + return .none } } @@ -333,63 +418,170 @@ private extension CategoryDetailFeature { } case .카테고리_내_컨텐츠_목록_조회_API: - return .run { [ - id = state.domain.category.id, - pageable = state.domain.pageable, - condition = state.domain.condition - ] send in - let request = BasePageableRequest( - page: pageable.page, - size: pageable.size, - sort: pageable.sort - ) - let conditionRequest = BaseConditionRequest(categoryIds: condition.categoryIds, isRead: condition.isUnreadFlitered, favorites: condition.isFavoriteFlitered) - let contentList = try await contentClient.카테고리_내_컨텐츠_목록_조회( - "\(id)", request, conditionRequest - ).toDomain() - pageable.page == 0 - ? await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentList)), animation: .pokitDissolve) - : await send(.inner(.pagenation_API_반영(contentList))) + switch state.type { + case .초대, .공유: + return .run { [ + id = state.domain.category.id, + categoryName = state.domain.category.categoryName, + pageable = state.domain.pageable + ] send in + let request = BasePageableRequest( + page: pageable.page, + size: pageable.size, + sort: pageable.sort + ) + let response = try await categoryClient.공유받은_카테고리_조회("\(id)", request) + + // SharedCategoryResponse.Content를 BaseContentItem으로 변환 + let baseContentItems = response.contents.data.map { content in + BaseContentItem( + id: content.contentId, + categoryName: categoryName, + categoryId: id, + title: content.title, + memo: content.memo, + thumbNail: content.thumbNail, + data: content.data, + domain: content.domain, + createdAt: content.createdAt, + isRead: false, + isFavorite: false, + keyword: nil + ) + } + + let contentList = BaseContentListInquiry( + data: baseContentItems, + page: response.contents.page, + size: response.contents.size, + sort: response.contents.sort.map { $0.toDomain() }, + hasNext: response.contents.hasNext + ) + + pageable.page == 0 + ? await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentList)), animation: .pokitDissolve) + : await send(.inner(.pagenation_API_반영(contentList))) + } + + case .참여: + return .run { [ + id = state.domain.category.id, + pageable = state.domain.pageable, + condition = state.domain.condition + ] send in + let request = BasePageableRequest( + page: pageable.page, + size: pageable.size, + sort: pageable.sort + ) + let conditionRequest = BaseConditionRequest(categoryIds: condition.categoryIds, isRead: condition.isUnreadFlitered, favorites: condition.isFavoriteFlitered) + let contentList = try await contentClient.카테고리_내_컨텐츠_목록_조회( + "\(id)", request, conditionRequest + ).toDomain() + pageable.page == 0 + ? await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentList)), animation: .pokitDissolve) + : await send(.inner(.pagenation_API_반영(contentList))) + } } case .페이징_재조회: - return .run { [ - pageable = state.domain.pageable, - categoryId = state.domain.category.id, - condition = state.domain.condition - ] send in - let stream = AsyncThrowingStream { continuation in - Task { - for page in 0...pageable.page { - let paeagableRequest = BasePageableRequest( - page: page, - size: pageable.size, - sort: pageable.sort - ) - let conditionRequest = BaseConditionRequest( - categoryIds: condition.categoryIds, - isRead: condition.isUnreadFlitered, - favorites: condition.isFavoriteFlitered - ) - let contentList = try await contentClient.카테고리_내_컨텐츠_목록_조회( - "\(categoryId)", - paeagableRequest, - conditionRequest - ).toDomain() - continuation.yield(contentList) + switch state.type { + case .초대, .공유: + return .run { [ + pageable = state.domain.pageable, + categoryId = state.domain.category.id, + categoryName = state.domain.category.categoryName + ] send in + let stream = AsyncThrowingStream { continuation in + Task { + for page in 0...pageable.page { + let paeagableRequest = BasePageableRequest( + page: page, + size: pageable.size, + sort: pageable.sort + ) + let response = try await categoryClient.공유받은_카테고리_조회("\(categoryId)", paeagableRequest) + + // SharedCategoryResponse.Content를 BaseContentItem으로 변환 + let baseContentItems = response.contents.data.map { content in + BaseContentItem( + id: content.contentId, + categoryName: categoryName, + categoryId: categoryId, + title: content.title, + memo: content.memo, + thumbNail: content.thumbNail, + data: content.data, + domain: content.domain, + createdAt: content.createdAt, + isRead: false, + isFavorite: false, + keyword: nil + ) + } + + let contentList = BaseContentListInquiry( + data: baseContentItems, + page: response.contents.page, + size: response.contents.size, + sort: response.contents.sort.map { $0.toDomain() }, + hasNext: response.contents.hasNext + ) + continuation.yield(contentList) + } + continuation.finish() } - continuation.finish() } + var contentItems: BaseContentListInquiry? = nil + for try await contentList in stream { + let items = contentItems?.data ?? [] + let newItems = contentList.data ?? [] + contentItems = contentList + contentItems?.data = items + newItems + } + guard let contentItems else { return } + await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentItems)), animation: .pokitSpring) } - var contentItems: BaseContentListInquiry? = nil - for try await contentList in stream { - let items = contentItems?.data ?? [] - let newItems = contentList.data ?? [] - contentItems = contentList - contentItems?.data = items + newItems + + case .참여: + return .run { [ + pageable = state.domain.pageable, + categoryId = state.domain.category.id, + condition = state.domain.condition + ] send in + let stream = AsyncThrowingStream { continuation in + Task { + for page in 0...pageable.page { + let paeagableRequest = BasePageableRequest( + page: page, + size: pageable.size, + sort: pageable.sort + ) + let conditionRequest = BaseConditionRequest( + categoryIds: condition.categoryIds, + isRead: condition.isUnreadFlitered, + favorites: condition.isFavoriteFlitered + ) + let contentList = try await contentClient.카테고리_내_컨텐츠_목록_조회( + "\(categoryId)", + paeagableRequest, + conditionRequest + ).toDomain() + continuation.yield(contentList) + } + continuation.finish() + } + } + var contentItems: BaseContentListInquiry? = nil + for try await contentList in stream { + let items = contentItems?.data ?? [] + let newItems = contentList.data ?? [] + contentItems = contentList + contentItems?.data = items + newItems + } + guard let contentItems else { return } + await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentItems)), animation: .pokitSpring) } - guard let contentItems else { return } - await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentItems)), animation: .pokitSpring) } case .클립보드_감지: @@ -406,26 +598,75 @@ private extension CategoryDetailFeature { let users = response.map { $0.toDomain() } await send(.inner(.포킷_초대된_유저_목록_조회_API_반영(users))) } + + case let .포킷_내보내기_API(categoryId, resignUserId): + return .run { send in + try await categoryClient.포킷_내보내기(categoryId, resignUserId) + await send(.inner(.내보내기_확인_시트_활성화(false))) + await send(.async(.포킷_초대된_유저_목록_조회_API)) + } + + case let .포킷_나가기_API(categoryId): + return .run { send in + try await categoryClient.포킷_나가기(categoryId) + await send(.inner(.나가기_확인_시트_활성화(false))) + await send(.delegate(.포킷나가기)) + await dismiss() + } + + case let .포킷_초대_수락_API(categoryId): + return .run { send in + try await categoryClient.포킷_초대_수락(categoryId) + await send(.inner(.타입_변경(.참여))) + await send(.delegate(.초대_수락_완료)) + } + + case .공유받은_포킷_저장_API: + return .run { [category = state.domain.category] send in + let request = CopiedCategoryRequest( + originCategoryId: category.id, + categoryName: category.categoryName, + categoryImageId: category.categoryImage.id, + keyword: category.keywordType.rawValue, + openType: category.openType.rawValue + ) + try await categoryClient.공유받은_카테고리_저장(request) + await send(.inner(.타입_변경(.참여))) + await send(.delegate(.저장_완료)) + } } } /// - Scope Effect func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect { switch action { - /// - 카테고리에 대한 `공유` / `수정` / `삭제` Delegate + /// - 카테고리에 대한 `공유` / `수정` / `삭제` / `나가기` Delegate case .categoryBottomSheet(let delegateAction): switch delegateAction { + case .shareCellButtonTapped: + return .run { send in + await send(.inner(.카테고리_시트_활성화(false))) + await send(.inner(.카카오톡_공유(.공유))) + } + case .editCellButtonTapped: return .run { [category = state.category] send in await send(.inner(.카테고리_시트_활성화(false))) await send(.delegate(.포킷수정(category))) } + case .deleteCellButtonTapped: return .run { send in await send(.inner(.카테고리_시트_활성화(false))) await send(.inner(.카테고리_삭제_시트_활성화(true))) } - + + case .leaveCellButtonTapped: + return .run { send in + await send(.inner(.카테고리_시트_활성화(false))) + await send(.inner(.나가기_확인_시트_활성화(true))) + } + default: return .none } /// - 카테고리의 `삭제`를 눌렀을 때 Sheet Delegate @@ -443,6 +684,45 @@ private extension CategoryDetailFeature { } } + /// - 참여인원 바텀시트 Delegate + case .participantsBottomSheet(let delegateAction): + switch delegateAction { + case .removeParticipant(let user): + return .run { send in + await send(.inner(.참여인원_시트_활성화(false))) + await send(.inner(.내보낼_유저_선택(user))) + } + } + + /// - 유저 내보내기 확인 바텀시트 Delegate + case .removeParticipantBottomSheet(let delegateAction): + switch delegateAction { + case .cancelButtonTapped: + return .run { send in + await send(.inner(.내보내기_확인_시트_활성화(false))) + } + + case .deleteButtonTapped: + guard let selectedUser = state.selectedUserToRemove else { return .none } + return .run { [categoryId = state.domain.category.id] send in + await send(.async(.포킷_내보내기_API(categoryId: categoryId, resignUserId: selectedUser.id))) + } + } + + /// - 포킷 나가기 확인 바텀시트 Delegate + case .leaveBottomSheet(let delegateAction): + switch delegateAction { + case .cancelButtonTapped: + return .run { send in + await send(.inner(.나가기_확인_시트_활성화(false))) + } + + case .deleteButtonTapped: + return .run { [categoryId = state.domain.category.id] send in + await send(.async(.포킷_나가기_API(categoryId: categoryId))) + } + } + case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content)))): return .send(.delegate(.contentItemTapped(content))) case .contents: diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift index 0e66d0ff..d197df78 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift @@ -30,26 +30,27 @@ public struct CategoryDetailView: View { public extension CategoryDetailView { var body: some View { WithPerceptionTracking { - ScrollView(showsIndicators: false) { - VStack(spacing: 24) { - header - scrollObservableView - LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { - Section { - contentScrollView - .padding(.horizontal, 20) - } header: { - VStack(spacing: 24) { - PokitDivider() - filterHeader - .padding(.horizontal, 20) - } - .padding(.bottom, 16) - .background(.white) - } + List { + Section { header } + .listRowInsets(EdgeInsets(.zero)) + + Section { + contentScrollView + } header: { + VStack(spacing: 24) { + PokitDivider() + + filterHeader + .padding(.horizontal, 20) } + .padding(.bottom, 16) + .background(.pokit(.bg(.base))) } + .listRowInsets(EdgeInsets(.zero)) } + .listStyle(.plain) + .listRowSpacing(0) + .background { scrollObservableView } .onPreferenceChange(ScrollOffsetKey.self) { if $0 != targetOffset { currentOffset = $0 @@ -64,29 +65,21 @@ public extension CategoryDetailView { }) .padding(.top, 12) .pokitNavigationBar { navigationBar } - .overlay( - if: !store.isFavoriteCategory, - alignment: .bottomTrailing - ) { - Button(action: { send(.링크_추가_버튼_눌렀을때) }) { - Image(.icon(.plus)) - .resizable() - .frame(width: 36, height: 36) - .padding(12) - .foregroundStyle(.pokit(.icon(.inverseWh))) - .background { - RoundedRectangle(cornerRadius: 9999, style: .continuous) - .fill(.pokit(.bg(.brand))) - } - .frame(width: 60, height: 60) - } - .padding(.trailing, 20) - .padding(.bottom, 39) + .overlay(alignment: .bottom) { + bottomOverlay } .ignoresSafeArea(edges: .bottom) .sheet(isPresented: $store.isCategorySheetPresented) { + let items: [PokitBottomSheet.Item] = { + if store.isSharedCategory { + return [.edit, .share, .leave] + } else { + return [.edit, .share, .delete] + } + }() + PokitBottomSheet( - items: [.edit, .delete], + items: items, delegateSend: { store.send(.scope(.categoryBottomSheet($0))) } ) } @@ -110,6 +103,30 @@ public extension CategoryDetailView { delegateSend: { store.send(.scope(.categoryDeleteBottomSheet($0))) } ) } + .sheet(isPresented: $store.isParticipantsSheetPresented) { + PokitParticipantsBottomSheet( + title: "포킷 공유 유저", + participants: store.invitedUsers, + isCreator: store.isCreator, + currentUserId: store.currentUserId, + creatorUserId: store.category.userId, + delegateSend: { store.send(.scope(.participantsBottomSheet($0))) } + ) + } + .sheet(isPresented: $store.isRemoveParticipantSheetPresented) { + if let selectedUser = store.selectedUserToRemove { + PokitDeleteBottomSheet( + type: .유저내보내기(selectedUser.nickname), + delegateSend: { store.send(.scope(.removeParticipantBottomSheet($0))) } + ) + } + } + .sheet(isPresented: $store.isLeaveSheetPresented) { + PokitDeleteBottomSheet( + type: .포킷나가기, + delegateSend: { store.send(.scope(.leaveBottomSheet($0))) } + ) + } .task { await send(.뷰가_나타났을때).finish() } } } @@ -124,13 +141,20 @@ private extension CategoryDetailView { action: { send(.dismiss) } ) } - if !store.isFavoriteCategory { + + if !store.isFavoriteCategory && store.type == .참여 { PokitHeaderItems(placement: .trailing) { + if store.isSharedCategory { + participantsView + .pokitBlurReplaceTransition(.pokitDissolve) + } + PokitToolbarButton( .icon(.kebab), action: { send(.카테고리_케밥_버튼_눌렀을때) } ) } + .animation(.pokitDissolve, value: store.isSharedCategory) } } .padding(.top, 8) @@ -171,7 +195,7 @@ private extension CategoryDetailView { HStack(spacing: 3.5) { let iconColor: Color = .pokit(.icon(.secondary)) let textColor: Color = .pokit(.text(.tertiary)) - + if store.category.openType == .비공개 { HStack(spacing: 2) { Image(.icon(.lock)) @@ -200,48 +224,73 @@ private extension CategoryDetailView { } } .padding(.bottom, 16) - PokitIconLButton( - "공유", - .icon(.share), - state: .filled(.primary), - size: .medium, - shape: .round, - action: { send(.공유_버튼_눌렀을때) } - ) + + switch store.type { + case .참여: + PokitIconLButton( + "초대", + .icon(.invite), + state: .filled(.primary), + size: .medium, + shape: .round, + action: { send(.공유_버튼_눌렀을때(.초대)) } + ) + case .초대: + PokitTextButton( + "초대 수락하기", + state: .filled(.primary), + size: .medium, + shape: .round, + action: { send(.초대_수락하기_버튼_눌렀을때) } + ) + case .공유: + PokitTextButton( + "저장하기", + state: .filled(.primary), + size: .medium, + shape: .round, + action: { send(.저장하기_버튼_눌렀을때) } + ) + } } } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) } @ViewBuilder var filterHeader: some View { let isFavoriteCategory = store.isFavoriteCategory let favoriteContentsCount = store.contents.filter { $0.content.isFavorite ?? false }.count - + HStack(spacing: isFavoriteCategory ? 2 : 8) { if isFavoriteCategory { Image(.icon(.link)) .resizable() .frame(width: 16, height: 16) .foregroundStyle(.pokit(.icon(.secondary))) - + Text("\(favoriteContentsCount)개") .foregroundStyle(.pokit(.text(.tertiary))) .pokitFont(.b2(.m)) - - } else { + + } else if store.type == .참여 { favoriteButton - + unreadButton } - + Spacer() - - PokitIconLTextLink( - store.sortType.title, - icon: .icon(.align), - action: { send(.정렬_버튼_눌렀을때) } - ) - .contentTransition(.numericText()) + + if store.type == .참여 { + PokitIconLTextLink( + store.sortType.title, + icon: .icon(.align), + action: { send(.정렬_버튼_눌렀을때) } + ) + .contentTransition(.numericText()) + } } } @@ -294,51 +343,144 @@ private extension CategoryDetailView { if !store.isLoading { if store.contents.isEmpty { PokitCaution(type: .포킷상세_링크없음) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) } else { - LazyVStack(spacing: 0) { - ForEach( - Array(store.scope(state: \.contents, action: \.contents)) - ) { store in - let isFirst = store.state.id == self.store.contents.first?.id - let isLast = store.state.id == self.store.contents.last?.id - - if !self.store.isFavoriteCategory { - ContentCardView( - store: store, - type: .linkList, - isFirst: isFirst, - isLast: isLast - ) - } else if store.content.isFavorite == true { - ContentCardView( - store: store, - type: .linkList, - isFirst: isFirst, - isLast: isLast - ) - } - } + ForEach( + Array(store.scope(state: \.contents, action: \.contents)) + ) { store in + let isFirst = store.state.id == self.store.contents.first?.id + let isLast = store.state.id == self.store.contents.last?.id - if store.hasNext { - PokitLoading() - .task { await send(.pagenation).finish() } + if !self.store.isFavoriteCategory { + ContentCardView( + store: store, + type: .linkList, + isFirst: isFirst, + isLast: isLast, + showKebab: self.store.type == .참여 + ) + } else if store.content.isFavorite == true { + ContentCardView( + store: store, + type: .linkList, + isFirst: isFirst, + isLast: isLast, + showKebab: self.store.type == .참여 + ) } - - Spacer() } - .padding(.bottom, 36) + + if store.hasNext { + PokitLoading() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) + .task { await send(.pagenation).finish() } + } } } else { PokitLoading() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) } } + @ViewBuilder + var bottomOverlay: some View { + if store.type == .참여 && !store.isFavoriteCategory { + HStack { + Spacer() + Button(action: { send(.링크_추가_버튼_눌렀을때) }) { + Image(.icon(.plus)) + .resizable() + .frame(width: 36, height: 36) + .padding(12) + .foregroundStyle(.pokit(.icon(.inverseWh))) + .background { + RoundedRectangle(cornerRadius: 9999, style: .continuous) + .fill(.pokit(.bg(.brand))) + } + .frame(width: 60, height: 60) + } + .padding(.trailing, 20) + .padding(.bottom, 39) + } + } + } + + @ViewBuilder + var participantsView: some View { + Button(action: { send(.참여인원_버튼_눌렀을때) }) { + GeometryReader { proxy in + let local = proxy.frame(in: .local) + let firstUser = store.invitedUsers.indices.contains(0) ? store.invitedUsers[0] : nil + let secondUser = store.invitedUsers.indices.contains(1) ? store.invitedUsers[1] : nil + + HStack(spacing: 2) { + participantsProfileImage(url: secondUser?.profile?.imageURL) + .overlay(Circle().stroke(.pokit(.border(.tertiary)), lineWidth: 1)) + + Text("\(store.invitedUsers.count)") + .foregroundStyle(.pokit(.text(.secondary))) + .pokitFont(.b3(.m)) + .frame(width: 18) + + Image(.icon(.arrowDown)) + .resizable() + .frame(width: 16, height: 16) + .foregroundStyle(.pokit(.icon(.tertiary))) + } + .padding(.trailing, 6) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(.pokit(.border(.tertiary)), lineWidth: 1) + ) + .offset(x: local.minX + 20, y: local.minY) + + participantsProfileImage(url: firstUser?.profile?.imageURL) + .overlay(Circle().stroke(.pokit(.border(.inverseWh)), lineWidth: 1)) + .offset(x: local.minX, y: local.minY) + } + .frame(width: 92, height: 28) + } + .buttonStyle(.plain) + } + + func participantsProfileImage(url: String?) -> some View { + Group { + if let url { + LazyImage(url: URL(string: url)) { phase in + Group { + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + } else { + PokitSpinner() + .foregroundStyle(.pokit(.icon(.brand))) + } + } + .animation(.pokitDissolve, value: phase.image) + } + } else { + Image(.image(.profile)) + .resizable() + .aspectRatio(contentMode: .fit) + } + } + .frame(width: 28, height: 28) + .clipShape(Circle()) + } + struct PokitCategorySheet: View { @State private var height: CGFloat = 0 var action: (BaseCategoryItem) -> Void var selectedItem: BaseCategoryItem? var list: [BaseCategoryItem] - + public init( selectedItem: BaseCategoryItem?, list: [BaseCategoryItem], @@ -348,7 +490,7 @@ private extension CategoryDetailView { self.list = list self.action = action } - + var body: some View { PokitList( selectedItem: selectedItem, diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryType.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryType.swift new file mode 100644 index 00000000..c4d5dd90 --- /dev/null +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryType.swift @@ -0,0 +1,14 @@ +// +// CategoryType.swift +// FeatureCategorySetting +// +// Created by 김도형 on 12/25/25. +// + +import Foundation + +public enum CategoryType { + case 참여 + case 초대 + case 공유 +} diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/PokitParticipantsBottomSheet.swift b/Projects/Feature/FeatureCategoryDetail/Sources/PokitParticipantsBottomSheet.swift new file mode 100644 index 00000000..b267f3a5 --- /dev/null +++ b/Projects/Feature/FeatureCategoryDetail/Sources/PokitParticipantsBottomSheet.swift @@ -0,0 +1,183 @@ +// +// PokitParticipantsBottomSheet.swift +// FeatureCategoryDetail +// +// Created by 김도형 on 12/25/25. +// + +import SwiftUI +import Domain +import DSKit +import NukeUI + +struct PokitParticipantsBottomSheet: View { + @State + private var height: CGFloat = 0 + let title: String + let participants: [InvitedUser] + let isCreator: Bool + let currentUserId: Int? + let creatorUserId: Int? + let delegateSend: ((CategoryDetailFeature.Action.ParticipantsBottomSheetDelegate) -> Void)? + + init( + title: String, + participants: [InvitedUser], + isCreator: Bool, + currentUserId: Int?, + creatorUserId: Int?, + delegateSend: ((CategoryDetailFeature.Action.ParticipantsBottomSheetDelegate) -> Void)? + ) { + self.title = title + self.participants = participants + self.isCreator = isCreator + self.currentUserId = currentUserId + self.creatorUserId = creatorUserId + self.delegateSend = delegateSend + } + + var body: some View { + VStack(spacing: 0) { + participantsList + } + .presentationDragIndicator(.visible) + .presentationDetents([.height(height)]) + .pokitPresentationCornerRadius() + .pokitPresentationBackground() + .readHeight() + .onPreferenceChange(HeightPreferenceKey.self) { height in + if let height { + self.height = height + } + } + .ignoresSafeArea(.all) + .padding(.top, 12) + .padding(.bottom, -20) + } + + private var headerView: some View { + HStack { + Text(title) + .pokitFont(.b1(.b)) + .foregroundStyle(.pokit(.text(.primary))) + + Spacer() + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + } + + @ViewBuilder + private var participantsList: some View { + let sortedParticipants = participants.sorted { first, second in + // 본인을 맨 위로 + if first.id == currentUserId { return true } + if second.id == currentUserId { return false } + return false + } + + ForEach(sortedParticipants) { participant in + let isLast = sortedParticipants.last == participant + + participantCell(participant) + .overlay(if: !isLast, alignment: .bottom) { + Rectangle().fill(.pokit(.border(.tertiary))) + .frame(height: 1) + } + } + } + + @ViewBuilder + private func participantCell(_ participant: InvitedUser) -> some View { + let isCurrentUser = currentUserId == participant.id + let isOwner = creatorUserId == participant.id + + HStack(spacing: 12) { + if let profile = participant.profile { + LazyImage(url: URL(string: profile.imageURL)) { state in + Group { + if let image = state.image { + image + .resizable() + } else { + Circle() + .fill(.pokit(.bg(.disable))) + } + } + .frame(width: 44, height: 44) + .clipShape(Circle()) + .overlay(Circle().stroke( + isCurrentUser + ? .pokit(.border(.brand)) + : .pokit(.border(.secondary)), + lineWidth: 1 + )) + } + } else { + Image(.image(.profile)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 44, height: 44) + .overlay(Circle().stroke( + isCurrentUser + ? .pokit(.border(.brand)) + : .pokit(.border(.secondary)), + lineWidth: 1 + )) + } + + Text(participant.nickname) + .pokitFont(.b1(.m)) + .foregroundStyle(.pokit(.text(.secondary))) + + Spacer() + + if isOwner && !isCurrentUser { + // 소유자 라벨 (비활성화 스타일) + PokitTextButton( + "소유자", + state: .default(.secondary), + size: .medium, + shape: .rectangle, + action: { } + ) + .disabled(true) + } else if isCreator && !isCurrentUser { + // 내보내기 버튼 (소유자가 다른 참여자 내보낼 때) + PokitTextButton( + "내보내기", + state: .stroke(.secondary), + size: .medium, + shape: .rectangle, + action: { delegateSend?(.removeParticipant(participant)) } + ) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + } +} + +@available(iOS 18.0, *) +#Preview { + @Previewable + @State var isPresented: Bool = true + + ZStack { + Color.green.ignoresSafeArea() + } + .sheet(isPresented: $isPresented) { + PokitParticipantsBottomSheet( + title: "포킷 공유 유저", + participants: [ + .init(id: 1, nickname: "Pokitmons", profile: nil), + .init(id: 2, nickname: "name1", profile: nil), + .init(id: 3, nickname: "name2", profile: nil) + ], + isCreator: true, + currentUserId: 1, + creatorUserId: 2, + delegateSend: { _ in } + ) + } +} diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift index a3b8cca2..1ebb613d 100644 --- a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift +++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift @@ -17,18 +17,21 @@ public struct ContentCardView: View { private let type: PokitLinkCard.CardType private let isFirst: Bool private let isLast: Bool - + private let showKebab: Bool + /// - Initializer public init( store: StoreOf, type: PokitLinkCard.CardType = .accept, isFirst: Bool = false, - isLast: Bool = false + isLast: Bool = false, + showKebab: Bool = true ) { self.store = store self.type = type self.isFirst = isFirst self.isLast = isLast + self.showKebab = showKebab } } //MARK: - View @@ -42,10 +45,19 @@ public extension ContentCardView { : isLast ? .bottom : .middle, type: type, action: { send(.컨텐츠_항목_눌렀을때) }, - kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때) }, + kebabAction: showKebab ? { send(.컨텐츠_항목_케밥_버튼_눌렀을때) } : nil, fetchMetaData: { send(.메타데이터_조회) }, favoriteAction: { send(.즐겨찾기_버튼_눌렀을때) } ) + .listRowInsets(EdgeInsets( + top: 0, + leading: 20, + bottom: isLast ? 36 : 0, + trailing: 20 + )) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .id(store.content.id) } } } diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift index 7b0bc337..7953d197 100644 --- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift +++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift @@ -111,17 +111,16 @@ public struct ContentSettingFeature { case URL_유효성_확인 case 링크복사_반영(String?) case 컨텐츠_상세_조회_API_반영(content: BaseContentDetail) - case 카테고리_상세_조회_API_반영(category: BaseCategory) case 카테고리_목록_조회_API_반영(categoryList: BaseCategoryListInquiry) case 선택한_포킷_인메모리_삭제 case 링크팝업_활성화(PokitLinkPopup.PopupType) case error(Error) case 키보드_감지_반영(Bool) + case 선택_카테고리_반영(BaseCategoryItem?) } public enum AsyncAction: Equatable { case 컨텐츠_상세_조회_API(id: Int) - case 카테고리_상세_조회_API(id: Int?, sharedId: Int?) case 카테고리_목록_조회_API case 컨텐츠_수정_API case 컨텐츠_추가_API @@ -303,55 +302,24 @@ private extension ContentSettingFeature { state.domain.memo = content.memo state.domain.alertYn = content.alertYn state.contentLoading = false - let id = content.category.categoryId - - return .merge( - .send(.inner(.URL_유효성_확인)), - .send(.async(.카테고리_상세_조회_API(id: id, sharedId: state.categoryId))) - ) - case .카테고리_상세_조회_API_반영(category: let category): - state.selectedPokit = BaseCategoryItem( - id: category.categoryId, - userId: 0, - categoryName: category.categoryName, - categoryImage: category.categoryImage, - contentCount: 0, - createdAt: "", - //TODO: v2 property 수정 - openType: .비공개, - keywordType: .default, - userCount: 0, - isFavorite: false - ) - return .none + return .send(.inner(.URL_유효성_확인)) case .카테고리_목록_조회_API_반영(categoryList: let categoryList): - /// - `카테고리_목록_조회`의 filter 옵션을 `false`로 해두었기 때문에 `미분류` 카테고리 또한 항목에서 조회가 가능함 - - /// [1]. `미분류`에 해당하는 인덱스 번호와 항목을 체크, 없다면 목록갱신이 불가함 - guard - let unclassifiedItemIdx = categoryList.data?.firstIndex(where: { - $0.categoryName == Constants.미분류 - }) - else { return .none } - guard - let unclassifiedItem = categoryList.data?.first(where: { - $0.categoryName == Constants.미분류 - }) - else { return .none } - - /// [2]. 새로운 list변수를 만들어주고 카테고리 항목 순서를 재배치 (최신순 정렬 시 미분류는 항상 맨 마지막) - var list = categoryList - list.data?.remove(at: unclassifiedItemIdx) - list.data?.insert(unclassifiedItem, at: 0) - /// [3]. 도메인 항목 리스트에 list 할당 - state.domain.categoryListInQuiry = list + state.domain.categoryListInQuiry = categoryList /// [4]. 최초 진입시: `미분류`로 설정함. 포킷 추가하고 왔다면 `@Shared`에 값이 있기 때문에 기존 값을 업데이트함 if state.selectedPokit == nil { - state.selectedPokit = unclassifiedItem + state.selectedPokit = categoryList.data?.first + } + return .run { [ + id = state.domain.categoryId, + sharedId = state.categoryId + ] send in + let selectedCategory = categoryList.data?.first { category in + return category.id == id || category.id == sharedId + } + await send(.inner(.선택_카테고리_반영(selectedCategory))) } - return .none case .선택한_포킷_인메모리_삭제: state.selectedPokit = nil return .none @@ -368,6 +336,9 @@ private extension ContentSettingFeature { case let .키보드_감지_반영(response): state.isKeyboardVisible = response return .none + case let .선택_카테고리_반영(selectedCategory): + state.selectedPokit = selectedCategory + return .none } } @@ -380,28 +351,13 @@ private extension ContentSettingFeature { let content = try await contentClient.컨텐츠_상세_조회("\(id)").toDomain() await send(.inner(.컨텐츠_상세_조회_API_반영(content: content)), animation: .pokitDissolve) } - case let .카테고리_상세_조회_API(id, sharedId): - return .run { send in - if let sharedId { - let category = try await categoryClient.카테고리_상세_조회("\(sharedId)").toDomain() - await send(.inner(.카테고리_상세_조회_API_반영(category: category))) - } else if let id { - let category = try await categoryClient.카테고리_상세_조회("\(id)").toDomain() - await send(.inner(.카테고리_상세_조회_API_반영(category: category))) - } - } case .카테고리_목록_조회_API: let request = BasePageableRequest( page: state.domain.pageable.page, size: 30, sort: state.domain.pageable.sort ) - let id = state.domain.categoryId - let sharedId = state.categoryId - return .merge( - .send(.async(.카테고리_상세_조회_API(id: id, sharedId: sharedId))), - categoryListFetch(request: request) - ) + return categoryListFetch(request: request) case .컨텐츠_수정_API: guard let contentId = state.domain.contentId, @@ -484,7 +440,24 @@ private extension ContentSettingFeature { func categoryListFetch(request: BasePageableRequest) -> Effect { return .run { send in let categoryList = try await categoryClient.카테고리_목록_조회(request, false, true).toDomain() - await send(.inner(.카테고리_목록_조회_API_반영(categoryList: categoryList)), animation: .pokitDissolve) + /// - `카테고리_목록_조회`의 filter 옵션을 `false`로 해두었기 때문에 `미분류` 카테고리 또한 항목에서 조회가 가능함 + + /// [1]. `미분류`에 해당하는 인덱스 번호와 항목을 체크, 없다면 목록갱신이 불가함 + guard let unclassifiedItemIdx = categoryList.data?.firstIndex(where: { + $0.categoryName == Constants.미분류 + }) + else { return } + guard let unclassifiedItem = categoryList.data?.first(where: { + $0.categoryName == Constants.미분류 + }) + else { return } + + /// [2]. 새로운 list변수를 만들어주고 카테고리 항목 순서를 재배치 (최신순 정렬 시 미분류는 항상 맨 마지막) + var list = categoryList + list.data?.remove(at: unclassifiedItemIdx) + list.data?.insert(unclassifiedItem, at: 0) + + await send(.inner(.카테고리_목록_조회_API_반영(categoryList: list)), animation: .pokitDissolve) } } } diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift index 7a754755..58eb9a91 100644 --- a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift +++ b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift @@ -479,6 +479,7 @@ private extension PokitRootFeature { } kakaoShareClient.카테고리_카카오톡_공유( CategoryKaKaoShareModel( + shareType: .공유, categoryName: selectedItem.categoryName, categoryId: selectedItem.id, imageURL: selectedItem.categoryImage.imageURL From 979dedbdac3241bc1e74e4e9f58dfc13ac912047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:23:23 +0900 Subject: [PATCH 06/15] =?UTF-8?q?[fix]=20#211=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/CategoryDetailFeature.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 52715ddc..79c51187 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -292,9 +292,19 @@ private extension CategoryDetailFeature { state.currentUserId = userId } - /// 단순 조회 액션들의 나열이기 때문에 merge로 우선 처리 + /// 데이터가 있으면 페이징 재조회, 없으면 초기 조회 + let contentListEffect: Effect = { + guard let _ = state.domain.contentList.data?.count else { + return .concatenate( + .send(.inner(.pagenation_초기화)), + .send(.async(.카테고리_내_컨텐츠_목록_조회_API)) + ) + } + return .send(.async(.페이징_재조회), animation: .pokitSpring) + }() + return .merge( - .send(.async(.카테고리_내_컨텐츠_목록_조회_API)), + contentListEffect, .send(.async(.카테고리_목록_조회_API)), .send(.async(.포킷_초대된_유저_목록_조회_API)), .send(.async(.클립보드_감지)) From 4c2ba6d8f46829e099fe7f6cb79e0e2aeee07eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:31:46 +0900 Subject: [PATCH 07/15] =?UTF-8?q?[feat]=20#211=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/DSKit/Sources/Components/PokitBottomSheet.swift | 9 ++++++++- .../Sources/CategoryDetailFeature.swift | 8 +++++++- .../Sources/CategoryDetailView.swift | 6 +++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Projects/DSKit/Sources/Components/PokitBottomSheet.swift b/Projects/DSKit/Sources/Components/PokitBottomSheet.swift index 08499532..10d1ed6a 100644 --- a/Projects/DSKit/Sources/Components/PokitBottomSheet.swift +++ b/Projects/DSKit/Sources/Components/PokitBottomSheet.swift @@ -87,6 +87,9 @@ public struct PokitBottomSheet: View { case .share: delegateSend?(.shareCellButtonTapped) return + case .pokitSetting: + delegateSend?(.pokitSettingCellButtonTapped) + return case .edit: delegateSend?(.editCellButtonTapped) return @@ -104,6 +107,7 @@ public extension PokitBottomSheet { enum Item: CaseIterable { case favorite case share + case pokitSetting case edit case delete case leave @@ -112,6 +116,7 @@ public extension PokitBottomSheet { switch self { case .favorite: return "즐겨찾기" case .share: return "공유하기" + case .pokitSetting: return "포킷 설정하기" case .edit: return "수정하기" case .delete: return "삭제하기" case .leave: return "나가기" @@ -122,9 +127,10 @@ public extension PokitBottomSheet { switch self { case .favorite: return .icon(.star) case .share: return .icon(.share) + case .pokitSetting: return .icon(.setup) case .edit: return .icon(.edit) case .delete: return .icon(.trash) - case .leave: return .icon(.arrowRight) + case .leave: return .icon(.trash) } } } @@ -132,6 +138,7 @@ public extension PokitBottomSheet { enum Delegate { case favoriteCellButtonTapped case shareCellButtonTapped + case pokitSettingCellButtonTapped case editCellButtonTapped case deleteCellButtonTapped case leaveCellButtonTapped diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 79c51187..1a2109cd 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -650,7 +650,7 @@ private extension CategoryDetailFeature { /// - Scope Effect func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect { switch action { - /// - 카테고리에 대한 `공유` / `수정` / `삭제` / `나가기` Delegate + /// - 카테고리에 대한 `공유` / `포킷 설정` / `삭제` / `나가기` Delegate case .categoryBottomSheet(let delegateAction): switch delegateAction { case .shareCellButtonTapped: @@ -659,6 +659,12 @@ private extension CategoryDetailFeature { await send(.inner(.카카오톡_공유(.공유))) } + case .pokitSettingCellButtonTapped: + return .run { [category = state.category] send in + await send(.inner(.카테고리_시트_활성화(false))) + await send(.delegate(.포킷수정(category))) + } + case .editCellButtonTapped: return .run { [category = state.category] send in await send(.inner(.카테고리_시트_활성화(false))) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift index d197df78..7e1324cf 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift @@ -72,12 +72,12 @@ public extension CategoryDetailView { .sheet(isPresented: $store.isCategorySheetPresented) { let items: [PokitBottomSheet.Item] = { if store.isSharedCategory { - return [.edit, .share, .leave] + return [.pokitSetting, .share, .leave] } else { - return [.edit, .share, .delete] + return [.pokitSetting, .share, .delete] } }() - + PokitBottomSheet( items: items, delegateSend: { store.send(.scope(.categoryBottomSheet($0))) } From dd7081859ee467c1c47c3349953db7896f0d9258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 12:47:45 +0900 Subject: [PATCH 08/15] =?UTF-8?q?[feat]=20#211=20=ED=8F=AC=ED=82=B7=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20-=20=EC=B0=B8=EC=97=AC=EC=9E=90,=20?= =?UTF-8?q?=EC=86=8C=EC=9C=A0=EC=9E=90=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/PokitCategorySettingFeature.swift | 37 ++++++-- .../Sources/PokitCategorySettingView.swift | 89 ++++++++++++------- 2 files changed, 87 insertions(+), 39 deletions(-) diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift index 925d9241..3774cef2 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift @@ -27,13 +27,15 @@ public struct PokitCategorySettingFeature { var keyboardClient @Dependency(UserNotificationClient.self) var userNotificationClient + @Dependency(UserDefaultsClient.self) + var userDefaultsClient @Dependency(\.openSettings) var openSetting /// - State @ObservableState public struct State: Equatable { fileprivate var domain: PokitCategorySetting - + var selectedProfile: BaseCategoryImage? { get { domain.categoryImage } set { domain.categoryImage = newValue } @@ -45,20 +47,25 @@ public struct PokitCategorySettingFeature { var profileImages: [BaseCategoryImage] { get { domain.imageList } } - + var selectedKeywordType: BaseInterestType { get { domain.keywordType } set { domain.keywordType = newValue } } - + var isPublicType: Bool { get { domain.openType == .공개 ? true : false } set { domain.openType = newValue ? .공개 : .비공개 } } var saveButtonEnabled: Bool { - !categoryName.isEmpty - && selectedProfile != nil - && (domain.openType == .공개 ? keywordSelectType != .normal : true) + // 참여자는 항상 활성화 (알림 설정만 변경) + if isParticipant { + return true + } + // 소유자는 기존 로직 + return !categoryName.isEmpty + && selectedProfile != nil + && (domain.openType == .공개 ? keywordSelectType != .normal : true) } var isCoEditing: Bool { let userCount = domain.userCount ?? 0 @@ -67,6 +74,17 @@ public struct PokitCategorySettingFeature { var alertEnable: Bool { isNotificationAuthorization && isAlert } + + /// 소유권 검증 + var categoryUserId: Int? + var currentUserId: Int? + var isOwner: Bool { + guard let currentUserId, let categoryUserId else { return true } + return categoryUserId == currentUserId + } + var isParticipant: Bool { + !isOwner && isCoEditing + } let type: SettingType var keywordSelectType: KeywordSelectType = .normal @@ -96,6 +114,7 @@ public struct PokitCategorySettingFeature { keywordType: category?.keywordType, userCount: category?.userCount ) + self.categoryUserId = category?.userId } } @@ -275,6 +294,12 @@ private extension PokitCategorySettingFeature { } case .뷰가_나타났을때: + /// 현재 로그인한 사용자 ID 가져오기 + if let userIdString = userDefaultsClient.stringKey(.userId), + let userId = Int(userIdString) { + state.currentUserId = userId + } + let selectType = state.selectedKeywordType if selectType != .default { state.keywordSelectType = .select(keywordName: selectType.title) diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift index 5f2d4f03..239ebeb9 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift @@ -37,11 +37,18 @@ public extension PokitCategorySettingView { PokitDivider() .padding(.horizontal, -20) .padding(.top, 28) - - if store.isCoEditing { alarmSettingSection } - - openTypeSettingSection - keywordSection + + // 공동 편집 중이면 알림 받기 표시 (소유자, 참여자 모두) + if store.isCoEditing { + alarmSettingSection + } + + // 소유자만 전체 공개 설정과 키워드 표시 + if store.isOwner { + openTypeSettingSection + keywordSection + } + Spacer() } .padding(.top, 16) @@ -81,8 +88,17 @@ public extension PokitCategorySettingView { } //MARK: - Configure View private extension PokitCategorySettingView { + @ViewBuilder var navigationBar: some View { - PokitHeader(title: store.type.title) { + let title: String = { + // 참여자는 "포킷 설정", 소유자는 기존 타이틀 + if store.isParticipant { + return "포킷 설정" + } + return store.type.title + }() + + PokitHeader(title: title) { PokitHeaderItems(placement: .leading) { PokitToolbarButton(.icon(.arrowLeft)) { send(.dismiss) @@ -122,30 +138,33 @@ private extension PokitCategorySettingView { } .frame(width: 80, height: 80) .overlay(alignment: .bottomTrailing) { - Circle() - .stroke( - .pokit(.icon(.tertiary)), - lineWidth: 1 - ) - .frame(width: 24, height: 24) - .background { - ZStack { - Circle() - .foregroundStyle( - .pokit(.icon(.inverseWh)) - ) - Button(action: { send(.프로필_설정_버튼_눌렀을때) }) { - Image(.icon(.edit)) - .resizable() - .frame(width: 18, height: 18) - .foregroundStyle(.pokit(.icon(.tertiary))) - .padding(2) + // 소유자만 프로필 편집 버튼 표시 + if store.isOwner { + Circle() + .stroke( + .pokit(.icon(.tertiary)), + lineWidth: 1 + ) + .frame(width: 24, height: 24) + .background { + ZStack { + Circle() + .foregroundStyle( + .pokit(.icon(.inverseWh)) + ) + Button(action: { send(.프로필_설정_버튼_눌렀을때) }) { + Image(.icon(.edit)) + .resizable() + .frame(width: 18, height: 18) + .foregroundStyle(.pokit(.icon(.tertiary))) + .padding(2) + } } + } - - } - .offset(x: 10) - .padding(.bottom, 3) + .offset(x: 10) + .padding(.bottom, 3) + } } } /// 타이틀 + 텍스트필드를 포함한 포킷명 입력 섹션 @@ -154,20 +173,24 @@ private extension PokitCategorySettingView { Text("포킷명") .pokitFont(.b2(.m)) .foregroundStyle(.pokit(.text(.secondary))) - + PokitTextInput( text: $store.categoryName, - type: store.categoryName.isEmpty ? .text : .iconR( + type: store.isOwner && !store.categoryName.isEmpty ? .iconR( icon: .icon(.x), action: { send(.포킷명지우기_버튼_눌렀을때) } - ), + ) : .text, shape: .rectangle, - state: $store.pokitNameTextInpuState, - placeholder: "포킷명을 입력해주세요.", + state: store.isParticipant + ? .constant(.disable) + : $store.pokitNameTextInpuState, + placeholder: store.isOwner ? "포킷명을 입력해주세요." : "", + info: store.isParticipant ? "수정 권한이 없습니다." : nil, maxLetter: 10, focusState: $isFocused, equals: true ) + .disabled(!store.isOwner) } } From a720fca9e9bbd00f5084141946bff6e8785d1593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 13:43:34 +0900 Subject: [PATCH 09/15] =?UTF-8?q?[feat]=20#211=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EB=82=B4=20=EC=BB=A8=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/CategoryDetailFeature.swift | 7 +++++++ .../FeatureCategoryDetail/Sources/CategoryDetailView.swift | 1 + 2 files changed, 8 insertions(+) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index 1a2109cd..cba5ffe4 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -110,6 +110,7 @@ public struct CategoryDetailFeature { case binding(BindingAction) case dismiss case pagenation + case 새로고침 /// 즐겨찾기 or 안읽음 버튼 눌렀을 때 case 분류_버튼_눌렀을때(SortCollectType) @@ -322,6 +323,12 @@ private extension CategoryDetailFeature { case .저장하기_버튼_눌렀을때: return .send(.async(.공유받은_포킷_저장_API)) + + case .새로고침: + return .concatenate( + .send(.inner(.pagenation_초기화), animation: .pokitDissolve), + .send(.async(.카테고리_내_컨텐츠_목록_조회_API)) + ) } } diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift index 7e1324cf..03961fe0 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift @@ -50,6 +50,7 @@ public extension CategoryDetailView { } .listStyle(.plain) .listRowSpacing(0) + .refreshable { await send(.새로고침).finish() } .background { scrollObservableView } .onPreferenceChange(ScrollOffsetKey.self) { if $0 != targetOffset { From 4d7fc93610a7b1f2b82dd22738eb08cdd353bd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 14:07:02 +0900 Subject: [PATCH 10/15] =?UTF-8?q?[refactor]=20#211=20ScrollView=20+=20Lazy?= =?UTF-8?q?VStack=20->=20List=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeaturePokit/Sources/PokitRootView.swift | 47 ++++++++++--------- .../Sources/Recommend/RecommendView.swift | 36 +++++++++----- .../Sources/Search/PokitSearchView.swift | 43 ++++++++--------- 3 files changed, 72 insertions(+), 54 deletions(-) diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift index 91e46d4f..379a16b7 100644 --- a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift +++ b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift @@ -32,9 +32,10 @@ public extension PokitRootView { WithPerceptionTracking { VStack(spacing: 0) { self.filterHeader + .padding(.horizontal, 20) + self.cardScrollView } - .padding(.horizontal, 20) .padding(.vertical, 16) .background(.pokit(.bg(.base))) .ignoresSafeArea(edges: .bottom) @@ -122,6 +123,7 @@ private extension PokitRootView { if store.folderType == .folder(.포킷) { pokitView .padding(.top, 20) + .padding(.horizontal, 20) } else { unclassifiedView } @@ -184,6 +186,7 @@ private extension PokitRootView { } else { unclassifiedList .padding(.top, 20) + .padding(.bottom, 74) } } else { PokitLoading() @@ -191,29 +194,31 @@ private extension PokitRootView { } var unclassifiedList: some View { - ScrollView { - LazyVStack(spacing: 0) { - ForEach( - Array(store.scope(state: \.contents, action: \.contents)) - ) { store in - let isFirst = store.state.id == self.store.contents.first?.id - let isLast = store.state.id == self.store.contents.last?.id - - ContentCardView( - store: store, - type: .linkList, - isFirst: isFirst, - isLast: isLast - ) - } + List { + ForEach( + Array(store.scope(state: \.contents, action: \.contents)) + ) { store in + let isFirst = store.state.id == self.store.contents.first?.id + let isLast = store.state.id == self.store.contents.last?.id + + ContentCardView( + store: store, + type: .linkList, + isFirst: isFirst, + isLast: isLast + ) + } - if store.unclassifiedHasNext { - PokitLoading() - .onAppear(perform: { send(.페이지_로딩중일때) }) - } + if store.unclassifiedHasNext { + PokitLoading() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) + .onAppear(perform: { send(.페이지_로딩중일때) }) } - .padding(.bottom, 150) } + .listStyle(.plain) + .listRowSpacing(0) } } diff --git a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift index 19a13ddc..4aa748b8 100644 --- a/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift +++ b/Projects/Feature/FeatureRecommend/Sources/Recommend/RecommendView.swift @@ -177,21 +177,33 @@ private extension RecommendView { func listContent( _ recommendedList: IdentifiedArrayOf ) -> some View { - ScrollView { - LazyVStack(spacing: 8) { - ForEach(recommendedList) { content in - recommendedCard(content) - } + List { + ForEach(recommendedList) { content in + let isFirst = recommendedList.first == content + let isLast = recommendedList.last == content - if store.hasNext { - PokitLoading() - .task { await send(.pagination).finish() } - } + recommendedCard(content) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets( + top: isFirst ? 12 : 0, + leading: 20, + bottom: !store.hasNext && isLast ? 150 : 0, + trailing: 20 + )) + .listRowSeparator(.hidden) + .id(content.id) + } + + if store.hasNext { + PokitLoading() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) + .task { await send(.pagination).finish() } } - .padding(.horizontal, 20) - .padding(.bottom, 150) - .padding(.top, 12) } + .listStyle(.plain) + .listRowSpacing(8) } @ViewBuilder diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift index 2225fda2..a6362084 100644 --- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift +++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift @@ -289,30 +289,31 @@ private extension PokitSearchView { } var resultListContent: some View { - ScrollView { - LazyVStack(spacing: 0) { - ForEach( - Array(store.scope(state: \.contents, action: \.contents)) - ) { store in - let isFirst = store.state.id == self.store.contents.first?.id - let isLast = store.state.id == self.store.contents.last?.id - - ContentCardView( - store: store, - type: .linkList, - isFirst: isFirst, - isLast: isLast - ) - } + List { + ForEach( + Array(store.scope(state: \.contents, action: \.contents)) + ) { store in + let isFirst = store.state.id == self.store.contents.first?.id + let isLast = store.state.id == self.store.contents.last?.id - if store.hasNext { - PokitLoading() - .task { await send(.로딩중일때, animation: .pokitDissolve).finish() } - } + ContentCardView( + store: store, + type: .linkList, + isFirst: isFirst, + isLast: isLast + ) + } + + if store.hasNext { + PokitLoading() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(.zero)) + .listRowSeparator(.hidden) + .task { await send(.로딩중일때, animation: .pokitDissolve).finish() } } - .padding(.horizontal, 20) - .padding(.bottom, 36) } + .listStyle(.plain) + .listRowSpacing(0) } var resultEmptyLabel: some View { From 4f2c0c96bc4609197404468cf5797049ab4fd1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 14:17:36 +0900 Subject: [PATCH 11/15] =?UTF-8?q?[chore]=20#211=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Category/CategoryListInquiryResponse+Extention.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Domain/Sources/DTO/Category/CategoryListInquiryResponse+Extention.swift b/Projects/Domain/Sources/DTO/Category/CategoryListInquiryResponse+Extention.swift index 1c776617..d4745561 100644 --- a/Projects/Domain/Sources/DTO/Category/CategoryListInquiryResponse+Extention.swift +++ b/Projects/Domain/Sources/DTO/Category/CategoryListInquiryResponse+Extention.swift @@ -33,7 +33,7 @@ public extension CategoryItemInquiryResponse { createdAt: self.createdAt, openType: BaseOpenType(rawValue: self.openType) ?? .비공개, keywordType: BaseInterestType(rawValue: self.keywordType.slashConvertUnderBar) ?? .default, - userCount: self.userCount, + userCount: self.userCount + 1, isFavorite: self.isFavorite ) } From ece06013db912a729d9302490a055d639e5c9741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 14:18:03 +0900 Subject: [PATCH 12/15] =?UTF-8?q?[fix]=20#211=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=B0=B8=EC=97=AC=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeatureCategoryDetail/Sources/CategoryDetailFeature.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift index cba5ffe4..5dd3f881 100644 --- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift +++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift @@ -280,6 +280,7 @@ private extension CategoryDetailFeature { return .run { send in await send(.inner(.pagenation_초기화), animation: .pokitDissolve) await send(.async(.카테고리_내_컨텐츠_목록_조회_API)) + await send(.async(.포킷_초대된_유저_목록_조회_API)) await send(.inner(.카테고리_선택_시트_활성화(false))) } From 16795e5caf7d9c3f9c1ab629201631fb5107a93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 15:45:00 +0900 Subject: [PATCH 13/15] =?UTF-8?q?[feat]=20#211=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=ED=95=A8=20=EC=95=88=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/PokitCaution.swift | 2 +- Projects/Domain/Sources/Alert/AlertItem.swift | 4 +-- .../Sources/Alert/PokitAlertBoxFeature.swift | 8 +++++- .../Sources/Alert/PokitAlertBoxView.swift | 26 +++++++++++++++---- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Projects/DSKit/Sources/Components/PokitCaution.swift b/Projects/DSKit/Sources/Components/PokitCaution.swift index 82baabdd..677b1c61 100644 --- a/Projects/DSKit/Sources/Components/PokitCaution.swift +++ b/Projects/DSKit/Sources/Components/PokitCaution.swift @@ -63,7 +63,7 @@ public enum CautionType { case .링크부족: return "링크를 5개 이상 저장하고 추천을 받아보세요" case .알림없음: - return "리마인드 알림을 설정하세요" + return "공동 편집 포킷을 만들어보세요" case .추천_링크없음: return "다른 사용자들이 링크를 저장하면\n추천해드릴게요" } diff --git a/Projects/Domain/Sources/Alert/AlertItem.swift b/Projects/Domain/Sources/Alert/AlertItem.swift index 114b62ca..cd66b1e7 100644 --- a/Projects/Domain/Sources/Alert/AlertItem.swift +++ b/Projects/Domain/Sources/Alert/AlertItem.swift @@ -15,12 +15,12 @@ public struct AlertItem: Identifiable, Equatable { public var title: String public var body: String public let createdAt: String - + public init( id: Int, userId: Int, contentId: Int, - thumbNail: String, + thumbNail: String, title: String, body: String, createdAt: String diff --git a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift index 5744b502..5480e35f 100644 --- a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift +++ b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift @@ -26,7 +26,9 @@ public struct PokitAlertBoxFeature { public init() {} fileprivate var domain = Alert() - + @Shared(.appStorage("lastAlertCheckDate")) + var lastAlertCheckDate: String? + var alertContents: IdentifiedArrayOf? { guard let list = domain.alertList.data else { return nil } var identifiedArray = IdentifiedArrayOf() @@ -134,6 +136,10 @@ private extension PokitAlertBoxFeature { switch action { case let .뷰가_나타났을때_알람_목록_조회_API_반영(list): state.domain.alertList = list + /// 가장 최신 알림의 날짜를 저장 (읽음 처리용) + if let latestAlert = list.data?.first { + state.lastAlertCheckDate = latestAlert.createdAt + } return .none case let .pagenation_알람_목록_조회_API_반영(alertList): diff --git a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift index d658ebca..de8d6fa5 100644 --- a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift +++ b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift @@ -37,11 +37,15 @@ public extension PokitAlertBoxView { List { ForEach(alertContents, id: \.id) { item in Button(action: { send(.알람_항목_선택했을때(item: item)) }) { - AlertContent(item: item) + AlertContent( + item: item, + lastCheckDate: store.lastAlertCheckDate + ) } .listRowSeparator(.hidden) .listRowInsets(EdgeInsets()) .onDelete(deleteAction: { delete(item) }) + .id(item.id) } .listRowBackground(Color.pokit(.bg(.base))) .padding(.top, 16) @@ -76,12 +80,19 @@ private extension PokitAlertBoxView { } struct AlertContent: View { - var item: AlertItem - - init(item: AlertItem) { + let item: AlertItem + let lastCheckDate: String? + + init(item: AlertItem, lastCheckDate: String?) { self.item = item + self.lastCheckDate = lastCheckDate + } + + private var isUnread: Bool { + guard let lastCheckDate else { return true } + return item.createdAt > lastCheckDate } - + var body: some View { VStack(alignment: .leading, spacing: 20) { HStack(spacing: 16) { @@ -117,6 +128,11 @@ private extension PokitAlertBoxView { .foregroundStyle(.pokit(.border(.tertiary))) } .padding(.top, 20) + .background( + isUnread + ? .pokit(.color(.orange(._50))) + : .clear + ) } } } From 0b72f8dec4069b6c56a607497a034fe0abdc1b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Fri, 26 Dec 2025 15:50:01 +0900 Subject: [PATCH 14/15] =?UTF-8?q?[feat]=20#211=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=83=81=EC=84=B8=20-=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EB=B2=84=ED=8A=BC=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/PokitCategorySettingView.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift index 239ebeb9..c8076c94 100644 --- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift +++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift @@ -239,13 +239,20 @@ private extension PokitCategorySettingView { if store.isPublicType { VStack(alignment: .leading, spacing: 4) { Button(action: { send(.키워드_바텀시트_활성화(true)) }) { - Text("포킷 키워드") - .pokitFont(.b1(.b)) - .foregroundStyle(.pokit(.text(.primary))) - - Spacer() - - Image(.icon(.arrowRight)) + HStack { + Text("포킷 키워드") + .pokitFont(.b1(.b)) + .foregroundStyle(.pokit(.text(.primary))) + + Spacer() + + Image(.icon(.arrowRight)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .foregroundStyle(.pokit(.icon(.primary))) + } + .contentShape(Rectangle()) } .buttonStyle(.plain) From db9d1c60ff79bf2c08dac4c355e4df70b581d216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=95?= Date: Mon, 2 Feb 2026 18:26:33 +0900 Subject: [PATCH 15/15] =?UTF-8?q?[chore]=20=EC=95=B1=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=202.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Resources/Pokit-info.plist | 2 +- Projects/App/ShareExtension/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Projects/App/Resources/Pokit-info.plist b/Projects/App/Resources/Pokit-info.plist index 02fec033..383c8b51 100644 --- a/Projects/App/Resources/Pokit-info.plist +++ b/Projects/App/Resources/Pokit-info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.0.2 + 2.1.0 CFBundleURLTypes diff --git a/Projects/App/ShareExtension/Info.plist b/Projects/App/ShareExtension/Info.plist index c2ade449..177581b1 100644 --- a/Projects/App/ShareExtension/Info.plist +++ b/Projects/App/ShareExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleName Pokit CFBundleShortVersionString - 2.0.1 + 2.1.0 CFBundleURLTypes