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
}
}