diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png index 4f5aed6e..0f986fc0 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png index ced1f64b..d68563b8 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png index 87880ff6..85cafb0c 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png index b3626693..96e50d08 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png index 454252ad..75f88b79 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png index 37c0ae6a..75565039 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png index 840a1681..1b517f22 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png index ae861d31..679cb8b1 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png index af1f6d20..133a2ce0 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png index 7dd62056..7d39e06f 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png index db7c2d3b..1a2f0179 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png index cd4d7dd6..113ecfac 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png index 268835cb..06c22b06 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png index 008be757..563c8004 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png index 3f7994a2..24d63f1a 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png index 5d8f94bb..f858b107 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png index 2a3e41a8..b2a5459c 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png index a7eddb63..6f1ac3c7 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png index bdc3f16a..d0da414f 100644 Binary files a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Projects/App/Resources/Pokit-info.plist b/Projects/App/Resources/Pokit-info.plist index e4aaeb2f..ec951b6a 100644 --- a/Projects/App/Resources/Pokit-info.plist +++ b/Projects/App/Resources/Pokit-info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.7 + 1.0.8 CFBundleURLTypes diff --git a/Projects/CoreKit/Sources/Data/DTO/Content/ContentDeleteRequest.swift b/Projects/CoreKit/Sources/Data/DTO/Content/ContentDeleteRequest.swift new file mode 100644 index 00000000..bc6c63b9 --- /dev/null +++ b/Projects/CoreKit/Sources/Data/DTO/Content/ContentDeleteRequest.swift @@ -0,0 +1,20 @@ +// +// ContentDeleteRequest.swift +// CoreKit +// +// Created by asobi on 12/30/24. +// + +import Foundation +/// 미분류 링크 삭제 +public struct ContentDeleteRequest: Encodable { + let contentId: [Int] + + public init(contentId: [Int]) { + self.contentId = contentId + } +} + +extension ContentDeleteRequest { + public static let mock: Self = Self(contentId: [551321312,4333333]) +} diff --git a/Projects/CoreKit/Sources/Data/DTO/Content/ContentMoveRequest.swift b/Projects/CoreKit/Sources/Data/DTO/Content/ContentMoveRequest.swift new file mode 100644 index 00000000..3bddab98 --- /dev/null +++ b/Projects/CoreKit/Sources/Data/DTO/Content/ContentMoveRequest.swift @@ -0,0 +1,21 @@ +// +// ContentMoveRequest.swift +// CoreKit +// +// Created by 김민호 on 12/29/24. +// +import Foundation +/// 미분류 링크를 카테고리로 이동 +public struct ContentMoveRequest: Encodable { + let contentIds: [Int] + let categoryId: Int + + public init(contentIds: [Int], categoryId: Int) { + self.contentIds = contentIds + self.categoryId = categoryId + } +} + +extension ContentMoveRequest { + public static let mock: Self = Self(contentIds: [123,456], categoryId: 444) +} diff --git a/Projects/CoreKit/Sources/Data/DTO/Version/VersionResponse.swift b/Projects/CoreKit/Sources/Data/DTO/Version/VersionResponse.swift index 017bedc1..ebfb8e27 100644 --- a/Projects/CoreKit/Sources/Data/DTO/Version/VersionResponse.swift +++ b/Projects/CoreKit/Sources/Data/DTO/Version/VersionResponse.swift @@ -8,9 +8,14 @@ import Foundation public struct VersionResponse: Decodable { - public let recentVersion: String + public let results: [VersionDTO] +} + +public struct VersionDTO: Decodable { + public let version: String + public let trackId: Int } extension VersionResponse { - static let mock: Self = Self(recentVersion: "1.0.0") + static let mock: Self = Self(results: [VersionDTO(version: "1.0.0", trackId: 2415354644)]) } diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift index ee6d1954..a506f1de 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift @@ -55,6 +55,12 @@ extension ContentClient: DependencyKey { try await provider.requestNoBody( .썸네일_수정(contentId: id, model: model) ) + }, + 미분류_링크_포킷_이동: { model in + try await provider.requestNoBody(.미분류_링크_포킷_이동(model: model)) + }, + 미분류_링크_삭제: { model in + try await provider.requestNoBody(.미분류_링크_삭제(model: model)) } ) }() diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift index ab5031b2..f9943f3e 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift @@ -18,7 +18,9 @@ extension ContentClient: TestDependencyKey { 카테고리_내_컨텐츠_목록_조회: { _, _, _ in .mock }, 미분류_카테고리_컨텐츠_조회: { _ in .mock }, 컨텐츠_검색: { _, _ in .mock }, - 썸네일_수정: { _, _ in } + 썸네일_수정: { _, _ in }, + 미분류_링크_포킷_이동: { _ in }, + 미분류_링크_삭제: { _ in } ) }() } diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift index 62de49d9..9c65dc5a 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift @@ -44,5 +44,11 @@ public struct ContentClient { _ contentId: String, _ model: ThumbnailRequest ) async throws -> Void + public var 미분류_링크_포킷_이동: @Sendable ( + _ model: ContentMoveRequest + ) async throws -> Void + public var 미분류_링크_삭제: @Sendable ( + _ model: ContentDeleteRequest + ) async throws -> Void } diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift index 0c1c5769..739aa56a 100644 --- a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift @@ -28,6 +28,8 @@ public enum ContentEndpoint { condition: BaseConditionRequest ) case 썸네일_수정(contentId: String, model: ThumbnailRequest) + case 미분류_링크_포킷_이동(model: ContentMoveRequest) + case 미분류_링크_삭제(model: ContentDeleteRequest) } extension ContentEndpoint: TargetType { @@ -57,13 +59,18 @@ extension ContentEndpoint: TargetType { return "" case let .썸네일_수정(contentId, _): return "/thumbnail/\(contentId)" + case .미분류_링크_포킷_이동: + return "" + case .미분류_링크_삭제: + return "/uncategorized" } } public var method: Moya.Method { switch self { case .컨텐츠_삭제, - .즐겨찾기_취소: + .즐겨찾기_취소, + .미분류_링크_삭제: return .put case .컨텐츠_상세_조회, @@ -72,7 +79,8 @@ extension ContentEndpoint: TargetType { return .post case .컨텐츠_수정, - .썸네일_수정: + .썸네일_수정, + .미분류_링크_포킷_이동: return .patch case .카태고리_내_컨텐츠_목록_조회, @@ -135,6 +143,12 @@ extension ContentEndpoint: TargetType { ) case let .썸네일_수정(_, model): return .requestJSONEncodable(model) + + case let .미분류_링크_포킷_이동(model): + return .requestJSONEncodable(model) + + case let .미분류_링크_삭제(model): + return .requestJSONEncodable(model) } } diff --git a/Projects/CoreKit/Sources/Data/Network/Version/VersionEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Version/VersionEndpoint.swift index 3b6fd55b..1f8a40bf 100644 --- a/Projects/CoreKit/Sources/Data/Network/Version/VersionEndpoint.swift +++ b/Projects/CoreKit/Sources/Data/Network/Version/VersionEndpoint.swift @@ -16,7 +16,9 @@ public enum VersionEndpoint { extension VersionEndpoint: TargetType { public var baseURL: URL { - return Constants.serverURL.appendingPathComponent(Constants.versionPath, conformingTo: .url) + let bundleID = Bundle.main.bundleIdentifier ?? "" + let countryCode = Locale.current.language.region?.identifier ?? "" + return URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleID)&country=\(countryCode)")! } public var path: String { diff --git a/Projects/DSKit/Sources/Components/PokitLinkCard.swift b/Projects/DSKit/Sources/Components/PokitLinkCard.swift index 7ff26664..826385cd 100644 --- a/Projects/DSKit/Sources/Components/PokitLinkCard.swift +++ b/Projects/DSKit/Sources/Components/PokitLinkCard.swift @@ -14,7 +14,7 @@ public struct PokitLinkCard: View { private let link: Item private let state: PokitLinkCard.State private let type: PokitLinkCard.CardType - private let action: () -> Void + private let action: (() -> Void)? private let kebabAction: (() -> Void)? private let fetchMetaData: (() -> Void)? private let favoriteAction: (() -> Void)? @@ -24,7 +24,7 @@ public struct PokitLinkCard: View { link: Item, state: PokitLinkCard.State, type: PokitLinkCard.CardType = .accept, - action: @escaping () -> Void, + action: (() -> Void)? = nil, kebabAction: (() -> Void)? = nil, fetchMetaData: (() -> Void)? = nil, favoriteAction: (() -> Void)? = nil, @@ -42,8 +42,14 @@ public struct PokitLinkCard: View { public var body: some View { VStack(spacing: 20) { - Button(action: action) { - buttonLabel + Group { + if let action { + Button(action: { action() }) { + buttonLabel + } + } else { + buttonLabel + } } .padding(.top, state == .top ? 0 : 20) diff --git a/Projects/DSKit/Sources/Components/PokitSelect.swift b/Projects/DSKit/Sources/Components/PokitSelect.swift index 2754573c..8da611e8 100644 --- a/Projects/DSKit/Sources/Components/PokitSelect.swift +++ b/Projects/DSKit/Sources/Components/PokitSelect.swift @@ -50,11 +50,18 @@ public struct PokitSelect: View { } .onChange(of: selectedItem) { onChangedSeletedItem($0) } .sheet(isPresented: $showSheet) { - listSheet - .presentationDragIndicator(.visible) - .pokitPresentationCornerRadius() - .presentationDetents([.height(564)]) - .pokitPresentationBackground() + PokitSelectSheet( + list: list, + itemSelected: { item in + listDismiss() + action(item) + }, + pokitAddAction: addAction + ) + .presentationDragIndicator(.visible) + .pokitPresentationCornerRadius() + .presentationDetents([.height(564)]) + .pokitPresentationBackground() } } @@ -96,63 +103,6 @@ public struct PokitSelect: View { .animation(.pokitDissolve, value: self.state) } - private var listSheet: some View { - Group { - if let list { - VStack(spacing: 0) { - if let addAction { - addButton { - listDismiss() - addAction() - } - } - - PokitList( - selectedItem: selectedItem, - list: list - ) { item in - action(item) - listCellTapped(item) - } - } - .padding(.top, 12) - .padding(.bottom, 20) - } else { - PokitLoading() - } - } - } - - @ViewBuilder - private func addButton( - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - HStack(spacing: 20) { - PokitIconButton( - .icon(.plusR), - state: .default(.secondary), - size: .medium, - shape: .round, - action: action - ) - - Text("포킷 추가하기") - .pokitFont(.b1(.b)) - .foregroundStyle(.pokit(.text(.primary))) - - Spacer() - } - .padding(.vertical, 22) - .padding(.horizontal, 30) - .background(alignment: .bottom) { - Rectangle() - .fill(.pokit(.border(.tertiary))) - .frame(height: 1) - } - } - } - private func partSelectButtonTapped() { showSheet = true } diff --git a/Projects/DSKit/Sources/Components/PokitSelectSheet.swift b/Projects/DSKit/Sources/Components/PokitSelectSheet.swift new file mode 100644 index 00000000..fff68ca0 --- /dev/null +++ b/Projects/DSKit/Sources/Components/PokitSelectSheet.swift @@ -0,0 +1,87 @@ +// +// PokitSelectSheet.swift +// DSKit +// +// Created by 김민호 on 12/27/24. +// + +import SwiftUI +import Util + + +public struct PokitSelectSheet: View { + @Binding + private var selectedItem: Item? + + private let list: [Item]? + private let itemSelected: (Item) -> Void + private let pokitAddAction: (() -> Void)? + + public init( + list: [Item]?, + selectedItem: Binding = .constant(nil), + itemSelected: @escaping (Item) -> Void, + pokitAddAction: (() -> Void)? + ) { + self.list = list + self._selectedItem = selectedItem + self.itemSelected = itemSelected + self.pokitAddAction = pokitAddAction + } + + + public var body: some View { + Group { + if let list { + VStack(spacing: 0) { + if let pokitAddAction { + addButton { + pokitAddAction() + } + } + PokitList( + selectedItem: selectedItem, + list: list + ) { item in + itemSelected(item) + } + } + .padding(.top, 12) + .padding(.bottom, 20) + } else { + PokitLoading() + } + } + } +} +extension PokitSelectSheet { + @ViewBuilder + private func addButton( + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + HStack(spacing: 20) { + PokitIconButton( + .icon(.plusR), + state: .default(.secondary), + size: .medium, + shape: .round, + action: action + ) + + Text("포킷 추가하기") + .pokitFont(.b1(.b)) + .foregroundStyle(.pokit(.text(.primary))) + + Spacer() + } + .padding(.vertical, 22) + .padding(.horizontal, 30) + .background(alignment: .bottom) { + Rectangle() + .fill(.pokit(.border(.tertiary))) + .frame(height: 1) + } + } + } +} diff --git a/Projects/DSKit/Sources/Modifiers/PokitNavigationBarModifier.swift b/Projects/DSKit/Sources/Modifiers/PokitNavigationBarModifier.swift index dc2d22a2..67606e7d 100644 --- a/Projects/DSKit/Sources/Modifiers/PokitNavigationBarModifier.swift +++ b/Projects/DSKit/Sources/Modifiers/PokitNavigationBarModifier.swift @@ -31,4 +31,8 @@ public extension View { func pokitNavigationBar(@ViewBuilder header: () -> Header) -> some View { modifier(PokitNavigationBarModifier(header: header)) } + + func pokitNavigationBar(_ header: @autoclosure () -> Header) -> some View { + modifier(PokitNavigationBarModifier(header: header)) + } } diff --git a/Projects/Domain/Sources/DTO/Version/VersionResponse+Extension.swift b/Projects/Domain/Sources/DTO/Version/VersionResponse+Extension.swift new file mode 100644 index 00000000..15483988 --- /dev/null +++ b/Projects/Domain/Sources/DTO/Version/VersionResponse+Extension.swift @@ -0,0 +1,19 @@ +// +// VersionResponse+Extension.swift +// Domain +// +// Created by 김민호 on 12/30/24. +// + +import Foundation + +import CoreKit + +public extension VersionResponse { + func toDomain() -> Version { + return .init( + self.results.first?.version ?? "", + trackId: self.results.first?.trackId ?? 0 + ) + } +} diff --git a/Projects/Domain/Sources/Version/Version.swift b/Projects/Domain/Sources/Version/Version.swift new file mode 100644 index 00000000..bb97b6fb --- /dev/null +++ b/Projects/Domain/Sources/Version/Version.swift @@ -0,0 +1,42 @@ +// +// Version.swift +// Domain +// +// Created by 김민호 on 12/30/24. +// + +import Foundation + +public struct Version: Comparable { + let major: Int + let minor: Int + let patch: Int + public let trackId: Int + + public init(_ version: String, trackId: Int) { + let components = version.split(separator: ".").compactMap { Int($0) } + self.major = components.count > 0 ? components[0] : 0 + self.minor = components.count > 1 ? components[1] : 0 + self.patch = components.count > 2 ? components[2] : 0 + self.trackId = trackId + } + + public static func < ( + lhs: Version, + rhs: Version + ) -> Bool { + if lhs.major != rhs.major { + return lhs.major < rhs.major + } + return lhs.minor < rhs.minor + } + + public static func == ( + lhs: Version, + rhs: Version + ) -> Bool { + return lhs.major == rhs.major && + lhs.minor == rhs.minor + } + +} diff --git a/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift index 4d8b4acd..3f3cdcdd 100644 --- a/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift +++ b/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift @@ -8,23 +8,30 @@ import Foundation import ComposableArchitecture import CoreKit +import Domain import Util +import UIKit @Reducer public struct SplashFeature { /// - Dependency @Dependency(\.continuousClock) var clock - @Dependency(UserDefaultsClient.self) + @Dependency(\.openURL) + var openURL + @Dependency(UserDefaultsClient.self) var userDefaults @Dependency(AuthClient.self) var authClient @Dependency(KeychainClient.self) var keychain + @Dependency(VersionClient.self) + var versionClient /// - State @ObservableState - public struct State: Equatable { + public struct State { @Shared(.appStorage("isNeedSessionDeleted")) var isNeedSessionDeleted: Bool = true + @Presents var alert: AlertState? public init() {} } /// - Action @@ -36,18 +43,26 @@ public struct SplashFeature { case delegate(DelegateAction) @CasePathable - public enum View: Equatable { + public enum View: BindableAction, Equatable { + case binding(BindingAction) case onAppear } public enum InnerAction: Equatable { case 키_제거 + case 앱스토어_알림_활성화(trackId: Int) + } + public enum AsyncAction: Equatable { case 없음 } + @CasePathable + public enum ScopeAction { + case alert(PresentationAction) } - public enum AsyncAction: Equatable { case doNothing } - public enum ScopeAction: Equatable { case doNothing } public enum DelegateAction: Equatable { case loginNeeded case autoLoginSuccess } + public enum Alert { + case 앱스토어_이동(trackId: Int) + } } /// initiallizer public init() {} @@ -73,7 +88,9 @@ public struct SplashFeature { } /// - Reducer body public var body: some ReducerOf { + BindingReducer(action: \.view) Reduce(self.core) + .ifLet(\.$alert, action: \.scope.alert) } } //MARK: - FeatureAction Effect @@ -81,9 +98,24 @@ private extension SplashFeature { /// - View Effect func handleViewAction(_ action: Action.View, state: inout State) -> Effect { switch action { + case .binding: + return .none + case .onAppear: return .run { [isNeedSessionDeleted = state.isNeedSessionDeleted] send in try await self.clock.sleep(for: .milliseconds(2000)) + /// Version Check + let response = try await versionClient.버전체크().toDomain() + guard + let info = Bundle.main.infoDictionary, + let currentVersion = info["CFBundleShortVersionString"] as? String else { return } + let appStoreVersion = response + let nowVersion = Version(currentVersion, trackId: response.trackId) + + if nowVersion < appStoreVersion { + await send(.inner(.앱스토어_알림_활성화(trackId: response.trackId))) + return + } if isNeedSessionDeleted { guard let platform = userDefaults.stringKey(.authPlatform) else { print("platform이 없어서 벗어남") @@ -161,6 +193,18 @@ private extension SplashFeature { await userDefaults.removeString(.authPlatform) await isNeedSessionDeleted.withLock { $0 = false } } + + case let .앱스토어_알림_활성화(trackId): + state.alert = .init(title: { + TextState("업데이트") + }, actions: { + ButtonState(role: .none, action: .앱스토어_이동(trackId: trackId)) { + TextState("앱스토어 이동") + } + }, message: { + TextState("최신버전의 포킷으로 업데이트가 필요합니다.") + }) + return .none } } /// - Async Effect @@ -169,7 +213,17 @@ private extension SplashFeature { } /// - Scope Effect func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect { - return .none + switch action { + case let .alert(.presented(.앱스토어_이동(trackId))): + return .run { _ in + if let url = URL(string: "https://apps.apple.com/app/id\(trackId)") { + await openURL.callAsFunction(url) + } + } + + case .alert: + return .none + } } /// - Delegate Effect func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect { diff --git a/Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift b/Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift index a4795fdd..28a16b2a 100644 --- a/Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift +++ b/Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift @@ -13,7 +13,8 @@ import ComposableArchitecture @ViewAction(for: SplashFeature.self) public struct SplashView: View { /// - Properties - public let store: StoreOf + @Perception.Bindable + public var store: StoreOf /// - Initializer public init(store: StoreOf) { self.store = store @@ -44,6 +45,7 @@ public extension SplashView { .pokit(.bg(.brand)) .ignoresSafeArea() } + .alert($store.scope(state: \.alert, action: \.scope.alert)) .onAppear { send(.onAppear) } } } diff --git a/Projects/Feature/FeaturePokit/Sources/PokitLinkEditFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitLinkEditFeature.swift new file mode 100644 index 00000000..23412a9d --- /dev/null +++ b/Projects/Feature/FeaturePokit/Sources/PokitLinkEditFeature.swift @@ -0,0 +1,315 @@ +// +// PokitLinkEditFeature.swift +// Feature +// +// Created by 김민호 on 12/24/24. + +import ComposableArchitecture +import CoreKit +import Domain +import DSKit +import FeatureCategorySetting +import Util + +@Reducer +public struct PokitLinkEditFeature { + /// - Dependency + @Dependency(\.dismiss) + var dismiss + @Dependency(CategoryClient.self) + var categoryClient + @Dependency(ContentClient.self) + var contentClient + /// - State + @ObservableState + public struct State: Equatable { + @Presents var addPokit: PokitCategorySettingFeature.State? + /// 링크 아이템 Doamin + var item: BaseContentListInquiry + /// 카테고리 아이템 Domain + var category: BaseCategoryListInquiry? + /// 링크 목록 + var list = IdentifiedArrayOf() + /// 선택한 링크 목록 + var selectedItems = IdentifiedArrayOf() + /// 포킷 이동 눌렀을 때 sheet + var categorySelectSheetPresetend: Bool = false + var linkDeleteSheetPresented: Bool = false + var linkPopup: PokitLinkPopup.PopupType? + + public init(linkList: BaseContentListInquiry) { + self.item = linkList + if let data = self.item.data { + data.forEach { list.append($0) } + } + } + } + + /// - Action + public enum Action: FeatureAction, ViewAction { + case view(View) + case inner(InnerAction) + case async(AsyncAction) + case scope(ScopeAction) + case delegate(DelegateAction) + + @CasePathable + public enum View: BindableAction, Equatable { + case binding(BindingAction) + case dismiss + + case 뷰가_나타났을때 + case 포킷_추가하기_버튼_눌렀을때 + case 링크팝업_버튼_눌렀을때 + case 경고시트_해제 + case 삭제확인_버튼_눌렀을때 + case 체크박스_선택했을때(BaseContentItem) + case 카테고리_선택했을때(BaseCategoryItem) + } + + public enum InnerAction { + case error(Error) + case 카테고리_이동_시트_활성화(Bool) + case 카테고리_삭제_시트_활성화(Bool) + case 경고팝업_활성화(PokitLinkPopup.PopupType) + case 카테고리_목록_조회_API_반영(BaseCategoryListInquiry) + case 미분류_API_반영(LinkEditType) + } + + public enum AsyncAction: Equatable { case 없음 } + + @CasePathable + public enum ScopeAction { + case floatButtonAction(PokitLinkEditFloatView.Delegate) + case addPokit(PresentationAction) + } + + public enum DelegateAction: Equatable { + case 링크_편집_종료(items: [BaseContentItem]) + } + } + + /// - Initiallizer + public init() {} + + /// - Reducer Core + private func core(into state: inout State, action: Action) -> Effect { + switch action { + /// - View + case .view(let viewAction): + return handleViewAction(viewAction, state: &state) + + /// - Inner + case .inner(let innerAction): + return handleInnerAction(innerAction, state: &state) + + /// - Async + case .async(let asyncAction): + return handleAsyncAction(asyncAction, state: &state) + + /// - Scope + case .scope(let scopeAction): + return handleScopeAction(scopeAction, state: &state) + + /// - Delegate + case .delegate(let delegateAction): + return handleDelegateAction(delegateAction, state: &state) + } + } + + /// - Reducer body + public var body: some ReducerOf { + BindingReducer(action: \.view) + Reduce(self.core) + .ifLet(\.$addPokit, action: \.scope.addPokit) { + PokitCategorySettingFeature() + } + } +} +//MARK: - FeatureAction Effect +private extension PokitLinkEditFeature { + /// - View Effect + func handleViewAction(_ action: Action.View, state: inout State) -> Effect { + switch action { + case .binding: + return .none + + case .dismiss: + return .send(.delegate(.링크_편집_종료(items: state.list.elements))) +// return .run { _ in await dismiss() } + + case .뷰가_나타났을때: + return fetchCateogryList() + + case .포킷_추가하기_버튼_눌렀을때: + state.categorySelectSheetPresetend = false + state.linkDeleteSheetPresented = false + state.addPokit = PokitCategorySettingFeature.State(type: .추가) + return .none + + case .경고시트_해제: + return .send(.inner(.카테고리_삭제_시트_활성화(false))) + + case .삭제확인_버튼_눌렀을때: + return linkDelete(state: &state) + + case let .체크박스_선택했을때(item): + /// 이미 체크되어 있다면 해제 + if state.selectedItems.contains(item) { + state.selectedItems.remove(id: item.id) + } else { + state.selectedItems.append(item) + } + return .none + + case let .카테고리_선택했을때(pokit): + /// 🚨 Error Case [1]: 체크한 것이 없는데 카테고리를 선택했을 때 + if state.selectedItems.isEmpty { + return .merge( + .send(.inner(.카테고리_이동_시트_활성화(false))), + .send(.inner(.경고팝업_활성화(.error(title: "링크를 선택해주세요.")))) + ) + } else { + return moveContentList(categoryId: pokit.id, state: &state) + } + + case .링크팝업_버튼_눌렀을때: + state.linkPopup = nil + return .none + } + } + + /// - Inner Effect + func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect { + switch action { + case let .error(error): + guard let errorResponse = error as? ErrorResponse else { return .none } + state.categorySelectSheetPresetend = false + state.linkDeleteSheetPresented = false + return .merge( + .send(.inner(.카테고리_이동_시트_활성화(false))), + .send(.inner(.카테고리_삭제_시트_활성화(false))), + .send(.inner(.경고팝업_활성화(.error(title: errorResponse.message))), + animation: .pokitSpring + ) + ) + + case let .경고팝업_활성화(type): + state.linkPopup = type + return .none + + case let .카테고리_이동_시트_활성화(isPresented): + state.categorySelectSheetPresetend = isPresented + return .none + + case let .카테고리_삭제_시트_활성화(isPresented): + state.linkDeleteSheetPresented = isPresented + return .none + + case let .카테고리_목록_조회_API_반영(response): + state.category = response + return .none + + case let .미분류_API_반영(type): + /// 1. 시트 내리기 + if type == .링크이동 { + state.categorySelectSheetPresetend = false + } else { + state.linkDeleteSheetPresented = false + } + /// 2. 선택했던 체크리스트 삭제 + state.selectedItems + .map { $0.id } + .forEach { state.list.remove(id: $0) } + state.selectedItems.removeAll() + + if state.list.isEmpty { + return .send(.delegate(.링크_편집_종료(items: []))) + } + return .none + } + } + + /// - Async Effect + func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect { + return .none + } + + /// - Scope Effect + func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect { + switch action { + case let .floatButtonAction(delegate): + switch delegate { + case .링크삭제_버튼_눌렀을때: + if state.selectedItems.isEmpty { + return .send(.inner(.경고팝업_활성화(.error(title: "링크를 선택해주세요.")))) + } else { + return .send(.inner(.카테고리_삭제_시트_활성화(true))) + } + + case .전체선택_버튼_눌렀을때: + state.selectedItems = state.list + return .none + + case .전체해제_버튼_눌렀을때: + state.selectedItems.removeAll() + return .none + + case .포킷이동_버튼_눌렀을때: + return .send(.inner(.카테고리_이동_시트_활성화(true))) + } + + case .addPokit(.presented(.delegate(.settingSuccess))): + state.addPokit = nil + return .merge( + fetchCateogryList(), + .send(.inner(.카테고리_이동_시트_활성화(true))) + ) + + case .addPokit: + return .none + } + } + + /// - Delegate Effect + func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect { + return .none + } + + /// 카테고리 목록 조회 API + func fetchCateogryList() -> Effect { + return .run { send in + let request: BasePageableRequest = BasePageableRequest(page: 0, size: 100, sort: ["createdAt", "desc"]) + let response = try await categoryClient.카테고리_목록_조회(request, false).toDomain() + await send(.inner(.카테고리_목록_조회_API_반영(response))) + } + } + + /// 미분류 링크 카테고리 이동 API + func moveContentList(categoryId: Int, state: inout State) -> Effect { + return .run { [contentIds = state.selectedItems] send in + let contentIds = contentIds.map { $0.id } + let request = ContentMoveRequest(contentIds: contentIds, categoryId: categoryId) + try await contentClient.미분류_링크_포킷_이동(request) + await send(.inner(.미분류_API_반영(.링크이동))) + } catch: { error, send in + await send(.inner(.error(error))) + } + } + + func linkDelete(state: inout State) -> Effect { + return .run { [contentIds = state.selectedItems.ids] send in + let request = ContentDeleteRequest(contentId: Array(contentIds)) + try await contentClient.미분류_링크_삭제(request) + await send(.inner(.미분류_API_반영(.링크삭제))) + } catch: { error, send in + await send(.inner(.error(error))) + } + } +} +public extension PokitLinkEditFeature { + enum LinkEditType: Equatable { + case 링크이동 + case 링크삭제 + } +} diff --git a/Projects/Feature/FeaturePokit/Sources/PokitLinkEditView.swift b/Projects/Feature/FeaturePokit/Sources/PokitLinkEditView.swift new file mode 100644 index 00000000..1472455f --- /dev/null +++ b/Projects/Feature/FeaturePokit/Sources/PokitLinkEditView.swift @@ -0,0 +1,148 @@ +// +// PokitLinkEditView.swift +// Feature +// +// Created by 김민호 on 12/24/24. + +import SwiftUI + +import ComposableArchitecture +import DSKit +import Domain +import CoreKit +import FeatureCategorySetting +import Util + +@ViewAction(for: PokitLinkEditFeature.self) +public struct PokitLinkEditView: View { + /// - Properties + @Perception.Bindable public var store: StoreOf + + /// - Initializer + public init(store: StoreOf) { + self.store = store + } +} +//MARK: - View +public extension PokitLinkEditView { + var body: some View { + WithPerceptionTracking { + VStack(spacing: 0) { + contentList + } + .padding(.top, 16) + .overlay(alignment: .bottom) { + if !store.list.isEmpty { + actionFloatButtonView + } + } + .padding(.horizontal, 20) + .padding(.bottom, 24) + .pokitNavigationBar(navigationBar) + .ignoresSafeArea(edges: .bottom) + .sheet(isPresented: $store.categorySelectSheetPresetend) { + PokitSelectSheet( + list: store.category?.data ?? nil, + itemSelected: { send(.카테고리_선택했을때($0)) }, + pokitAddAction: { send(.포킷_추가하기_버튼_눌렀을때) } + ) + .presentationDragIndicator(.visible) + .pokitPresentationCornerRadius() + .presentationDetents([.height(564)]) + .pokitPresentationBackground() + + } + .sheet(isPresented: $store.linkDeleteSheetPresented) { + PokitAlert( + "링크를 정말 삭제하시겠습니까?", + message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.", + confirmText: "삭제", + action: { send(.삭제확인_버튼_눌렀을때) }, + cancelAction: { send(.경고시트_해제) } + ) + } + .overlay(alignment: .bottom) { + if store.linkPopup != nil { + PokitLinkPopup( + type: $store.linkPopup, + action: { send(.링크팝업_버튼_눌렀을때, animation: .pokitSpring) } + ) + .pokitMaxWidth() + } + } + /// fullScreenCover를 통해 새로운 Destination을 만들었음 + /// 그렇지 않으면 MainPath에서 관리해야 하고, `LinkEdit`을 모듈로 빼야 함 + /// 추후 여러 군데에서 사용 된다면 그때 진행 + .fullScreenCover( + item: $store.scope( + state: \.addPokit, + action: \.scope.addPokit + ) + ) { store in + PokitCategorySettingView(store: store) + } + .task { await send(.뷰가_나타났을때).finish() } + } + } +} +//MARK: - Configure View +private extension PokitLinkEditView { + var navigationBar: some View { + PokitHeader(title: "링크 분류하기") { + PokitHeaderItems(placement: .leading) { + PokitToolbarButton(.icon(.x)) { + send(.dismiss) + } + } + } + .padding(.top, 8) + } + + var contentList: some View { + ScrollView { + ForEach(store.list, id: \.id) { item in + let isFirst = item.id == self.store.list.first?.id + let isLast = item.id == self.store.list.last?.id + PokitLinkCard( + link: item, + state: isFirst + ? .top + : isLast ? .bottom : .middle, + type: .unCatgorized(isSelected: store.selectedItems.contains(item)), + action: nil, + kebabAction: nil, + fetchMetaData: {}, + favoriteAction: nil, + selectAction: { send(.체크박스_선택했을때(item)) } + ) + } + } + .scrollIndicators(.hidden) + .padding(.bottom, 38) + } + + var actionFloatButtonView: some View { + PokitLinkEditFloatView( + delegateSend: { store.send(.scope(.floatButtonAction($0))) } + ) + } +} +//MARK: - Preview +#Preview { + PokitLinkEditView( + store: Store( + initialState: .init( + linkList: BaseContentListInquiry( + data: [BaseContentItem(id: 3, categoryName: "23", categoryId: 255, title: "2323", memo: nil, thumbNail: Constants.mockImageUrl, data: "", domain: "", createdAt: "", isRead: false, isFavorite: false)], + page: 0, + size: 0, + sort: [], + hasNext: false + ) + ), + reducer: { PokitLinkEditFeature() } + ) + ) +} + + diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift index ff05b68c..05bbd626 100644 --- a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift +++ b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift @@ -4,6 +4,8 @@ // // Created by 김민호 on 7/16/24. +import Foundation + import ComposableArchitecture import FeatureContentCard import Domain @@ -23,6 +25,7 @@ public struct PokitRootFeature { /// - State @ObservableState public struct State: Equatable { + @Presents var linkEdit: PokitLinkEditFeature.State? var folderType: PokitRootFilterType = .folder(.포킷) var sortType: PokitRootFilterType = .sort(.최신순) @@ -71,6 +74,7 @@ public struct PokitRootFeature { case 케밥_버튼_눌렀을때(BaseCategoryItem) case 포킷추가_버튼_눌렀을때 case 링크추가_버튼_눌렀을때 + case 편집하기_버튼_눌렀을때 case 카테고리_눌렀을때(BaseCategoryItem) case 컨텐츠_항목_눌렀을때(BaseContentItem) case 뷰가_나타났을때 @@ -83,6 +87,7 @@ public struct PokitRootFeature { case 카테고리_삭제_시트_활성화(Bool) case 미분류_카테고리_조회_API_반영(contentList: BaseContentListInquiry) + case 미분류_전쳬_링크_조회_API_반영(contentList: BaseContentListInquiry) case 미분류_카테고리_페이징_조회_API_반영(contentList: BaseContentListInquiry) case 미분류_카테고리_컨텐츠_삭제_API_반영(contentId: Int) @@ -99,14 +104,17 @@ public struct PokitRootFeature { case 카테고리_삭제_API(categoryId: Int) case 미분류_카테고리_조회_API + case 미분류_전쳬_링크_조회_API case 미분류_카테고리_페이징_조회_API case 미분류_카테고리_페이징_재조회_API } + @CasePathable public enum ScopeAction { case bottomSheet(PokitBottomSheet.Delegate) case deleteBottomSheet(PokitDeleteBottomSheet.Delegate) case contents(IdentifiedActionOf) + case linkEdit(PresentationAction) } public enum DelegateAction: Equatable { @@ -162,9 +170,8 @@ public struct PokitRootFeature { public var body: some ReducerOf { BindingReducer(action: \.view) Reduce(self.core) - .forEach(\.contents, action: \.contents) { - ContentCardFeature() - } + .forEach(\.contents, action: \.contents) { ContentCardFeature() } + .ifLet(\.$linkEdit, action: \.scope.linkEdit) { PokitLinkEditFeature() } } } @@ -197,7 +204,7 @@ private extension PokitRootFeature { return .send(.inner(.sort), animation: .pokitDissolve) case .folder(.미분류): - state.sortType = .sort(state.sortType == .sort(.오래된순) ? .최신순 : .오래된순) + state.sortType = .sort(.최신순) return .send(.inner(.sort), animation: .pokitDissolve) default: return .none @@ -212,6 +219,9 @@ private extension PokitRootFeature { case .링크추가_버튼_눌렀을때: return .run { send in await send(.delegate(.링크추가_버튼_눌렀을때)) } + + case .편집하기_버튼_눌렀을때: + return .run { send in await send(.async(.미분류_전쳬_링크_조회_API)) } case .카테고리_눌렀을때(let category): return .run { send in await send(.delegate(.categoryTapped(category))) } @@ -288,6 +298,10 @@ private extension PokitRootFeature { state.isLoading = false return .none + case let .미분류_전쳬_링크_조회_API_반영(contentList): + state.linkEdit = PokitLinkEditFeature.State(linkList: contentList) + return .none + case let .카테고리_조회_API_반영(categoryList): state.domain.categoryList = categoryList return .none @@ -373,6 +387,13 @@ private extension PokitRootFeature { await send(.inner(.미분류_카테고리_조회_API_반영(contentList: contentList)), animation: .pokitSpring) } + case .미분류_전쳬_링크_조회_API: + return .run { [pageable = state.domain.pageable] send in + let request = BasePageableRequest(page: 0, size: 100, sort: pageable.sort) + let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회(request).toDomain() + await send(.inner(.미분류_전쳬_링크_조회_API_반영(contentList: contentList))) + } + case .카테고리_조회_API: state.domain.pageable.page = 0 return .run { [pageable = state.domain.pageable] send in @@ -499,6 +520,29 @@ private extension PokitRootFeature { case .contents: return .none + case let .linkEdit(.presented(.delegate(.링크_편집_종료(list)))): + /// 링크가 비어있을때는 전부 삭제 + if list.isEmpty { + state.contents.removeAll() + } else { + /// 링크가 일부 있을 때 -> 그 일부를 붙여넣기 + var linkIds = IdentifiedArrayOf() + list.forEach { item in + state.contents.forEach { content in + if item.id == content.content.id { + linkIds.append(content) + } + } + } + state.contents.removeAll() + state.contents = linkIds + } + state.linkEdit = nil + return .none + + case .linkEdit: + return .none + default: return .none } } diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift index 66c4b556..498a48cd 100644 --- a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift +++ b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift @@ -54,6 +54,16 @@ public extension PokitRootView { delegateSend: { store.send(.scope(.deleteBottomSheet($0)), animation: .pokitSpring) } ) } + .fullScreenCover( + item: $store.scope( + state: \.linkEdit, + action: \.scope.linkEdit + ) + ) { store in + WithPerceptionTracking { + PokitLinkEditView(store: store) + } + } .task { await send(.뷰가_나타났을때).finish() } } } @@ -85,8 +95,9 @@ private extension PokitRootView { ) Spacer() - - if !store.contents.isEmpty { + + /// 카테고리가 있을 때 `정렬` 추가 + if store.folderType == .folder(.포킷) && store.categories != nil { PokitIconLTextLink( store.sortType == .sort(.최신순) ? "최신순" : store.folderType == .folder(.포킷) ? "이름순" : "오래된순", @@ -95,8 +106,15 @@ private extension PokitRootView { ) .contentTransition(.numericText()) } + /// 미분류 링크가 있을 때 `편집하기` 추가 + if store.folderType == .folder(.미분류) && !store.contents.isEmpty { + PokitTextLink( + "편집하기", + color: .bg(.brand), + action: { send(.편집하기_버튼_눌렀을때) } + ) + } } - .animation(.snappy(duration: 0.7), value: store.folderType) } var cardScrollView: some View { diff --git a/Projects/Feature/FeaturePokit/Sources/Sheet/PokitLinkEditFloatView.swift b/Projects/Feature/FeaturePokit/Sources/Sheet/PokitLinkEditFloatView.swift new file mode 100644 index 00000000..98fc7899 --- /dev/null +++ b/Projects/Feature/FeaturePokit/Sources/Sheet/PokitLinkEditFloatView.swift @@ -0,0 +1,111 @@ +// +// PokitLinkEditFloatView.swift +// Feature +// +// Created by 김민호 on 12/27/24. +// + +import SwiftUI + +/// `포킷` -> `미분류` -> `편집하기` -> `하단 float Button` +public struct PokitLinkEditFloatView: View { + /// 전체 선택/해제 toggle + @State private var isChecked: Bool = false + private let delegateSend: ((PokitLinkEditFloatView.Delegate) -> Void)? + + public init( + delegateSend: ((PokitLinkEditFloatView.Delegate) -> Void)? + ) { + self.delegateSend = delegateSend + } + + public var body: some View { + RoundedRectangle(cornerRadius: 16) + .foregroundStyle(.pokit(.bg(.brand))) + .frame(height: 84) + .overlay { + HStack(spacing: 0) { + button(isChecked ? .전체해제 : .전체선택) + Spacer() + button(.링크삭제) + Spacer() + button(.포킷이동) + } + .padding(.horizontal, 20) + } + .pokitShadow( + x: 0, + y: -2, + blur: 20, + spread: 0, + color: Color.black, + colorPercent: 10 + ) + } +} +private extension PokitLinkEditFloatView { + func button(_ type: PokitLinkEditFloatType) -> some View { + Button { + if type == .전체선택 || + type == .전체해제 { + isChecked.toggle() + } + delegateSend?(type.action) + } label: { + VStack(spacing: 4) { + type.icon + Text(type.label) + .pokitFont(.detail2) + .padding(.horizontal, 18) + } + } + .buttonStyle(.plain) + .foregroundStyle(.white) + } +} +public extension PokitLinkEditFloatView { + enum PokitLinkEditFloatType: String { + case 전체해제 = "전체 해제" + case 전체선택 = "전체 선택" + case 링크삭제 = "링크 삭제" + case 포킷이동 = "포킷 이동" + + var label: String { self.rawValue } + + var icon: Image { + switch self { + case .전체해제: + return Image(.icon(.allUncheck)) + case .전체선택: + return Image(.icon(.allCheck)) + case .링크삭제: + return Image(.icon(.trash)) + case .포킷이동: + return Image(.icon(.movePokit)) + } + } + + var action: Delegate { + switch self { + case .전체해제: + return .전체해제_버튼_눌렀을때 + case .전체선택: + return .전체선택_버튼_눌렀을때 + case .링크삭제: + return .링크삭제_버튼_눌렀을때 + case .포킷이동: + return .포킷이동_버튼_눌렀을때 + } + } + } + + enum Delegate { + case 전체선택_버튼_눌렀을때 + case 전체해제_버튼_눌렀을때 + case 링크삭제_버튼_눌렀을때 + case 포킷이동_버튼_눌렀을때 + } +} +#Preview { + PokitLinkEditFloatView(delegateSend: {_ in }).padding(20) +} diff --git a/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift b/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift index 6ddbeaaa..2f47cfc3 100644 --- a/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift +++ b/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift @@ -2,9 +2,66 @@ import ComposableArchitecture import XCTest @testable import FeaturePokit +@testable import Domain final class FeaturePokitTests: XCTestCase { - func test() { + /// ex) 1.3.5버전일 때 + /// 1: Major + /// 3: Minor + /// 5: Patch + func test_patch_버전이_앱스토어가_더_커도_업데이트가_필요하지_않다() { + let current_V = Version("1.0.8", trackId: 0) + let appStore_V = Version("1.0.9", trackId: 0) + + let result = updateNeeded(currentVersion: current_V, appStoreVersion: appStore_V) + /// expect: False + XCTAssertFalse(result) + } + + func test_minor_버전이_앱스토어가_더_클때_업데이트가_필요하다() { + let current_V = Version("1.0.8", trackId: 0) + let appStore_V = Version("1.1.8", trackId: 0) + + let result = updateNeeded(currentVersion: current_V, appStoreVersion: appStore_V) + /// expect: True + XCTAssertTrue(result) + } + + func test_major_버전이_앱스토어가_더_클때_업데이트가_필요하다() { + let current_V = Version("1.0.8", trackId: 0) + let appStore_V = Version("2.0.8", trackId: 0) + + let result = updateNeeded(currentVersion: current_V, appStoreVersion: appStore_V) + /// expect: True + XCTAssertTrue(result) + } + + func test_major는_앱스토어가_더_높고_minor는_현재_버전이_더_클때_업데이트가_필요하다() { + let current_V = Version("1.6.4", trackId: 0) + let appStore_V = Version("2.1.4", trackId: 0) + + let result = updateNeeded(currentVersion: current_V, appStoreVersion: appStore_V) + /// expect: True + XCTAssertTrue(result) + } + + func test_버전이_같다면_업데이트가_필요하지_않다() { + let current_V = Version("1.0.8", trackId: 0) + let appStore_V = Version("1.0.8", trackId: 0) + + let result = updateNeeded(currentVersion: current_V, appStoreVersion: appStore_V) + /// expect: False + XCTAssertFalse(result) + } + +} + +extension FeaturePokitTests { + func updateNeeded( + currentVersion: Version, + appStoreVersion: Version + ) -> Bool { + return currentVersion < appStoreVersion } }