diff --git a/Projects/App/Resources/Pokit-info.plist b/Projects/App/Resources/Pokit-info.plist
index ab1b69d0..716b5cdb 100644
--- a/Projects/App/Resources/Pokit-info.plist
+++ b/Projects/App/Resources/Pokit-info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.5
+ 1.0.6
CFBundleURLTypes
diff --git a/Projects/App/ShareExtension/Sources/ShareRootFeature.swift b/Projects/App/ShareExtension/Sources/ShareRootFeature.swift
index b63b2e44..0634c43d 100644
--- a/Projects/App/ShareExtension/Sources/ShareRootFeature.swift
+++ b/Projects/App/ShareExtension/Sources/ShareRootFeature.swift
@@ -106,10 +106,10 @@ struct ShareRootFeature {
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
case .URL_파싱_수행:
- guard let item = state.context?.inputItems.first as? NSExtensionItem,
- let itemProvider = item.attachments?.first else {
- return .none
- }
+ guard
+ let item = state.context?.inputItems.first as? NSExtensionItem,
+ let itemProvider = item.attachments?.first
+ else { return .none }
return .run { send in
var urlItem: (any NSSecureCoding)? = nil
diff --git a/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift b/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
index ace38842..2bb9afe9 100644
--- a/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
+++ b/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
@@ -60,7 +60,8 @@ public struct AppDelegateFeature {
let setting = await self.userNotifications.getNotificationSettings()
switch setting.authorizationStatus {
case .authorized, .notDetermined:
- guard try await self.userNotifications.requestAuthorization([.alert, .sound])
+ guard
+ try await self.userNotifications.requestAuthorization([.alert, .sound])
else { return }
default: return
}
diff --git a/Projects/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift
index 3da32829..f0684502 100644
--- a/Projects/App/Sources/MainTab/MainTabFeature.swift
+++ b/Projects/App/Sources/MainTab/MainTabFeature.swift
@@ -11,6 +11,7 @@ import FeaturePokit
import FeatureRemind
import FeatureContentDetail
import Domain
+import DSKit
import Util
import CoreKit
@@ -28,7 +29,7 @@ public struct MainTabFeature {
public struct State: Equatable {
var selectedTab: MainTab = .pokit
var isBottomSheetPresented: Bool = false
- var isLinkSheetPresented: Bool = false
+ var linkPopup: PokitLinkPopup.PopupType?
var isErrorSheetPresented: Bool = false
var link: String?
@@ -40,6 +41,7 @@ public struct MainTabFeature {
@Presents var contentDetail: ContentDetailFeature.State?
@Shared(.inMemory("SelectCategory")) var categoryId: Int?
@Shared(.inMemory("PushTapped")) var isPushTapped: Bool = false
+ var categoryOfSavedContent: BaseCategoryItem?
public init() {
self.pokit = .init()
@@ -64,7 +66,7 @@ public struct MainTabFeature {
public enum View: Equatable {
case addButtonTapped
case addSheetTypeSelected(TabAddSheetType)
- case linkCopyButtonTapped
+ case 링크팝업_버튼_눌렀을때
case onAppear
case onOpenURL(url: URL)
case 경고_확인버튼_클릭
@@ -75,6 +77,8 @@ public struct MainTabFeature {
case 공유포킷_이동(sharedCategory: CategorySharing.SharedCategory)
case 경고_띄움(BaseError)
case errorSheetPresented(Bool)
+ case 링크팝업_활성화(PokitLinkPopup.PopupType)
+ case 카테고리상세_이동(category: BaseCategoryItem)
}
public enum AsyncAction: Equatable {
case 공유받은_카테고리_조회(categoryId: Int)
@@ -93,6 +97,10 @@ public struct MainTabFeature {
/// - Reducer Core
private func core(into state: inout State, action: Action) -> Effect {
switch action {
+ case .binding(\.linkPopup):
+ guard state.linkPopup == nil else { return .none }
+ state.categoryOfSavedContent = nil
+ return .none
case .binding:
return .none
case let .pushAlertTapped(isTapped):
@@ -156,9 +164,8 @@ private extension MainTabFeature {
case .포킷추가: return .send(.delegate(.포킷추가하기))
}
- case .linkCopyButtonTapped:
- state.isLinkSheetPresented = false
- return .run { send in await send(.delegate(.링크추가하기)) }
+ case .링크팝업_버튼_눌렀을때:
+ return linkPopupButtonTapped(state: &state)
case .onAppear:
if state.isPushTapped {
@@ -177,15 +184,15 @@ private extension MainTabFeature {
}
)
case .onOpenURL(url: let url):
- guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
- return .none
- }
+ guard
+ let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
+ else { return .none }
let queryItems = components.queryItems ?? []
- guard let categoryIdString = queryItems.first(where: { $0.name == "categoryId" })?.value,
- let categoryId = Int(categoryIdString) else {
- return .none
- }
+ guard
+ let categoryIdString = queryItems.first(where: { $0.name == "categoryId" })?.value,
+ let categoryId = Int(categoryIdString)
+ else { return .none }
return .send(.async(.공유받은_카테고리_조회(categoryId: categoryId)))
case .경고_확인버튼_클릭:
@@ -198,7 +205,10 @@ private extension MainTabFeature {
switch action {
case let .linkCopySuccess(url):
guard let url else { return .none }
- state.isLinkSheetPresented = true
+ state.linkPopup = .link(
+ title: Constants.복사한_링크_저장하기_문구,
+ url: url.absoluteString
+ )
state.link = url.absoluteString
return .none
@@ -209,7 +219,18 @@ private extension MainTabFeature {
case let .errorSheetPresented(isPresented):
state.isErrorSheetPresented = isPresented
return .none
-
+
+ case let .링크팝업_활성화(type):
+ state.linkPopup = type
+ return .none
+ case let .카테고리상세_이동(category):
+ if category.categoryName == "미분류" {
+ state.selectedTab = .pokit
+ state.path.removeAll()
+ return .send(.pokit(.delegate(.미분류_카테고리_활성화)))
+ }
+ state.path.append(.카테고리상세(.init(category: category)))
+ return .none
default: return .none
}
}
@@ -238,4 +259,19 @@ private extension MainTabFeature {
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
return .none
}
+
+ func linkPopupButtonTapped(state: inout State) -> Effect {
+ switch state.linkPopup {
+ case .link:
+ state.linkPopup = nil
+ return .send(.delegate(.링크추가하기))
+ case .success:
+ state.linkPopup = nil
+ guard let category = state.categoryOfSavedContent else { return .none }
+ state.categoryOfSavedContent = nil
+ return .send(.inner(.카테고리상세_이동(category: category)))
+ case .error, .text, .warning, .none:
+ return .none
+ }
+ }
}
diff --git a/Projects/App/Sources/MainTab/MainTabFeatureView.swift b/Projects/App/Sources/MainTab/MainTabFeatureView.swift
index abe6d5d6..3e695dd4 100644
--- a/Projects/App/Sources/MainTab/MainTabFeatureView.swift
+++ b/Projects/App/Sources/MainTab/MainTabFeatureView.swift
@@ -73,12 +73,10 @@ public extension MainTabView {
}
}
- if self.store.isLinkSheetPresented {
+ if self.store.linkPopup != nil {
PokitLinkPopup(
- "복사한 링크 저장하기",
- isPresented: $store.isLinkSheetPresented,
- type: .link(url: self.store.link ?? ""),
- action: { send(.linkCopyButtonTapped) }
+ type: $store.linkPopup,
+ action: { send(.링크팝업_버튼_눌렀을때, animation: .pokitSpring) }
)
}
}
@@ -94,12 +92,10 @@ private extension MainTabView {
tabView
.overlay(alignment: .bottom) {
VStack(spacing: 0) {
- if store.isLinkSheetPresented {
+ if store.linkPopup != nil {
PokitLinkPopup(
- "복사한 링크 저장하기",
- isPresented: $store.isLinkSheetPresented,
- type: .link(url: store.link ?? ""),
- action: { send(.linkCopyButtonTapped) }
+ type: $store.linkPopup,
+ action: { send(.링크팝업_버튼_눌렀을때, animation: .pokitSpring) }
)
.padding(.bottom, 20)
}
@@ -265,7 +261,8 @@ private extension MainTabView {
}
.padding(.horizontal, 28)
.onTapGesture {
- UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
+ UIImpactFeedbackGenerator(style: .light)
+ .impactOccurred()
store.send(.binding(.set(\.selectedTab, tab)))
}
}
diff --git a/Projects/App/Sources/MainTab/MainTabPath.swift b/Projects/App/Sources/MainTab/MainTabPath.swift
index 09714197..21ec7cfd 100644
--- a/Projects/App/Sources/MainTab/MainTabPath.swift
+++ b/Projects/App/Sources/MainTab/MainTabPath.swift
@@ -16,6 +16,7 @@ import FeatureContentSetting
import FeatureContentList
import FeatureCategorySharing
import Domain
+import Util
@Reducer
public struct MainTabPath {
@@ -140,8 +141,10 @@ public extension MainTabFeature {
case .contentDetail(.presented(.delegate(.즐겨찾기_갱신_완료))),
.contentDetail(.presented(.delegate(.컨텐츠_조회_완료))),
.contentDetail(.presented(.delegate(.컨텐츠_삭제_완료))):
- guard let stackElementId = state.path.ids.last,
- let lastPath = state.path.last else {
+ guard
+ let stackElementId = state.path.ids.last,
+ let lastPath = state.path.last
+ else {
switch state.selectedTab {
case .pokit:
return .send(.pokit(.delegate(.미분류_카테고리_컨텐츠_조회)))
@@ -173,13 +176,17 @@ public extension MainTabFeature {
return .none
/// - 링크추가 및 수정에서 저장하기 눌렀을 때
- case let .path(.element(stackElementId, action: .링크추가및수정(.delegate(.저장하기_완료)))):
+ case let .path(.element(stackElementId, action: .링크추가및수정(.delegate(.저장하기_완료(contentId))))):
+ state.categoryOfSavedContent = contentId
state.path.removeLast()
switch state.path.last {
case .검색:
- return .send(.path(.element(id: stackElementId, action: .검색(.delegate(.컨텐츠_검색)))))
+ return .merge(
+ .send(.path(.element(id: stackElementId, action: .검색(.delegate(.컨텐츠_검색))))),
+ .send(.inner(.링크팝업_활성화(.success(title: Constants.링크_저장_완료_문구))), animation: .pokitSpring)
+ )
default:
- return .none
+ return .send(.inner(.링크팝업_활성화(.success(title: Constants.링크_저장_완료_문구))), animation: .pokitSpring)
}
/// - 각 화면에서 링크 복사 감지했을 때 (링크 추가 및 수정 화면 제외)
case let .path(.element(_, action: .알림함(.delegate(.linkCopyDetected(url))))),
@@ -216,7 +223,7 @@ public extension MainTabFeature {
),
title: content.title,
data: content.data,
- memo: content.memo,
+ memo: content.memo ?? "",
createdAt: content.createdAt,
favorites: nil,
alertYn: .no
diff --git a/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift b/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
index 037792af..ea5eaaf4 100644
--- a/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
+++ b/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
@@ -44,16 +44,20 @@ public final class TokenInterceptor: RequestInterceptor {
dueTo error: Error,
completion: @escaping (RetryResult) -> Void
) {
- guard let response = request.task?.response as? HTTPURLResponse,
- response.statusCode == 401 else {
+ guard
+ let response = request.task?.response as? HTTPURLResponse,
+ response.statusCode == 401
+ else {
completion(.doNotRetryWithError(error))
return
}
print("🚀 Retry: statusCode: \(response.statusCode)")
- guard keychain.read(.accessToken) != nil,
- let refreshToken = keychain.read(.refreshToken) else {
+ guard
+ keychain.read(.accessToken) != nil,
+ let refreshToken = keychain.read(.refreshToken)
+ else {
deleteAllToken()
completion(.doNotRetryWithError(error))
return
diff --git a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift
index a27d8a0a..925454c3 100644
--- a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift
@@ -45,14 +45,16 @@ extension KakaoShareClient: DependencyKey {
buttons: [button]
)
- guard ShareApi.isKakaoTalkSharingAvailable(),
- let templateJsonData = try? SdkJSONEncoder.custom.encode(template),
- let templateJsonObject = SdkUtils.toJsonObject(templateJsonData) else {
+ guard
+ ShareApi.isKakaoTalkSharingAvailable(),
+ let templateJsonData = try? SdkJSONEncoder.custom.encode(template),
+ let templateJsonObject = SdkUtils.toJsonObject(templateJsonData)
+ else {
/// 🚨 Error Case [1]: 카카오톡 미설치
- guard let url = URL(string: "itms-apps://itunes.apple.com/app/id362057947"),
- UIApplication.shared.canOpenURL(url) else {
- return
- }
+ guard
+ let url = URL(string: "itms-apps://itunes.apple.com/app/id362057947"),
+ UIApplication.shared.canOpenURL(url)
+ else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return
diff --git a/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/AppleLoginController.swift b/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/AppleLoginController.swift
index f8c243c9..fda12a78 100644
--- a/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/AppleLoginController.swift
+++ b/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/AppleLoginController.swift
@@ -33,22 +33,28 @@ public final class AppleLoginController: NSObject, ASAuthorizationControllerDele
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
- guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
+ guard
+ let credential = authorization.credential as? ASAuthorizationAppleIDCredential
+ else {
continuation?.resume(throwing: SocialLoginError.invalidCredential)
continuation = nil
return
}
- guard let tokenData = credential.identityToken,
- let token = String(data: tokenData, encoding: .utf8) else {
+ guard
+ let tokenData = credential.identityToken,
+ let token = String(data: tokenData, encoding: .utf8)
+ else {
continuation?.resume(throwing: SocialLoginError.appleLoginError(.invalidIdentityToken))
continuation = nil
return
}
- guard let authorizationCode = credential.authorizationCode,
- let codeString = String(data: authorizationCode, encoding: .utf8) else {
+ guard
+ let authorizationCode = credential.authorizationCode,
+ let codeString = String(data: authorizationCode, encoding: .utf8)
+ else {
continuation?.resume(throwing: SocialLoginError.appleLoginError(.invalidAuthorizationCode))
continuation = nil
return
diff --git a/Projects/CoreKit/Sources/Data/DTO/Base/ContentBaseResponse.swift b/Projects/CoreKit/Sources/Data/DTO/Base/ContentBaseResponse.swift
index df8e5fc8..2568b90a 100644
--- a/Projects/CoreKit/Sources/Data/DTO/Base/ContentBaseResponse.swift
+++ b/Projects/CoreKit/Sources/Data/DTO/Base/ContentBaseResponse.swift
@@ -13,9 +13,11 @@ public struct ContentBaseResponse: Decodable {
public let data: String
public let domain: String
public let title: String
+ public let memo: String?
public let thumbNail: String
public let createdAt: String
public let isRead: Bool?
+ public let isFavorite: Bool?
}
extension ContentBaseResponse {
@@ -29,9 +31,11 @@ extension ContentBaseResponse {
data: "https://www.youtube.com/watch?v=wtSwdGJzQCQ",
domain: "youtube",
title: "신서유기",
+ memo: nil,
thumbNail: "https://i.ytimg.com/vi/NnOC4_kH0ok/hqdefault.jpg?sqp=-oaymwEjCNACELwBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLDN6u6mTjbaVmRZ4biJS_aDq4uvAQ",
createdAt: "2024.08.08",
- isRead: false
+ isRead: false,
+ isFavorite: true
)
}
}
diff --git a/Projects/CoreKit/Sources/Data/DTO/Category/CategoryEditResponse.swift b/Projects/CoreKit/Sources/Data/DTO/Category/CategoryEditResponse.swift
index e7de613a..a1681c90 100644
--- a/Projects/CoreKit/Sources/Data/DTO/Category/CategoryEditResponse.swift
+++ b/Projects/CoreKit/Sources/Data/DTO/Category/CategoryEditResponse.swift
@@ -35,7 +35,7 @@ extension CategoryImageResponse {
public static var mock: [Self] = [
Self(
imageId: 2312,
- imageUrl: "https://pokit-storage.s3.ap-northeast-2.amazonaws.com/logo/pokit.png"
+ imageUrl: Constants.기본_썸네일_주소.absoluteString
),
Self(
imageId: 23122,
diff --git a/Projects/CoreKit/Sources/Data/DTO/Content/ThumbnailRequest.swift b/Projects/CoreKit/Sources/Data/DTO/Content/ThumbnailRequest.swift
new file mode 100644
index 00000000..e44ff24e
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/DTO/Content/ThumbnailRequest.swift
@@ -0,0 +1,16 @@
+//
+// ThumbnailRequest.swift
+// CoreKit
+//
+// Created by 김도형 on 12/1/24.
+//
+
+import Foundation
+
+public struct ThumbnailRequest: Encodable {
+ private let thumbnail: String
+
+ public init(thumbnail: String) {
+ self.thumbnail = thumbnail
+ }
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift
index d5027625..ee6d1954 100644
--- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift
@@ -50,6 +50,11 @@ extension ContentClient: DependencyKey {
condition: condition
)
)
+ },
+ 썸네일_수정: { id, model in
+ try await provider.requestNoBody(
+ .썸네일_수정(contentId: id, 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 7b22cf86..ab5031b2 100644
--- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift
@@ -17,7 +17,8 @@ extension ContentClient: TestDependencyKey {
즐겨찾기_취소: { _ in },
카테고리_내_컨텐츠_목록_조회: { _, _, _ in .mock },
미분류_카테고리_컨텐츠_조회: { _ in .mock },
- 컨텐츠_검색: { _, _ in .mock }
+ 컨텐츠_검색: { _, _ in .mock },
+ 썸네일_수정: { _, _ in }
)
}()
}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
index c7210f37..62de49d9 100644
--- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
@@ -40,5 +40,9 @@ public struct ContentClient {
_ pageable: BasePageableRequest,
_ condition: BaseConditionRequest
) async throws -> ContentListInquiryResponse
+ public var 썸네일_수정: @Sendable (
+ _ contentId: String,
+ _ model: ThumbnailRequest
+ ) async throws -> Void
}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift
index 593fd99a..0c1c5769 100644
--- a/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentEndpoint.swift
@@ -27,6 +27,7 @@ public enum ContentEndpoint {
pageable: BasePageableRequest,
condition: BaseConditionRequest
)
+ case 썸네일_수정(contentId: String, model: ThumbnailRequest)
}
extension ContentEndpoint: TargetType {
@@ -54,6 +55,8 @@ extension ContentEndpoint: TargetType {
return "/uncategorized"
case .컨텐츠_검색:
return ""
+ case let .썸네일_수정(contentId, _):
+ return "/thumbnail/\(contentId)"
}
}
@@ -68,7 +71,8 @@ extension ContentEndpoint: TargetType {
.컨텐츠_추가:
return .post
- case .컨텐츠_수정:
+ case .컨텐츠_수정,
+ .썸네일_수정:
return .patch
case .카태고리_내_컨텐츠_목록_조회,
@@ -129,6 +133,8 @@ extension ContentEndpoint: TargetType {
],
encoding: URLEncoding.default
)
+ case let .썸네일_수정(_, model):
+ return .requestJSONEncodable(model)
}
}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/Contents.json
new file mode 100644
index 00000000..810a7d79
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_allcheck.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/icon_allcheck.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/icon_allcheck.svg
new file mode 100644
index 00000000..375a1345
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_allcheck.imageset/icon_allcheck.svg
@@ -0,0 +1,4 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/Contents.json
new file mode 100644
index 00000000..170c3b31
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_alluncheck.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/icon_alluncheck.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/icon_alluncheck.svg
new file mode 100644
index 00000000..d038a9b5
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_alluncheck.imageset/icon_alluncheck.svg
@@ -0,0 +1,5 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/Contents.json
new file mode 100644
index 00000000..6a687d92
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_arrow-down2.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/icon_arrow-down2.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/icon_arrow-down2.svg
new file mode 100644
index 00000000..7ec97e97
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_arrow-down2.imageset/icon_arrow-down2.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/Contents.json
new file mode 100644
index 00000000..20663e8f
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_hashtag.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/icon_hashtag.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/icon_hashtag.svg
new file mode 100644
index 00000000..50a28eb8
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_hashtag.imageset/icon_hashtag.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/Contents.json
new file mode 100644
index 00000000..4dc49364
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_invite.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/icon_invite.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/icon_invite.svg
new file mode 100644
index 00000000..d7b7dc78
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_invite.imageset/icon_invite.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/Contents.json
new file mode 100644
index 00000000..75bc547f
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_lock.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/icon_lock.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/icon_lock.svg
new file mode 100644
index 00000000..2bf0bfdd
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_lock.imageset/icon_lock.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/Contents.json
new file mode 100644
index 00000000..ff4a10cb
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_member.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/icon_member.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/icon_member.svg
new file mode 100644
index 00000000..6e8af029
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_member.imageset/icon_member.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/Contents.json
new file mode 100644
index 00000000..94632333
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_memo.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/icon_memo.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/icon_memo.svg
new file mode 100644
index 00000000..6c891dbc
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_memo.imageset/icon_memo.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/Contents.json
new file mode 100644
index 00000000..2108e930
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_movepokit.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/icon_movepokit.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/icon_movepokit.svg
new file mode 100644
index 00000000..259a08c2
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_movepokit.imageset/icon_movepokit.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/Contents.json
new file mode 100644
index 00000000..2046ad44
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_pin.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/icon_pin.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/icon_pin.svg
new file mode 100644
index 00000000..a36bb254
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_pin.imageset/icon_pin.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/Contents.json
new file mode 100644
index 00000000..5c65fb7f
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_report.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/icon_report.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/icon_report.svg
new file mode 100644
index 00000000..8067621f
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_report.imageset/icon_report.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/Contents.json
new file mode 100644
index 00000000..59b65b24
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_savepokit.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/icon_savepokit.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/icon_savepokit.svg
new file mode 100644
index 00000000..39691b8a
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_savepokit.imageset/icon_savepokit.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/Contents.json b/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/Contents.json
new file mode 100644
index 00000000..40dd0b71
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_xs.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/icon_xs.svg b/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/icon_xs.svg
new file mode 100644
index 00000000..3b3a4d1e
--- /dev/null
+++ b/Projects/DSKit/Resources/Assets.xcassets/icon_xs.imageset/icon_xs.svg
@@ -0,0 +1,3 @@
+
diff --git a/Projects/DSKit/Sources/Components/PoKitIconRButton.swift b/Projects/DSKit/Sources/Components/PoKitIconRButton.swift
index 0e42b772..7483ea0c 100644
--- a/Projects/DSKit/Sources/Components/PoKitIconRButton.swift
+++ b/Projects/DSKit/Sources/Components/PoKitIconRButton.swift
@@ -50,8 +50,8 @@ public struct PokitIconRButton: View {
.frame(width: self.size.iconSize.width, height: self.size.iconSize.height)
.foregroundStyle(self.state.iconColor)
}
- .padding(.leading, self.lPadding)
- .padding(.trailing, self.tPadding)
+ .padding(.leading, self.size.hPadding + 4)
+ .padding(.trailing, self.size.hPadding)
.padding(.vertical, self.size.vPadding)
.background {
RoundedRectangle(cornerRadius: shape.radius(size: self.size), style: .continuous)
@@ -61,27 +61,6 @@ public struct PokitIconRButton: View {
.stroke(self.state.backgroundStrokeColor, lineWidth: 1)
}
}
- }
-
- private var tPadding: CGFloat {
- switch self.size {
- case .small:
- return 8
- case .medium:
- return 16
- case .large:
- return 20
- }
- }
-
- private var lPadding: CGFloat {
- switch self.size {
- case .small:
- return 12
- case .medium:
- return 20
- case .large:
- return 24
- }
+ .frame(minWidth: self.size.minWidth)
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitBadge.swift b/Projects/DSKit/Sources/Components/PokitBadge.swift
index 4afa8ab3..adace557 100644
--- a/Projects/DSKit/Sources/Components/PokitBadge.swift
+++ b/Projects/DSKit/Sources/Components/PokitBadge.swift
@@ -8,51 +8,95 @@
import SwiftUI
public struct PokitBadge: View {
- private let labelText: String
private let state: PokitBadge.State
- public init(
- _ labelText: String,
- state: PokitBadge.State
- ) {
- self.labelText = labelText
+ public init(state: PokitBadge.State) {
self.state = state
}
public var body: some View {
- Text(labelText)
- .pokitFont(.l4)
- .foregroundStyle(
- state == .unCategorized ? .pokit(.text(.secondary)) : .pokit(.text(.tertiary))
- )
- .padding(.horizontal, state == .small ? 4 : 8)
- .padding(.vertical, state == .small ? 2 : 4)
+ label
.background {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(backgroundColor)
- .overlay {
- if state == .unRead {
- RoundedRectangle(cornerRadius: 4, style: .continuous)
- .stroke(.pokit(.border(.tertiary)), lineWidth: 1)
- }
- }
}
}
private var backgroundColor: Color {
switch self.state {
- case .default, .small: return .pokit(.bg(.primary))
+ case .default, .small, .memo, .member: return .pokit(.bg(.primary))
case .unCategorized: return .pokit(.color(.grayScale(._50)))
- case .unRead: return .pokit(.bg(.base))
+ case .unRead: return Color(red: 1, green: 0.95, blue: 0.92)
}
}
+
+ private var labelColor: Color {
+ switch self.state {
+ case .default, .small: return .pokit(.text(.tertiary))
+ case .unCategorized: return .pokit(.text(.secondary))
+ case .unRead: return .pokit(.text(.brand))
+ case .memo, .member: return .pokit(.icon(.secondary))
+ }
+ }
+
+ private var label: some View {
+ Group {
+ switch self.state {
+ case let .default(labelText):
+ Text(labelText)
+ .pokitFont(.l4)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ case let .small(labelText):
+ Text(labelText)
+ .pokitFont(.l4)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 2)
+ case .unCategorized:
+ Text("미분류")
+ .pokitFont(.l4)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ case .unRead:
+ Text("안읽음")
+ .pokitFont(.l4)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ case .memo:
+ Image(.icon(.memo))
+ .resizable()
+ .frame(width: 16, height: 16)
+ .padding(2)
+ case .member:
+ Image(.icon(.member))
+ .resizable()
+ .frame(width: 16, height: 16)
+ .padding(2)
+ }
+ }
+ .foregroundStyle(labelColor)
+ }
}
public extension PokitBadge {
- enum State {
- case `default`
- case small
+ enum State: Equatable {
+ case `default`(String)
+ case small(String)
case unCategorized
case unRead
+ case memo
+ case member
}
}
+
+#Preview {
+ PokitBadge(state: .unRead)
+
+ PokitBadge(state: .default("포킷명"))
+
+ PokitBadge(state: .unCategorized)
+
+ PokitBadge(state: .memo)
+
+ PokitBadge(state: .member)
+}
diff --git a/Projects/DSKit/Sources/Components/PokitBookmark.swift b/Projects/DSKit/Sources/Components/PokitBookmark.swift
new file mode 100644
index 00000000..609602bf
--- /dev/null
+++ b/Projects/DSKit/Sources/Components/PokitBookmark.swift
@@ -0,0 +1,71 @@
+//
+// PokitBookmark.swift
+// DSKit
+//
+// Created by 김도형 on 11/28/24.
+//
+
+import SwiftUI
+
+public struct PokitBookmark: View {
+ private let state: PokitBookmark.State
+ private let action: () -> Void
+
+ public init(
+ state: PokitBookmark.State,
+ action: @escaping () -> Void
+ ) {
+ self.state = state
+ self.action = action
+ }
+
+ public var body: some View {
+ Button(action: action) {
+ Image(.icon(.starFill))
+ .resizable()
+ .frame(width: 24, height: 24)
+ .foregroundStyle(starColor)
+ .padding(4)
+ .background {
+ RoundedRectangle(
+ cornerRadius: 9999,
+ style: .continuous
+ )
+ .fill(backgroundColor)
+ }
+ }
+ .disabled(state == .disable)
+ }
+
+ private var starColor: Color {
+ switch state {
+ case .default, .disable:
+ return .pokit(.icon(.disable))
+ case .active:
+ return .pokit(.icon(.brand))
+ }
+ }
+
+ private var backgroundColor: Color {
+ switch state {
+ case .default, .active:
+ return .pokit(.bg(.baseIcon))
+ case .disable:
+ return .pokit(.bg(.disable))
+ }
+ }
+}
+
+extension PokitBookmark {
+ public enum State {
+ case `default`
+ case active
+ case disable
+ }
+}
+
+#Preview {
+ PokitBookmark(state: .active) {
+
+ }
+}
diff --git a/Projects/DSKit/Sources/Components/PokitCalendar.swift b/Projects/DSKit/Sources/Components/PokitCalendar.swift
index c4f58435..f3e95270 100644
--- a/Projects/DSKit/Sources/Components/PokitCalendar.swift
+++ b/Projects/DSKit/Sources/Components/PokitCalendar.swift
@@ -208,13 +208,13 @@ public struct PokitCalendar: View {
let year = calendar.component(.year, from: date)
let month = calendar.component(.month, from: date)
- guard let range = calendar.range(
- of: .day,
- in: .month,
- for: date
- ) else {
- return dates
- }
+ guard
+ let range = calendar.range(
+ of: .day,
+ in: .month,
+ for: date
+ )
+ else { return dates }
dates = range.map { day in
var components = DateComponents()
@@ -255,13 +255,13 @@ public struct PokitCalendar: View {
return dates
}
- guard let monthRange = calendar.range(
- of: .day,
- in: .month,
- for: monthDate
- ) else {
- return dates
- }
+ guard
+ let monthRange = calendar.range(
+ of: .day,
+ in: .month,
+ for: monthDate
+ )
+ else { return dates }
let monthDays = Array(monthRange).suffix(firstWeekday - 1)
@@ -369,25 +369,24 @@ public struct PokitCalendar: View {
}
private func beforeButtonTapped() {
- guard let date = calendar.date(
- byAdding: .month,
- value: -1,
- to: currentDate
- ) else {
- return
- }
+ guard
+ let date = calendar.date(
+ byAdding: .month,
+ value: -1,
+ to: currentDate
+ ) else { return }
self.page = formatter.string(from: date)
}
private func nextButtonTapped() {
- guard let date = calendar.date(
- byAdding: .month,
- value: 1,
- to: currentDate
- ) else {
- return
- }
+ guard
+ let date = calendar.date(
+ byAdding: .month,
+ value: 1,
+ to: currentDate
+ )
+ else { return }
self.page = formatter.string(from: date)
}
diff --git a/Projects/DSKit/Sources/Components/PokitIconLButton.swift b/Projects/DSKit/Sources/Components/PokitIconLButton.swift
index 340e5a2f..0cc803f1 100644
--- a/Projects/DSKit/Sources/Components/PokitIconLButton.swift
+++ b/Projects/DSKit/Sources/Components/PokitIconLButton.swift
@@ -49,8 +49,8 @@ public struct PokitIconLButton: View {
.pokitFont(self.size.font)
.foregroundStyle(self.state.textColor)
}
- .padding(.leading, self.lPadding)
- .padding(.trailing, self.tPadding)
+ .padding(.leading, self.size.hPadding)
+ .padding(.trailing, self.size.hPadding + 4)
.padding(.vertical, self.size.vPadding)
.background {
RoundedRectangle(cornerRadius: shape.radius(size: self.size), style: .continuous)
@@ -60,27 +60,6 @@ public struct PokitIconLButton: View {
.stroke(self.state.backgroundStrokeColor, lineWidth: 1)
}
}
- }
-
- private var lPadding: CGFloat {
- switch self.size {
- case .small:
- return 8
- case .medium:
- return 16
- case .large:
- return 20
- }
- }
-
- private var tPadding: CGFloat {
- switch self.size {
- case .small:
- return 12
- case .medium:
- return 20
- case .large:
- return 24
- }
+ .frame(minWidth: self.size.minWidth)
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitIconLInput.swift b/Projects/DSKit/Sources/Components/PokitIconLInput.swift
deleted file mode 100644
index 2662c978..00000000
--- a/Projects/DSKit/Sources/Components/PokitIconLInput.swift
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// PokitIconLInput.swift
-// DSKit
-//
-// Created by 김도형 on 6/28/24.
-//
-
-import SwiftUI
-
-public struct PokitIconLInput: View {
- @Binding private var text: String
-
- private let icon: PokitImage
-
- @State private var state: PokitInputStyle.State
-
- private var focusState: FocusState.Binding
-
- private let shape: PokitInputStyle.Shape
- private let equals: Value
- private let placeholder: String
- private let onSubmit: (() -> Void)?
- private let iconTappedAction: (() -> Void)?
-
- public init(
- text: Binding,
- icon: PokitImage,
- state: PokitInputStyle.State = .default,
- placeholder: String = "내용을 입력해주세요.",
- shape: PokitInputStyle.Shape,
- focusState: FocusState.Binding,
- equals: Value,
- onSubmit: (() -> Void)? = nil,
- iconTappedAction: (() -> Void)? = nil
- ) {
- self._text = text
- self.icon = icon
- self._state = State(initialValue: state)
- self.shape = shape
- self.focusState = focusState
- self.equals = equals
- self.placeholder = placeholder
- self.onSubmit = onSubmit
- self.iconTappedAction = iconTappedAction
- }
-
- public var body: some View {
- HStack(spacing: 8) {
- iconButton(icon: icon)
-
- textField
- }
- .onChange(of: text) { onChangedText($0) }
- .padding(.vertical, shape == .round ? 8 : 13)
- .padding(.trailing, shape == .round ? 20 : 12)
- .padding(.leading, 13)
- .background(
- state: self.state,
- shape: shape
- )
- }
-
- private var textField: some View {
- TextField(text: $text) {
- placeholderLabel
- }
- .autocorrectionDisabled()
- .textInputAutocapitalization(.never)
- .focused(focusState, equals: equals)
- .pokitFont(.b3(.m))
- .foregroundStyle(.pokit(.text(.secondary)))
- .disabled(state == .disable || state == .readOnly)
- .onSubmit {
- onSubmit?()
- }
- .onChange(of: focusState.wrappedValue) { onChangedFocuseState($0) }
- }
-
- private var placeholderLabel: some View {
- Text(placeholder)
- .foregroundStyle(self.state.infoColor)
- }
-
- @ViewBuilder
- private func iconButton(icon: PokitImage) -> some View {
- Button {
- if let iconTappedAction {
- iconTappedAction()
- } else {
- onSubmit?()
- }
- } label: {
- Image(icon)
- .resizable()
- .frame(width: 24, height: 24)
- .foregroundStyle(state.iconColor)
- .animation(.pokitDissolve, value: self.state)
- }
- }
-
- private func onChangedText(_ newValue: String) {
- state = newValue != "" ? .input : .default
- }
-
- private func onChangedFocuseState(_ newValue: Value) {
- state = newValue == equals ? .active : .default
- }
-}
diff --git a/Projects/DSKit/Sources/Components/PokitIconRInput.swift b/Projects/DSKit/Sources/Components/PokitIconRInput.swift
deleted file mode 100644
index 674bd280..00000000
--- a/Projects/DSKit/Sources/Components/PokitIconRInput.swift
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// PokitIconRInput.swift
-// DSKit
-//
-// Created by 김도형 on 6/28/24.
-//
-
-import SwiftUI
-
-public struct PokitIconRInput: View {
- @Binding private var text: String
-
- private let icon: PokitImage
-
- @State private var state: PokitInputStyle.State
-
- private var focusState: FocusState.Binding
-
- private let shape: PokitInputStyle.Shape
- private let equals: Value
- private let placeholder: String
- private let onSubmit: (() -> Void)?
- private let iconTappedAction: (() -> Void)?
-
- public init(
- text: Binding,
- icon: PokitImage,
- state: PokitInputStyle.State = .default,
- placeholder: String = "내용을 입력해주세요.",
- shape: PokitInputStyle.Shape,
- focusState: FocusState.Binding,
- equals: Value,
- onSubmit: (() -> Void)? = nil,
- iconTappedAction: (() -> Void)? = nil
- ) {
- self._text = text
- self.icon = icon
- self._state = State(initialValue: state)
- self.shape = shape
- self.focusState = focusState
- self.equals = equals
- self.placeholder = placeholder
- self.onSubmit = onSubmit
- self.iconTappedAction = iconTappedAction
- }
-
- public var body: some View {
- HStack(spacing: 8) {
- textField
-
- iconButton(icon: icon)
- }
- .onChange(of: text) { onChangedText($0) }
- .padding(.vertical, shape == .round ? 8 : 13)
- .padding(.leading, shape == .round ? 20 : 12)
- .padding(.trailing, 13)
- .background(
- state: self.state,
- shape: shape
- )
- }
-
- private var textField: some View {
- TextField(text: $text) {
- placeholderLabel
- }
- .autocorrectionDisabled()
- .textInputAutocapitalization(.never)
- .focused(focusState, equals: equals)
- .pokitFont(.b3(.m))
- .foregroundStyle(.pokit(.text(.secondary)))
- .disabled(state == .disable || state == .readOnly)
- .onSubmit {
- onSubmit?()
- }
- .onChange(of: focusState.wrappedValue) { onChangedFocuseState($0) }
- }
-
- private var placeholderLabel: some View {
- Text(placeholder)
- .foregroundStyle(self.state.infoColor)
- }
-
- @ViewBuilder
- private func iconButton(icon: PokitImage) -> some View {
- Button {
- if let iconTappedAction {
- iconTappedAction()
- } else {
- onSubmit?()
- }
- } label: {
- Image(icon)
- .resizable()
- .frame(width: 24, height: 24)
- .foregroundStyle(state.iconColor)
- .animation(.pokitDissolve, value: self.state)
- }
- }
-
- private func onChangedText(_ newValue: String) {
- state = newValue != "" ? .input : .default
- }
-
- private func onChangedFocuseState(_ newValue: Value) {
- state = newValue == equals ? .active : .default
- }
-}
diff --git a/Projects/DSKit/Sources/Components/PokitLinkCard.swift b/Projects/DSKit/Sources/Components/PokitLinkCard.swift
index a3d5a5be..7ff26664 100644
--- a/Projects/DSKit/Sources/Components/PokitLinkCard.swift
+++ b/Projects/DSKit/Sources/Components/PokitLinkCard.swift
@@ -12,25 +12,48 @@ import NukeUI
public struct PokitLinkCard: View {
private let link: Item
+ private let state: PokitLinkCard.State
+ private let type: PokitLinkCard.CardType
private let action: () -> Void
private let kebabAction: (() -> Void)?
private let fetchMetaData: (() -> Void)?
+ private let favoriteAction: (() -> Void)?
+ private let selectAction: (() -> Void)?
public init(
link: Item,
+ state: PokitLinkCard.State,
+ type: PokitLinkCard.CardType = .accept,
action: @escaping () -> Void,
kebabAction: (() -> Void)? = nil,
- fetchMetaData: (() -> Void)? = nil
+ fetchMetaData: (() -> Void)? = nil,
+ favoriteAction: (() -> Void)? = nil,
+ selectAction: (() -> Void)? = nil
) {
self.link = link
+ self.state = state
+ self.type = type
self.action = action
self.kebabAction = kebabAction
self.fetchMetaData = fetchMetaData
+ self.favoriteAction = favoriteAction
+ self.selectAction = selectAction
}
public var body: some View {
- Button(action: action) {
- buttonLabel
+ VStack(spacing: 20) {
+ Button(action: action) {
+ buttonLabel
+ }
+ .padding(.top, state == .top ? 0 : 20)
+
+ if case .top = state {
+ divider
+ }
+
+ if case .middle = state {
+ divider
+ }
}
}
@@ -68,6 +91,28 @@ public struct PokitLinkCard: View {
}
}
}
+ .overlay(alignment: .bottomLeading) {
+ if case .linkList = type,
+ let isFavorite = link.isFavorite {
+ PokitBookmark(
+ state: isFavorite ? .active : .default,
+ action: { favoriteAction?() }
+ )
+ .padding(4)
+ }
+ }
+ .overlay(alignment: .topLeading) {
+ if case .unCatgorized(let isSelected) = type {
+ PokitCheckBox(
+ baseState: .default,
+ selectedState: .filled,
+ isSelected: .constant(isSelected),
+ shape: .rectangle,
+ action: { selectAction?() }
+ )
+ .padding(4)
+ }
+ }
}
private var title: some View {
@@ -91,12 +136,17 @@ public struct PokitLinkCard: View {
HStack(spacing: 6) {
PokitBadge(
- link.categoryName,
- state: isUnCategorized ? .unCategorized : .default
+ state: isUnCategorized
+ ? .unCategorized
+ : .default(link.categoryName)
)
if let isRead = link.isRead, !isRead {
- PokitBadge("안읽음", state: .unRead)
+ PokitBadge(state: .unRead)
+ }
+
+ if let memo = link.memo, !memo.isEmpty {
+ PokitBadge(state: .memo)
}
}
}
@@ -149,17 +199,18 @@ public struct PokitLinkCard: View {
.frame(width: 124, height: 94)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
+}
+
+extension PokitLinkCard {
+ public enum State {
+ case top
+ case middle
+ case bottom
+ }
- @ViewBuilder
- public func divider(isFirst: Bool, isLast: Bool) -> some View {
- let edge: Edge.Set = isFirst ? .bottom : isLast ? .top : .vertical
-
- self
- .padding(edge, 20)
- .background(alignment: .bottom) {
- if !isLast {
- divider
- }
- }
+ public enum CardType {
+ case linkList
+ case unCatgorized(isSelected: Bool)
+ case accept
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitLinkPopup.swift b/Projects/DSKit/Sources/Components/PokitLinkPopup.swift
index 37c4167f..b4d9de71 100644
--- a/Projects/DSKit/Sources/Components/PokitLinkPopup.swift
+++ b/Projects/DSKit/Sources/Components/PokitLinkPopup.swift
@@ -9,11 +9,11 @@ import SwiftUI
import Combine
public struct PokitLinkPopup: View {
- @Binding private var isPresented: Bool
+ @Binding
+ private var type: PokitLinkPopup.PopupType?
- @State private var second: Int = 0
- private let titleKey: String
- private let type: PokitLinkPopup.PopupType
+ @State
+ private var second: Int = 0
private let action: (() -> Void)?
private let timer = Timer.publish(
every: 1,
@@ -22,14 +22,10 @@ public struct PokitLinkPopup: View {
).autoconnect()
public init(
- _ titleKey: String,
- isPresented: Binding,
- type: PokitLinkPopup.PopupType,
+ type: Binding,
action: (() -> Void)? = nil
) {
- self.titleKey = titleKey
- self._isPresented = isPresented
- self.type = type
+ self._type = type
self.action = action
}
@@ -37,66 +33,50 @@ public struct PokitLinkPopup: View {
ZStack {
background
- Group {
- switch self.type {
- case let .link(url):
- linkPopup(url)
- case .text:
- textPopup
- }
- }
+ popup
.padding(.vertical, 12)
.padding(.horizontal, 20)
}
.frame(width: 335, height: 60)
.transition(.move(edge: .bottom).combined(with: .opacity))
.onReceive(timer) { _ in
- guard second < 2 && isPresented else {
+ guard second < 2 else {
closedPopup()
return
}
second += 1
}
+ .onAppear(perform: feedback)
}
- @ViewBuilder
- private func linkPopup(_ url: String) -> some View {
- HStack {
+ private var popup: some View {
+ HStack(spacing: 0) {
Button {
action?()
} label: {
VStack(alignment: .leading, spacing: 0) {
- Text(titleKey)
- .lineLimit(1)
+ Text(title)
+ .lineLimit(2)
.pokitFont(.b2(.b))
.multilineTextAlignment(.leading)
- .foregroundStyle(.pokit(.text(.inverseWh)))
+ .foregroundStyle(textColor)
- Text(url)
- .lineLimit(1)
- .pokitFont(.detail2)
- .foregroundStyle(.pokit(.text(.inverseWh)))
+ if case let .link(_, url) = type {
+ Text(url)
+ .lineLimit(1)
+ .pokitFont(.detail2)
+ .foregroundStyle(.pokit(.text(.inverseWh)))
+ }
}
- Spacer(minLength: 72)
- }
-
- closeButton
- }
- }
-
- private var textPopup: some View {
- HStack {
- Button {
- action?()
- } label: {
- Text(titleKey)
- .lineLimit(2)
- .pokitFont(.b3(.b))
- .multilineTextAlignment(.leading)
- .foregroundStyle(.pokit(.text(.inverseWh)))
+ if case .success = type {
+ Image(.icon(.check))
+ .resizable()
+ .frame(width: 24, height: 24)
+ .foregroundStyle(.pokit(.icon(.inverseWh)))
+ }
- Spacer(minLength: 54)
+ Spacer(minLength: 72)
}
closeButton
@@ -105,7 +85,7 @@ public struct PokitLinkPopup: View {
private var background: some View {
RoundedRectangle(cornerRadius: 9999, style: .continuous)
- .fill(.pokit(.bg(.tertiary)))
+ .fill(backgroundColor)
}
private var closeButton: some View {
@@ -113,37 +93,110 @@ public struct PokitLinkPopup: View {
Image(.icon(.x))
.resizable()
.frame(width: 24, height: 24)
- .foregroundStyle(.pokit(.icon(.inverseWh)))
+ .foregroundStyle(iconColor)
}
}
private func closedPopup() {
withAnimation(.pokitSpring) {
+ type = nil
second = 0
- isPresented = false
+ }
+ }
+
+ private func feedback() {
+ switch type {
+ case .link, .text, .warning:
+ UINotificationFeedbackGenerator()
+ .notificationOccurred(.warning)
+ case .success:
+ UINotificationFeedbackGenerator()
+ .notificationOccurred(.success)
+ case .error:
+ UINotificationFeedbackGenerator()
+ .notificationOccurred(.error)
+ case .none: break
+ }
+ }
+
+ private var backgroundColor: Color {
+ switch type {
+ case .link, .text:
+ return .pokit(.bg(.tertiary))
+ case .success:
+ return .pokit(.bg(.success))
+ case .error:
+ return .pokit(.bg(.error))
+ case .warning:
+ return .pokit(.bg(.warning))
+ case .none: return .clear
+ }
+ }
+
+ private var iconColor: Color {
+ switch type {
+ case .warning:
+ return .pokit(.icon(.primary))
+ default:
+ return .pokit(.icon(.inverseWh))
+ }
+ }
+
+ private var textColor: Color {
+ switch type {
+ case .warning:
+ return .pokit(.text(.primary))
+ default:
+ return .pokit(.text(.inverseWh))
+ }
+ }
+
+ private var title: String {
+ switch type {
+ case let .link(title, _),
+ let .text(title),
+ let .success(title),
+ let .error(title),
+ let .warning(title):
+ return title
+ default: return ""
}
}
}
public extension PokitLinkPopup {
- enum PopupType {
- case link(url: String)
- case text
+ enum PopupType: Equatable {
+ case link(title: String, url: String)
+ case text(title: String)
+ case success(title: String)
+ case error(title: String)
+ case warning(title: String)
}
}
#Preview {
VStack {
PokitLinkPopup(
- "복사한 링크 저장하기",
- isPresented: .constant(true),
- type: .link(url: "https://www.youtube.com/watch?v=xSTwqKUyM8k")
+ type: .constant(.link(
+ title: "복사한 링크 저장하기",
+ url: "https://www.youtube.com/watch?v=xSTwqKUyM8k"
+ ))
+ )
+
+ PokitLinkPopup(
+ type: .constant(.text(title: "최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요."))
+ )
+
+ PokitLinkPopup(
+ type: .constant(.success(title: "링크저장 완료"))
+ )
+
+ PokitLinkPopup(
+ type: .constant(.error(title: "링크저장 실패"))
)
PokitLinkPopup(
- "최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요.",
- isPresented: .constant(true),
- type: .text
+ type: .constant(.warning(title: "저장공간 부족"))
)
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitLinkPreview.swift b/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
index ea17a12c..46a22a0c 100644
--- a/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
+++ b/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
@@ -7,20 +7,21 @@
import SwiftUI
+import Util
import NukeUI
public struct PokitLinkPreview: View {
@Environment(\.openURL)
private var openURL
- private let title: String
- private let url: String
- private let imageURL: String
+ private let title: String?
+ private let url: String?
+ private let imageURL: String?
public init(
- title: String,
- url: String,
- imageURL: String
+ title: String?,
+ url: String?,
+ imageURL: String?
) {
self.title = title
self.url = url
@@ -33,22 +34,14 @@ public struct PokitLinkPreview: View {
}
}
- @MainActor
private var buttonLabel: some View {
HStack(spacing: 16) {
- LazyImage(url: URL(string: imageURL)) { phase in
- Group {
- if let image = phase.image {
- image
- .resizable()
- .aspectRatio(contentMode: .fill)
- } else {
- PokitSpinner()
- .foregroundStyle(.pokit(.icon(.brand)))
- .frame(width: 48, height: 48)
- }
+ Group {
+ if let url = imageURL {
+ thumbnail(url: url)
+ } else {
+ Color.pokit(.bg(.secondary))
}
- .animation(.pokitDissolve, value: phase.image)
}
.frame(width: 124, height: 108)
.clipped()
@@ -61,41 +54,90 @@ public struct PokitLinkPreview: View {
.background {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(.pokit(.bg(.base)))
+ .shadow(color: .black.opacity(0.06), radius: 3, x: 2, y: 2)
}
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(.pokit(.border(.tertiary)), lineWidth: 1)
}
- .shadow(color: .black.opacity(0.06), radius: 3, x: 2, y: 2)
- .onAppear {
- withAnimation {
- UINotificationFeedbackGenerator()
- .notificationOccurred(.success)
+ .onChange(of: imageURL, perform: onChangeImageURL)
+ }
+
+ @MainActor
+ private func thumbnail(url: String) -> some View {
+ LazyImage(url: URL(string: url)) { phase in
+ Group {
+ if let image = phase.image {
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ } else {
+ PokitSpinner()
+ .foregroundStyle(.pokit(.icon(.brand)))
+ .frame(width: 48, height: 48)
+ }
}
+ .animation(.pokitDissolve, value: phase.image)
}
}
@ViewBuilder
- private func info(title: String) -> some View {
+ private func info(title: String?) -> some View {
VStack(alignment: .leading, spacing: 8) {
- Text(title)
- .pokitFont(.b3(.b))
- .foregroundStyle(.pokit(.text(.secondary)))
- .multilineTextAlignment(.leading)
- .lineLimit(2)
+ if let title {
+ Text(title)
+ .pokitFont(.b3(.b))
+ .foregroundStyle(.pokit(.text(.secondary)))
+ .multilineTextAlignment(.leading)
+ .lineLimit(2)
+ } else {
+ placeholder(23)
+ }
- Text(url)
- .pokitFont(.detail2)
- .foregroundStyle(.pokit(.text(.tertiary)))
- .multilineTextAlignment(.leading)
- .lineLimit(2)
+ if let url {
+ Text(url)
+ .pokitFont(.detail2)
+ .foregroundStyle(.pokit(.text(.tertiary)))
+ .multilineTextAlignment(.leading)
+ .lineLimit(2)
+ } else {
+ placeholder(55)
+ }
}
.padding(.vertical, 16)
.padding(.trailing, 20)
}
+ @ViewBuilder
+ private func placeholder(_ tLength: CGFloat) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ RoundedRectangle(cornerRadius: 2, style: .continuous)
+ .fill(.pokit(.bg(.secondary)))
+ .frame(height: 16)
+
+ RoundedRectangle(cornerRadius: 2, style: .continuous)
+ .fill(.pokit(.bg(.secondary)))
+ .padding(.trailing, tLength)
+ .frame(height: 16)
+ }
+ }
+
private func buttonTapped() {
- guard let url = URL(string: url) else { return }
+ guard
+ let urlString = url,
+ let url = URL(string: urlString)
+ else { return }
openURL(url)
}
+
+ private func onChangeImageURL(_ imageURL: String?) {
+ guard imageURL != nil else { return }
+ let isError = title == Constants.제목을_입력해주세요_문구
+ UINotificationFeedbackGenerator()
+ .notificationOccurred(isError ? .error : .success)
+ }
+}
+
+#Preview {
+ PokitLinkPreview(title: nil, url: nil, imageURL: nil)
}
diff --git a/Projects/DSKit/Sources/Components/PokitList.swift b/Projects/DSKit/Sources/Components/PokitList.swift
index c3ca1b6d..ac0b3a76 100644
--- a/Projects/DSKit/Sources/Components/PokitList.swift
+++ b/Projects/DSKit/Sources/Components/PokitList.swift
@@ -8,6 +8,7 @@
import SwiftUI
import Util
+import NukeUI
public struct PokitList: View {
@Namespace
@@ -15,15 +16,18 @@ public struct PokitList: View {
private let selectedItem: Item?
private let list: [Item]
+ private let isDisabled: Bool
private let action: (Item) -> Void
public init(
selectedItem: Item?,
list: [Item],
+ isDisabled: Bool = false,
action: @escaping (Item) -> Void
) {
self.selectedItem = selectedItem
self.list = list
+ self.isDisabled = isDisabled
self.action = action
}
@@ -54,29 +58,61 @@ public struct PokitList: View {
Button {
action(item)
} label: {
- HStack {
+ HStack(spacing: 12) {
+ thumbNail(url: item.categoryImage.imageURL)
+
VStack(alignment: .leading, spacing: 4) {
Text(item.categoryName)
.pokitFont(.b1(.b))
- .foregroundStyle(.pokit(.text(.primary)))
+ .foregroundStyle(
+ isDisabled
+ ? .pokit(.text(.disable))
+ : .pokit(.text(.primary))
+ )
Text("링크 \(item.contentCount)개")
.pokitFont(.detail1)
- .foregroundStyle(.pokit(.text(.tertiary)))
+ .foregroundStyle(
+ isDisabled
+ ? .pokit(.text(.disable))
+ : .pokit(.text(.tertiary))
+ )
}
Spacer()
}
- .padding(.leading, 28)
- .padding(.trailing, 20)
- .padding(.vertical, 13)
+ .padding(.vertical, 12)
+ .padding(.horizontal, 20)
.background {
if isSelected {
Color.pokit(.bg(.primary))
.matchedGeometryEffect(id: "SELECT", in: heroEffect)
+ } else {
+ isDisabled
+ ? Color.pokit(.bg(.disable))
+ : Color.pokit(.bg(.base))
}
}
}
.animation(.pokitDissolve, value: isSelected)
+ .disabled(isDisabled)
+ }
+
+ @MainActor
+ private func thumbNail(url: String) -> some View {
+ LazyImage(url: URL(string: url)) { state in
+ Group {
+ if let image = state.image {
+ image
+ .resizable()
+ } else {
+ PokitSpinner()
+ .foregroundStyle(.pokit(.icon(.brand)))
+ .frame(width: 48, height: 48)
+ }
+ }
+ .animation(.pokitDissolve, value: state.image)
+ }
+ .frame(width: 60, height: 60)
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitListButton.swift b/Projects/DSKit/Sources/Components/PokitListButton.swift
new file mode 100644
index 00000000..67ef0dfd
--- /dev/null
+++ b/Projects/DSKit/Sources/Components/PokitListButton.swift
@@ -0,0 +1,138 @@
+//
+// PokitListButton.swift
+// DSKit
+//
+// Created by 김도형 on 12/1/24.
+//
+
+import SwiftUI
+
+public struct PokitListButton: View {
+ @Binding
+ private var isOn: Bool
+
+ private let title: String
+ private let type: PokitListButton.ListButtonType
+ private let action: () -> Void
+
+ public init(
+ title: String,
+ type: PokitListButton.ListButtonType,
+ isOn: Binding = .constant(false),
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.type = type
+ self._isOn = isOn
+ self.action = action
+ }
+
+ public var body: some View {
+ Button(action: action) {
+ label
+ }
+ }
+
+ private var label: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ switch type {
+ case let .default(icon, iconColor),
+ let .bottomSheet(icon, iconColor, _),
+ let .subText(icon, iconColor, _):
+ Text(title)
+ .pokitFont(.b1(.m))
+ .foregroundStyle(.pokit(.text(.secondary)))
+
+ Spacer()
+
+ Image(icon)
+ .resizable()
+ .frame(width: 24, height: 24)
+ .foregroundStyle(iconColor)
+ case .toggle:
+ Toggle(isOn: $isOn) {
+ Text(title)
+ .pokitFont(.b1(.m))
+ .foregroundStyle(.pokit(.text(.secondary)))
+ }
+ .tint(.pokit(.icon(.brand)))
+ }
+
+ }
+
+ if case let .subText(_, _, subeText) = type {
+ Text(subeText)
+ .pokitFont(.detail1)
+ .foregroundStyle(.pokit(.text(.tertiary)))
+ .lineLimit(1)
+ }
+
+ if case let .toggle(subeText) = type {
+ Text(subeText)
+ .pokitFont(.detail1)
+ .foregroundStyle(.pokit(.text(.tertiary)))
+ .lineLimit(1)
+ }
+ }
+ .padding(.horizontal, 24)
+ .padding(.vertical, 20)
+ .background(alignment: .bottom) {
+ if case let .bottomSheet(_, _, isLast) = type, !isLast {
+ Rectangle()
+ .fill(.pokit(.border(.tertiary)))
+ .frame(height: 1)
+ }
+ }
+ }
+}
+
+extension PokitListButton {
+ public enum ListButtonType {
+ case `default`(icon: PokitImage, iconColor: Color)
+ case bottomSheet(icon: PokitImage, iconColor: Color, isLast: Bool = false)
+ case subText(icon: PokitImage, iconColor: Color, subeText: String)
+ case toggle(subeText: String)
+ }
+}
+
+@available(iOS 18.0, *)
+#Preview {
+ @Previewable
+ @State var isOn: Bool = false
+
+ PokitListButton(
+ title: "공지사항",
+ type: .default(
+ icon: .icon(.arrowRight),
+ iconColor: .pokit(.icon(.primary))
+ ),
+ action: { }
+ )
+
+ PokitListButton(
+ title: "공지사항",
+ type: .bottomSheet(
+ icon: .icon(.edit),
+ iconColor: .pokit(.icon(.primary))
+ ),
+ action: { }
+ )
+
+ PokitListButton(
+ title: "공지사항",
+ type: .subText(
+ icon: .icon(.arrowRight),
+ iconColor: .pokit(.icon(.primary)),
+ subeText: "포킷에 저장된 링크가 다른 사용자에게 추천됩니다."
+ ),
+ action: { }
+ )
+
+ PokitListButton(
+ title: "공지사항",
+ type: .toggle(subeText: "포킷에 저장된 링크가 다른 사용자에게 추천됩니다."),
+ isOn: $isOn,
+ action: { }
+ )
+}
diff --git a/Projects/DSKit/Sources/Components/PokitPartTextArea.swift b/Projects/DSKit/Sources/Components/PokitPartTextArea.swift
index 37f91e25..8f2ec0ac 100644
--- a/Projects/DSKit/Sources/Components/PokitPartTextArea.swift
+++ b/Projects/DSKit/Sources/Components/PokitPartTextArea.swift
@@ -10,24 +10,26 @@ import SwiftUI
public struct PokitPartTextArea: View {
@Binding private var text: String
- @State private var state: PokitInputStyle.State
+ @Binding private var state: PokitInputStyle.State
private var focusState: FocusState.Binding
-
+ private let baseState: PokitInputStyle.State
private let equals: Value
private let placeholder: String
private let onSubmit: (() -> Void)?
public init(
text: Binding,
- state: PokitInputStyle.State = .default,
+ state: Binding,
+ baseState: PokitInputStyle.State = .default,
placeholder: String = "내용을 입력해주세요.",
focusState: FocusState.Binding,
equals: Value,
onSubmit: (() -> Void)? = nil
) {
self._text = text
- self._state = State(initialValue: state)
+ self._state = state
+ self.baseState = baseState
self.focusState = focusState
self.equals = equals
self.placeholder = placeholder
@@ -47,7 +49,11 @@ public struct PokitPartTextArea: View {
.foregroundStyle(.pokit(.text(.primary)))
.scrollContentBackground(.hidden)
.focused(focusState, equals: equals)
- .disabled(state == .disable || state == .readOnly)
+ .disabled(
+ state == .disable ||
+ state == .readOnly ||
+ state == .memo(isReadOnly: true)
+ )
.onSubmit {
onSubmit?()
}
@@ -77,7 +83,7 @@ public struct PokitPartTextArea: View {
case .error(message: let message):
state = .error(message: message)
default:
- state = .default
+ state = baseState
}
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitSelect.swift b/Projects/DSKit/Sources/Components/PokitSelect.swift
index 6c322356..2754573c 100644
--- a/Projects/DSKit/Sources/Components/PokitSelect.swift
+++ b/Projects/DSKit/Sources/Components/PokitSelect.swift
@@ -20,13 +20,15 @@ public struct PokitSelect: View {
private let label: String
private let list: [Item]?
private let action: (Item) -> Void
+ private let addAction: (() -> Void)?
public init(
selectedItem: Binding- = .constant(nil),
state: PokitSelect.SelectState = .default,
label: String,
list: [Item]?,
- action: @escaping (Item) -> Void
+ action: @escaping (Item) -> Void,
+ addAction: (() -> Void)?
) {
self._selectedItem = selectedItem
if selectedItem.wrappedValue != nil {
@@ -37,6 +39,7 @@ public struct PokitSelect: View {
self.label = label
self.list = list
self.action = action
+ self.addAction = addAction
}
public var body: some View {
@@ -50,7 +53,7 @@ public struct PokitSelect: View {
listSheet
.presentationDragIndicator(.visible)
.pokitPresentationCornerRadius()
- .presentationDetents([.medium])
+ .presentationDetents([.height(564)])
.pokitPresentationBackground()
}
}
@@ -96,14 +99,23 @@ public struct PokitSelect: View {
private var listSheet: some View {
Group {
if let list {
- PokitList(
- selectedItem: selectedItem,
- list: list
- ) { item in
- action(item)
- listCellTapped(item)
+ VStack(spacing: 0) {
+ if let addAction {
+ addButton {
+ listDismiss()
+ addAction()
+ }
+ }
+
+ PokitList(
+ selectedItem: selectedItem,
+ list: list
+ ) { item in
+ action(item)
+ listCellTapped(item)
+ }
}
- .padding(.top, 36)
+ .padding(.top, 12)
.padding(.bottom, 20)
} else {
PokitLoading()
@@ -111,6 +123,36 @@ public struct PokitSelect: View {
}
}
+ @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
}
@@ -125,6 +167,10 @@ public struct PokitSelect: View {
private func onChangedSeletedItem(_ newValue: Item?) {
state = newValue != nil ? .input : .default
}
+
+ private func listDismiss() {
+ showSheet = false
+ }
}
public extension PokitSelect {
diff --git a/Projects/DSKit/Sources/Components/PokitTextArea.swift b/Projects/DSKit/Sources/Components/PokitTextArea.swift
index ef8eb530..0a02a2f1 100644
--- a/Projects/DSKit/Sources/Components/PokitTextArea.swift
+++ b/Projects/DSKit/Sources/Components/PokitTextArea.swift
@@ -15,9 +15,10 @@ public struct PokitTextArea: View {
private var focusState: FocusState.Binding
+ private let baseState: PokitInputStyle.State
private let errorMessage: String?
private let equals: Value
- private let label: String
+ private let label: String?
private let placeholder: String
private let info: String?
private let maxLetter: Int
@@ -25,8 +26,9 @@ public struct PokitTextArea: View {
public init(
text: Binding,
- label: String,
+ label: String? = nil,
state: Binding,
+ baseState: PokitInputStyle.State = .default,
errorMessage: String? = nil,
placeholder: String = "내용을 입력해주세요.",
info: String? = nil,
@@ -38,6 +40,7 @@ public struct PokitTextArea: View {
self._text = text
self.label = label
self._state = state
+ self.baseState = baseState
self.errorMessage = errorMessage
self.focusState = focusState
self.equals = equals
@@ -49,18 +52,20 @@ public struct PokitTextArea: View {
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
- PokitLabel(text: label, size: .large)
- .padding(.bottom, 8)
+ if let label {
+ PokitLabel(text: label, size: .large)
+ .padding(.bottom, 8)
+ }
PokitPartTextArea(
text: $text,
- state: state,
+ state: $state,
+ baseState: baseState,
placeholder: placeholder,
focusState: focusState,
equals: equals,
onSubmit: onSubmit
)
- .onChange(of: focusState.wrappedValue) { onChangedFocuseState($0) }
.onChange(of: state) { onChangedState($0) }
infoLabel
@@ -94,23 +99,31 @@ public struct PokitTextArea: View {
Spacer()
- Group {
- switch state {
- case .error:
- Text("\(text.count > maxLetter ? maxLetter : text.count)/\(maxLetter)")
- .foregroundStyle(.pokit(.text(.error)))
- default:
- Text("\(text.count > maxLetter ? maxLetter : text.count)/\(maxLetter)")
- .foregroundStyle(.pokit(.text(.tertiary)))
- }
+ if state != .memo(isReadOnly: true) &&
+ state != .memo(isReadOnly: false) {
+ textCount
+ .pokitBlurReplaceTransition(.pokitDissolve)
}
- .pokitFont(.detail1)
- .contentTransition(.numericText())
- .animation(.pokitDissolve, value: text)
}
.padding(.top, 4)
}
+ private var textCount: some View {
+ Group {
+ switch state {
+ case .error:
+ Text("\(text.count > maxLetter ? maxLetter : text.count)/\(maxLetter)")
+ .foregroundStyle(.pokit(.text(.error)))
+ default:
+ Text("\(text.count > maxLetter ? maxLetter : text.count)/\(maxLetter)")
+ .foregroundStyle(.pokit(.text(.tertiary)))
+ }
+ }
+ .pokitFont(.detail1)
+ .contentTransition(.numericText())
+ .animation(.pokitDissolve, value: text)
+ }
+
private func onChangedText(_ newValue: String) {
if isMaxLetters {
self.text = String(newValue.prefix(maxLetter + 1))
@@ -122,19 +135,6 @@ public struct PokitTextArea: View {
state = newValue ? .error(message: "최대 \(maxLetter)자까지 입력가능합니다.") : .active
}
- private func onChangedFocuseState(_ newValue: Value) {
- if newValue == equals {
- state = .active
- } else {
- switch state {
- case .error(message: let message):
- state = .error(message: message)
- default:
- state = .default
- }
- }
- }
-
private func onChangedState(_ newValue: PokitInputStyle.State) {
switch newValue {
case .error:
diff --git a/Projects/DSKit/Sources/Components/PokitTextButton.swift b/Projects/DSKit/Sources/Components/PokitTextButton.swift
index 5620597b..286ff6f0 100644
--- a/Projects/DSKit/Sources/Components/PokitTextButton.swift
+++ b/Projects/DSKit/Sources/Components/PokitTextButton.swift
@@ -39,7 +39,7 @@ public struct PokitTextButton: View {
Text(self.labelText)
.pokitFont(self.size.font)
.foregroundStyle(self.state.textColor)
- .padding(.horizontal, self.hPadding)
+ .padding(.horizontal, self.size.hPadding)
.padding(.vertical, self.size.vPadding)
.background {
RoundedRectangle(cornerRadius: shape.radius(size: self.size), style: .continuous)
@@ -49,16 +49,6 @@ public struct PokitTextButton: View {
.stroke(self.state.backgroundStrokeColor, lineWidth: 1)
}
}
- }
-
- private var hPadding: CGFloat {
- switch self.size {
- case .small:
- return 12.5
- case .medium:
- return 26
- case .large:
- return 34.5
- }
+ .frame(minWidth: self.size.minWidth)
}
}
diff --git a/Projects/DSKit/Sources/Components/PokitTextInput.swift b/Projects/DSKit/Sources/Components/PokitTextInput.swift
index fe9564e7..23875fcd 100644
--- a/Projects/DSKit/Sources/Components/PokitTextInput.swift
+++ b/Projects/DSKit/Sources/Components/PokitTextInput.swift
@@ -15,6 +15,8 @@ public struct PokitTextInput: View {
private var focusState: FocusState.Binding
+ private let type: PokitInputStyle.InputType
+ private let shape: PokitInputStyle.Shape
private let equals: Value
private let label: String?
private let placeholder: String
@@ -25,6 +27,8 @@ public struct PokitTextInput: View {
public init(
text: Binding,
label: String? = nil,
+ type: PokitInputStyle.InputType = .text,
+ shape: PokitInputStyle.Shape,
state: Binding,
placeholder: String = "내용을 입력해주세요.",
info: String? = nil,
@@ -35,6 +39,8 @@ public struct PokitTextInput: View {
) {
self._text = text
self.label = label
+ self.type = type
+ self.shape = shape
self._state = state
self.focusState = focusState
self.equals = equals
@@ -51,7 +57,26 @@ public struct PokitTextInput: View {
.padding(.bottom, 8)
}
- textField
+ HStack(spacing: 8) {
+ if case let .iconL(icon, action) = type {
+ iconButton(icon: icon, action: action)
+ .pokitBlurReplaceTransition(.pokitDissolve)
+ }
+
+ textField
+
+ if case let .iconR(icon, action) = type {
+ iconButton(icon: icon, action: action)
+ .pokitBlurReplaceTransition(.pokitDissolve)
+ }
+ }
+ .padding(.vertical, vPadding)
+ .padding(.leading, lPadding)
+ .padding(.trailing, tPadding)
+ .background(
+ state: self.state,
+ shape: self.shape
+ )
infoLabel
}
@@ -69,12 +94,6 @@ public struct PokitTextInput: View {
.focused(focusState, equals: equals)
.pokitFont(.b3(.m))
.foregroundStyle(.pokit(.text(.secondary)))
- .padding(.vertical, 16)
- .padding(.horizontal, 12)
- .background(
- state: self.state,
- shape: .rectangle
- )
.disabled(state == .disable || state == .readOnly)
.onSubmit {
onSubmit?()
@@ -131,6 +150,61 @@ public struct PokitTextInput: View {
.padding(.top, 4)
}
+ @ViewBuilder
+ private func iconButton(
+ icon: PokitImage,
+ action: (() -> Void)?
+ ) -> some View {
+ Button {
+ if let action {
+ action()
+ } else {
+ onSubmit?()
+ }
+ } label: {
+ Image(icon)
+ .resizable()
+ .frame(width: 24, height: 24)
+ .foregroundStyle(state.iconColor)
+ .animation(.pokitDissolve, value: self.state)
+ }
+ }
+
+ private var vPadding: CGFloat {
+ switch type {
+ case .text: return 16
+ case .iconR, .iconL:
+ switch shape {
+ case .rectangle: return 13
+ case .round: return 8
+ }
+ }
+ }
+
+ private var tPadding: CGFloat {
+ switch type {
+ case .text: return 12
+ case .iconR:
+ switch shape {
+ case .rectangle: return 12
+ case .round: return 20
+ }
+ case .iconL: return 13
+ }
+ }
+
+ private var lPadding: CGFloat {
+ switch type {
+ case .text: return 12
+ case .iconL:
+ switch shape {
+ case .rectangle: return 12
+ case .round: return 20
+ }
+ case .iconR: return 13
+ }
+ }
+
private func onChangedText(_ newValue: String) {
guard let maxLetter else { return }
diff --git a/Projects/DSKit/Sources/Extensions/Color+Extension.swift b/Projects/DSKit/Sources/Extensions/Color+Extension.swift
index e63a8d42..503eb67e 100644
--- a/Projects/DSKit/Sources/Extensions/Color+Extension.swift
+++ b/Projects/DSKit/Sources/Extensions/Color+Extension.swift
@@ -144,7 +144,7 @@ public extension Color {
case .bg(let bg):
switch bg {
case .base: return .pokitColor(.grayScale(.white))
- case .baseIcon: return Color(hue: 0, saturation: 0, brightness: 85, opacity: 0.6)
+ case .baseIcon: return Color(red: 0.85, green: 0.85, blue: 0.85).opacity(0.6)
case .brand: return .pokitColor(.orange(._700))
case .disable: return .pokitColor(.grayScale(._200))
case .error: return .pokitColor(.red(._500))
diff --git a/Projects/DSKit/Sources/Foundation/PokitButtonStyle.swift b/Projects/DSKit/Sources/Foundation/PokitButtonStyle.swift
index 647f504c..3c0e7225 100644
--- a/Projects/DSKit/Sources/Foundation/PokitButtonStyle.swift
+++ b/Projects/DSKit/Sources/Foundation/PokitButtonStyle.swift
@@ -128,6 +128,22 @@ extension PokitButtonStyle.Size {
case .large: return 13
}
}
+
+ var hPadding: CGFloat {
+ switch self {
+ case .small: return 8
+ case .medium: return 16
+ case .large: return 20
+ }
+ }
+
+ var minWidth: CGFloat {
+ switch self {
+ case .small: return 50
+ case .medium: return 80
+ case .large: return 100
+ }
+ }
}
extension PokitButtonStyle.Shape {
diff --git a/Projects/DSKit/Sources/Foundation/PokitImage.swift b/Projects/DSKit/Sources/Foundation/PokitImage.swift
index e83d4aec..16354ee4 100644
--- a/Projects/DSKit/Sources/Foundation/PokitImage.swift
+++ b/Projects/DSKit/Sources/Foundation/PokitImage.swift
@@ -79,6 +79,30 @@ public enum PokitImage {
return DSKitAsset.iconGoogle.swiftUIImage
case .spinner:
return DSKitAsset.iconSpinner.swiftUIImage
+ case .invite:
+ return DSKitAsset.iconInvite.swiftUIImage
+ case .memo:
+ return DSKitAsset.iconMemo.swiftUIImage
+ case .arrowDown2:
+ return DSKitAsset.iconArrowDown2.swiftUIImage
+ case .hashtag:
+ return DSKitAsset.iconHashtag.swiftUIImage
+ case .savePokit:
+ return DSKitAsset.iconSavepokit.swiftUIImage
+ case .pin:
+ return DSKitAsset.iconPin.swiftUIImage
+ case .member:
+ return DSKitAsset.iconMember.swiftUIImage
+ case .lock:
+ return DSKitAsset.iconLock.swiftUIImage
+ case .movePokit:
+ return DSKitAsset.iconMovepokit.swiftUIImage
+ case .allCheck:
+ return DSKitAsset.iconAllcheck.swiftUIImage
+ case .allUncheck:
+ return DSKitAsset.iconAlluncheck.swiftUIImage
+ case .report:
+ return DSKitAsset.iconReport.swiftUIImage
}
case .logo(let name):
switch name {
@@ -138,6 +162,18 @@ public extension PokitImage {
case check
case google
case spinner
+ case invite
+ case memo
+ case arrowDown2
+ case hashtag
+ case savePokit
+ case pin
+ case member
+ case lock
+ case movePokit
+ case allCheck
+ case allUncheck
+ case report
}
enum Logo {
diff --git a/Projects/DSKit/Sources/Foundation/PokitInputStyle.swift b/Projects/DSKit/Sources/Foundation/PokitInputStyle.swift
index 9c5fb34d..41b32aec 100644
--- a/Projects/DSKit/Sources/Foundation/PokitInputStyle.swift
+++ b/Projects/DSKit/Sources/Foundation/PokitInputStyle.swift
@@ -15,6 +15,7 @@ public enum PokitInputStyle: Equatable {
case disable
case readOnly
case error(message: String)
+ case memo(isReadOnly: Bool)
var infoColor: Color {
switch self {
@@ -28,23 +29,27 @@ public enum PokitInputStyle: Equatable {
var backgroundColor: Color {
switch self {
case .default, .input, .active, .error:
- return .pokit(.bg(.primary))
+ return .pokit(.bg(.base))
case .disable:
return .pokit(.bg(.disable))
case .readOnly:
return .pokit(.bg(.secondary))
+ case let .memo(isReadOnly):
+ return isReadOnly
+ ? .pokit(.bg(.primary))
+ : Color(red: 1, green: 0.96, blue: 0.89)
}
}
var backgroundStrokeColor: Color {
switch self {
- case .default, .input:
+ case .input, .memo:
return .clear
case .active:
return .pokit(.border(.brand))
case .disable:
return .pokit(.border(.disable))
- case .readOnly:
+ case .readOnly, .default:
return .pokit(.border(.secondary))
case .error:
return .pokit(.border(.error))
@@ -53,7 +58,7 @@ public enum PokitInputStyle: Equatable {
var iconColor: Color {
switch self {
- case .default, .readOnly:
+ case .default, .readOnly, .memo:
return .pokit(.icon(.secondary))
case .input, .active:
return .pokit(.icon(.primary))
@@ -78,4 +83,16 @@ public enum PokitInputStyle: Equatable {
}
}
}
+
+ public enum InputType {
+ case text
+ case iconR(
+ icon: PokitImage,
+ action: (() -> Void)? = nil
+ )
+ case iconL(
+ icon: PokitImage,
+ action: (() -> Void)? = nil
+ )
+ }
}
diff --git a/Projects/DSKit/Sources/Modifiers/PokitInputModifier.swift b/Projects/DSKit/Sources/Modifiers/PokitInputModifier.swift
index 86d786bb..50d92009 100644
--- a/Projects/DSKit/Sources/Modifiers/PokitInputModifier.swift
+++ b/Projects/DSKit/Sources/Modifiers/PokitInputModifier.swift
@@ -20,16 +20,13 @@ struct PokitInputModifier: ViewModifier {
}
func body(content: Content) -> some View {
- let backgroundColor = state == .active ? .pokit(.bg(.base)) : self.state.backgroundColor
- let backgroundStrokeColor = state == .active ? .pokit(.border(.brand)) : self.state.backgroundStrokeColor
-
content
.background {
RoundedRectangle(cornerRadius: shape.radius, style: .continuous)
- .fill(backgroundColor)
+ .fill(self.state.backgroundColor)
.overlay {
RoundedRectangle(cornerRadius: shape.radius, style: .continuous)
- .stroke(backgroundStrokeColor, lineWidth: 1)
+ .stroke(self.state.backgroundStrokeColor, lineWidth: 1)
}
}
.animation(.pokitDissolve, value: state)
diff --git a/Projects/Domain/Sources/Base/BaseContentDetail.swift b/Projects/Domain/Sources/Base/BaseContentDetail.swift
index 9a59096d..8e8fef34 100644
--- a/Projects/Domain/Sources/Base/BaseContentDetail.swift
+++ b/Projects/Domain/Sources/Base/BaseContentDetail.swift
@@ -12,7 +12,7 @@ public struct BaseContentDetail: Equatable {
public let category: BaseCategoryInfo
public let title: String
public let data: String
- public let memo: String
+ public var memo: String
public let createdAt: String
public var favorites: Bool?
public var alertYn: RemindState
diff --git a/Projects/Domain/Sources/Base/BaseContentItem.swift b/Projects/Domain/Sources/Base/BaseContentItem.swift
index 59c93b12..d630964d 100644
--- a/Projects/Domain/Sources/Base/BaseContentItem.swift
+++ b/Projects/Domain/Sources/Base/BaseContentItem.swift
@@ -14,31 +14,37 @@ public struct BaseContentItem: Identifiable, Equatable, PokitLinkCardItem, Sorta
public let categoryName: String
public let categoryId: Int
public let title: String
+ public var memo: String?
public var thumbNail: String
public let data: String
public let domain: String
public let createdAt: String
public let isRead: Bool?
+ public var isFavorite: Bool?
public init(
id: Int,
categoryName: String,
categoryId: Int,
title: String,
+ memo: String?,
thumbNail: String,
data: String,
domain: String,
createdAt: String,
- isRead: Bool?
+ isRead: Bool?,
+ isFavorite: Bool?
) {
self.id = id
self.categoryName = categoryName
self.categoryId = categoryId
self.title = title
+ self.memo = memo
self.thumbNail = thumbNail
self.data = data
self.domain = domain
self.createdAt = createdAt
self.isRead = isRead
+ self.isFavorite = isFavorite
}
}
diff --git a/Projects/Domain/Sources/CategorySharing/CategorySharing.swift b/Projects/Domain/Sources/CategorySharing/CategorySharing.swift
index f60d91aa..da404c8d 100644
--- a/Projects/Domain/Sources/CategorySharing/CategorySharing.swift
+++ b/Projects/Domain/Sources/CategorySharing/CategorySharing.swift
@@ -63,10 +63,11 @@ extension CategorySharing {
public let data: String
public let domain: String
public let title: String
- public let memo: String
+ public let memo: String?
public let thumbNail: String
public let createdAt: String
public let categoryName: String
public let isRead: Bool? = false
+ public let isFavorite: Bool? = false
}
}
diff --git a/Projects/Domain/Sources/DTO/Base/BaseContentResponse+Extension.swift b/Projects/Domain/Sources/DTO/Base/BaseContentResponse+Extension.swift
index d195655e..e67218ed 100644
--- a/Projects/Domain/Sources/DTO/Base/BaseContentResponse+Extension.swift
+++ b/Projects/Domain/Sources/DTO/Base/BaseContentResponse+Extension.swift
@@ -17,11 +17,13 @@ public extension ContentBaseResponse {
categoryName: self.category.categoryName,
categoryId: self.category.categoryId,
title: self.title,
+ memo: self.memo,
thumbNail: self.thumbNail,
data: self.data,
domain: self.domain,
createdAt: self.createdAt,
- isRead: self.isRead
+ isRead: self.isRead,
+ isFavorite: self.isFavorite
)
}
}
diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
index 5de0c984..c9ce9e5d 100644
--- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
+++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
@@ -53,9 +53,6 @@ public struct CategoryDetailFeature {
return identifiedArray
}
var contents: IdentifiedArrayOf = []
- var kebobSelectedType: PokitDeleteBottomSheet.SheetType?
- var selectedContentItem: BaseContentItem?
- var shareSheetItem: BaseContentItem? = nil
/// sheet Presented
var isCategorySheetPresented: Bool = false
var isCategorySelectSheetPresented: Bool = false
@@ -86,13 +83,11 @@ public struct CategoryDetailFeature {
case binding(BindingAction)
case dismiss
case pagenation
- case 카테고리_케밥_버튼_눌렀을때(PokitDeleteBottomSheet.SheetType, selectedItem: BaseContentItem?)
+ case 카테고리_케밥_버튼_눌렀을때
case 카테고리_선택_버튼_눌렀을때
case 카테고리_선택했을때(BaseCategoryItem)
case 필터_버튼_눌렀을때
- case 컨텐츠_항목_눌렀을때(BaseContentItem)
case 뷰가_나타났을때
- case 링크_공유_완료되었을때
}
public enum InnerAction: Equatable {
@@ -102,7 +97,6 @@ public struct CategoryDetailFeature {
case 카테고리_목록_조회_API_반영(BaseCategoryListInquiry)
case 카테고리_내_컨텐츠_목록_조회_API_반영(BaseContentListInquiry)
- case 컨텐츠_삭제_API_반영(id: Int)
case pagenation_API_반영(BaseContentListInquiry)
case pagenation_초기화
}
@@ -110,7 +104,6 @@ public struct CategoryDetailFeature {
public enum AsyncAction: Equatable {
case 카테고리_내_컨텐츠_목록_조회_API
case 카테고리_목록_조회_API
- case 컨텐츠_삭제_API(id: Int)
case 페이징_재조회
case 클립보드_감지
}
@@ -181,9 +174,7 @@ private extension CategoryDetailFeature {
case .binding:
return .none
- case let .카테고리_케밥_버튼_눌렀을때(selectedType, selectedItem):
- state.kebobSelectedType = selectedType
- state.selectedContentItem = selectedItem
+ case .카테고리_케밥_버튼_눌렀을때:
return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
case .카테고리_선택_버튼_눌렀을때:
@@ -201,9 +192,6 @@ private extension CategoryDetailFeature {
state.isFilterSheetPresented.toggle()
return .none
- case .컨텐츠_항목_눌렀을때(let selectedItem):
- return .run { send in await send(.delegate(.contentItemTapped(selectedItem))) }
-
case .dismiss:
return .run { _ in await dismiss() }
@@ -217,10 +205,6 @@ private extension CategoryDetailFeature {
case .pagenation:
state.domain.pageable.page += 1
return .send(.async(.카테고리_내_컨텐츠_목록_조회_API))
-
- case .링크_공유_완료되었을때:
- state.shareSheetItem = nil
- return .none
}
}
@@ -241,9 +225,11 @@ private extension CategoryDetailFeature {
case let .카테고리_목록_조회_API_반영(response):
state.domain.categoryListInQuiry = response
- guard let first = response.data?.first(where: { item in
- item.id == state.domain.category.id
- }) else { return .none }
+ guard
+ let first = response.data?.first(where: { item in
+ item.id == state.domain.category.id
+ })
+ else { return .none }
state.domain.category = first
return .none
@@ -257,14 +243,6 @@ private extension CategoryDetailFeature {
state.isLoading = false
return .none
- case let .컨텐츠_삭제_API_반영(id):
- state.domain.contentList.data?.removeAll { $0.id == id }
- state.contents.removeAll { $0.content.id == id }
- state.domain.category.contentCount -= 1
- state.selectedContentItem = nil
- state.isPokitDeleteSheetPresented = false
- state.kebobSelectedType = nil
- return .none
case .pagenation_API_반영(let contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
@@ -313,12 +291,6 @@ private extension CategoryDetailFeature {
: await send(.inner(.pagenation_API_반영(contentList)))
}
- case let .컨텐츠_삭제_API(contentId):
- return .run { send in
- let _ = try await contentClient.컨텐츠_삭제("\(contentId)")
- await send(.inner(.컨텐츠_삭제_API_반영(id: contentId)), animation: .pokitSpring)
- }
-
case .페이징_재조회:
return .run { [
pageable = state.domain.pageable,
@@ -376,41 +348,20 @@ private extension CategoryDetailFeature {
case .categoryBottomSheet(let delegateAction):
switch delegateAction {
case .shareCellButtonTapped:
- switch state.kebobSelectedType {
- case .링크삭제:
- state.shareSheetItem = state.selectedContentItem
- case .포킷삭제:
- kakaoShareClient.카테고리_카카오톡_공유(
- CategoryKaKaoShareModel(
- categoryName: state.domain.category.categoryName,
- categoryId: state.domain.category.id,
- imageURL: state.domain.category.categoryImage.imageURL
- )
+ kakaoShareClient.카테고리_카카오톡_공유(
+ CategoryKaKaoShareModel(
+ categoryName: state.domain.category.categoryName,
+ categoryId: state.domain.category.id,
+ imageURL: state.domain.category.categoryImage.imageURL
)
- default: return .none
- }
-
+ )
state.isCategorySheetPresented = false
return .none
-
case .editCellButtonTapped:
- return .run { [
- content = state.selectedContentItem,
- type = state.kebobSelectedType,
- category = state.category
- ] send in
- guard let type else { return }
- switch type {
- case .링크삭제:
- guard let content else { return }
- await send(.inner(.카테고리_시트_활성화(false)))
- await send(.delegate(.링크수정(contentId: content.id)))
- case .포킷삭제:
- await send(.inner(.카테고리_시트_활성화(false)))
- await send(.delegate(.포킷수정(category)))
- }
+ return .run { [category = state.category] send in
+ await send(.inner(.카테고리_시트_활성화(false)))
+ await send(.delegate(.포킷수정(category)))
}
-
case .deleteCellButtonTapped:
return .run { send in
await send(.inner(.카테고리_시트_활성화(false)))
@@ -426,27 +377,11 @@ private extension CategoryDetailFeature {
return .run { send in await send(.inner(.카테고리_삭제_시트_활성화(false))) }
case .deleteButtonTapped:
- guard let selectedType = state.kebobSelectedType else {
- /// 🚨 Error Case [1]: 해당 타입의 항목을 삭제하려는데 선택한 `타입`이 없을 때
- state.isPokitDeleteSheetPresented = false
- return .none
- }
- switch selectedType {
- case .링크삭제:
- guard let selectedItem = state.selectedContentItem else {
- /// 🚨 Error Case [1]: 링크 타입의 항목을 삭제하려는데 선택한 `링크항목`이 없을 때
- state.isPokitDeleteSheetPresented = false
- return .none
- }
- return .send(.async(.컨텐츠_삭제_API(id: selectedItem.id)))
- case .포킷삭제:
- state.isPokitDeleteSheetPresented = false
- state.kebobSelectedType = nil
- return .run { [categoryId = state.domain.category.id] send in
- await send(.inner(.카테고리_삭제_시트_활성화(false)))
- await send(.delegate(.포킷삭제))
- try await categoryClient.카테고리_삭제(categoryId)
- }
+ state.isPokitDeleteSheetPresented = false
+ return .run { [categoryId = state.domain.category.id] send in
+ await send(.inner(.카테고리_삭제_시트_활성화(false)))
+ await send(.delegate(.포킷삭제))
+ try await categoryClient.카테고리_삭제(categoryId)
}
}
/// - 필터 버튼을 눌렀을 때
@@ -469,12 +404,8 @@ private extension CategoryDetailFeature {
)
}
- case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_눌렀을때(content)))):
- return .send(.delegate(.contentItemTapped(content)))
case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content)))):
- state.kebobSelectedType = .링크삭제
- state.selectedContentItem = content
- return .send(.inner(.카테고리_시트_활성화(true)))
+ return .send(.delegate(.contentItemTapped(content)))
case .contents:
return .none
}
diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
index 50eb73e7..5c20db9f 100644
--- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
+++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
@@ -42,15 +42,6 @@ public extension CategoryDetailView {
delegateSend: { store.send(.scope(.categoryBottomSheet($0))) }
)
}
- .sheet(item: $store.shareSheetItem) { content in
- if let shareURL = URL(string: content.data) {
- PokitShareSheet(
- items: [shareURL],
- completion: { send(.링크_공유_완료되었을때) }
- )
- .presentationDetents([.medium, .large])
- }
- }
.sheet(isPresented: $store.isCategorySelectSheetPresented) {
if let categories = store.categories {
PokitCategorySheet(
@@ -66,7 +57,7 @@ public extension CategoryDetailView {
}
.sheet(isPresented: $store.isPokitDeleteSheetPresented) {
PokitDeleteBottomSheet(
- type: store.kebobSelectedType ?? .포킷삭제,
+ type: .포킷삭제,
delegateSend: { store.send(.scope(.categoryDeleteBottomSheet($0))) }
)
}
@@ -95,7 +86,7 @@ private extension CategoryDetailView {
PokitHeaderItems(placement: .trailing) {
PokitToolbarButton(
.icon(.kebab),
- action: { send(.카테고리_케밥_버튼_눌렀을때(.포킷삭제, selectedItem: nil)) }
+ action: { send(.카테고리_케밥_버튼_눌렀을때) }
)
}
}
@@ -149,13 +140,14 @@ private extension CategoryDetailView {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(
- store.scope(state: \.contents, action: \.contents)
+ Array(store.scope(state: \.contents, action: \.contents))
) { store in
let isFirst = store.state.id == self.store.contents.first?.id
let isLast = store.state.id == self.store.contents.last?.id
ContentCardView(
store: store,
+ type: .linkList,
isFirst: isFirst,
isLast: isLast
)
diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
index b6413230..4113db9f 100644
--- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
+++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
@@ -87,6 +87,7 @@ public struct PokitCategorySettingFeature {
case 프로필_설정_버튼_눌렀을때
case 저장_버튼_눌렀을때
case 뷰가_나타났을때
+ case 포킷명지우기_버튼_눌렀을때
}
public enum InnerAction: Equatable {
@@ -215,6 +216,9 @@ private extension PokitCategorySettingFeature {
.send(.async(.프로필_목록_조회_API)),
.send(.async(.클립보드_감지))
)
+ case .포킷명지우기_버튼_눌렀을때:
+ state.domain.categoryName = ""
+ return .none
}
}
diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
index e0244982..739291c7 100644
--- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
+++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
@@ -121,8 +121,14 @@ private extension PokitCategorySettingView {
Text("포킷명")
.pokitFont(.b2(.m))
.foregroundStyle(.pokit(.text(.secondary)))
+
PokitTextInput(
text: $store.categoryName,
+ type: store.categoryName.isEmpty ? .text : .iconR(
+ icon: .icon(.x),
+ action: { send(.포킷명지우기_버튼_눌렀을때) }
+ ),
+ shape: .rectangle,
state: $store.pokitNameTextInpuState,
placeholder: "포킷명을 입력해주세요.",
maxLetter: 10,
diff --git a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
index 9f97170e..fb4d1b5b 100644
--- a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
+++ b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
@@ -152,11 +152,13 @@ private extension CategorySharingFeature {
categoryName: content.categoryName,
categoryId: state.category.categoryId,
title: content.title,
+ memo: content.memo,
thumbNail: content.thumbNail,
data: content.data,
domain: content.domain,
createdAt: content.createdAt,
- isRead: content.isRead
+ isRead: content.isRead,
+ isFavorite: content.isFavorite
)))
}
state.isLoading = false
@@ -176,11 +178,13 @@ private extension CategorySharingFeature {
categoryName: content.categoryName,
categoryId: state.category.categoryId,
title: content.title,
+ memo: content.memo,
thumbNail: content.thumbNail,
data: content.data,
domain: content.domain,
createdAt: content.createdAt,
- isRead: content.isRead
+ isRead: content.isRead,
+ isFavorite: content.isFavorite
)))
}
state.isLoading = false
@@ -220,16 +224,6 @@ private extension CategorySharingFeature {
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
switch action {
- case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_눌렀을때(content)))):
- let sharedContent = state.domain.sharedCategory.contentList.data.first { item in
- item.id == content.id
- }
- guard let sharedContent else { return .none }
-
- return .send(.delegate(.컨텐츠_아이템_클릭(
- categoryId: state.category.categoryId,
- content: sharedContent
- )))
case .contents:
return .none
}
diff --git a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
index 25b8502c..d1bb4240 100644
--- a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
+++ b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
@@ -107,6 +107,7 @@ private extension CategorySharingView {
ContentCardView(
store: store,
+ type: .linkList,
isFirst: isFirst,
isLast: isLast
)
diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift
index e2480df9..5255b2b1 100644
--- a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift
+++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift
@@ -4,11 +4,12 @@
//
// Created by 김도형 on 11/17/24.
-import Foundation
+import SwiftUI
import ComposableArchitecture
import Domain
import CoreKit
+import DSKit
import Util
@Reducer
@@ -16,6 +17,10 @@ public struct ContentCardFeature {
/// - Dependency
@Dependency(SwiftSoupClient.self)
private var swiftSoupClient
+ @Dependency(\.openURL)
+ private var openURL
+ @Dependency(ContentClient.self)
+ private var contentClient
/// - State
@ObservableState
public struct State: Equatable, Identifiable {
@@ -39,21 +44,25 @@ public struct ContentCardFeature {
public enum View: Equatable {
case 컨텐츠_항목_눌렀을때
case 컨텐츠_항목_케밥_버튼_눌렀을때
+ case 즐겨찾기_버튼_눌렀을때
case 메타데이터_조회
}
public enum InnerAction: Equatable {
case 메타데이터_조회_수행_반영(String)
+ case 즐겨찾기_API_반영(Bool)
}
public enum AsyncAction: Equatable {
case 메타데이터_조회_수행
+ case 즐겨찾기_API
+ case 즐겨찾기_취소_API
+ case 썸네일_수정_API
}
public enum ScopeAction: Equatable { case doNothing }
public enum DelegateAction: Equatable {
- case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
}
}
@@ -97,11 +106,23 @@ private extension ContentCardFeature {
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
case .컨텐츠_항목_눌렀을때:
- return .send(.delegate(.컨텐츠_항목_눌렀을때(content: state.content)))
+ guard let url = URL(string: state.content.data) else {
+ return .none
+ }
+ return .run { _ in await openURL(url) }
case .컨텐츠_항목_케밥_버튼_눌렀을때:
return .send(.delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content: state.content)))
case .메타데이터_조회:
return .send(.async(.메타데이터_조회_수행))
+ case .즐겨찾기_버튼_눌렀을때:
+ guard let isFavorite = state.content.isFavorite else {
+ return .none
+ }
+ UIImpactFeedbackGenerator(style: .light)
+ .impactOccurred()
+ return isFavorite
+ ? .send(.async(.즐겨찾기_취소_API))
+ : .send(.async(.즐겨찾기_API))
}
}
@@ -110,6 +131,9 @@ private extension ContentCardFeature {
switch action {
case let .메타데이터_조회_수행_반영(imageURL):
state.content.thumbNail = imageURL
+ return .send(.async(.썸네일_수정_API))
+ case .즐겨찾기_API_반영(let favorite):
+ state.content.isFavorite = favorite
return .none
}
}
@@ -126,6 +150,25 @@ private extension ContentCardFeature {
guard let imageURL else { return }
await send(.inner(.메타데이터_조회_수행_반영(imageURL)))
}
+ case .즐겨찾기_API:
+ return .run { [id = state.content.id] send in
+ let _ = try await contentClient.즐겨찾기("\(id)")
+ await send(.inner(.즐겨찾기_API_반영(true)), animation: .pokitDissolve)
+ }
+ case .즐겨찾기_취소_API:
+ return .run { [id = state.content.id] send in
+ try await contentClient.즐겨찾기_취소("\(id)")
+ await send(.inner(.즐겨찾기_API_반영(false)), animation: .pokitDissolve)
+ }
+ case .썸네일_수정_API:
+ return .run { [content = state.content] _ in
+ let request = ThumbnailRequest(thumbnail: content.thumbNail)
+
+ try await contentClient.썸네일_수정(
+ contentId: "\(content.id)",
+ model: request
+ )
+ }
}
}
diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift
index 086a4be0..a3b8cca2 100644
--- a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift
+++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift
@@ -14,16 +14,19 @@ import DSKit
public struct ContentCardView: View {
/// - Properties
public var store: StoreOf
+ private let type: PokitLinkCard.CardType
private let isFirst: Bool
private let isLast: Bool
/// - Initializer
public init(
store: StoreOf,
+ type: PokitLinkCard.CardType = .accept,
isFirst: Bool = false,
isLast: Bool = false
) {
self.store = store
+ self.type = type
self.isFirst = isFirst
self.isLast = isLast
}
@@ -34,11 +37,15 @@ public extension ContentCardView {
WithPerceptionTracking {
PokitLinkCard(
link: store.content,
+ state: isFirst
+ ? .top
+ : isLast ? .bottom : .middle,
+ type: type,
action: { send(.컨텐츠_항목_눌렀을때) },
kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때) },
- fetchMetaData: { send(.메타데이터_조회) }
+ fetchMetaData: { send(.메타데이터_조회) },
+ favoriteAction: { send(.즐겨찾기_버튼_눌렀을때) }
)
- .divider(isFirst: isFirst, isLast: isLast)
}
}
}
@@ -55,11 +62,13 @@ private extension ContentCardView {
categoryName: "미분류",
categoryId: 992 ,
title: "youtube",
+ memo: nil,
thumbNail: "https://i.ytimg.com/vi/NnOC4_kH0ok/hqdefault.jpg?sqp=-oaymwEjCNACELwBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLDN6u6mTjbaVmRZ4biJS_aDq4uvAQ",
data: "https://www.youtube.com/watch?v=wtSwdGJzQCQ",
domain: "신서유기",
createdAt: "2024.08.08",
- isRead: false
+ isRead: false,
+ isFavorite: true
)),
reducer: { ContentCardFeature() }
)
diff --git a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
index e6f39e3e..af6ef08e 100644
--- a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
+++ b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
@@ -40,12 +40,13 @@ public struct ContentDetailFeature {
var contentId: Int? {
get { domain.contentId }
}
-
+ var memo: String = ""
var linkTitle: String? = nil
var linkImageURL: String? = nil
var showAlert: Bool = false
- var showLinkPreview = false
var showShareSheet: Bool = false
+ var memoTextAreaState: PokitInputStyle.State = .memo(isReadOnly: true)
+ var linkPopup: PokitLinkPopup.PopupType?
}
/// - Action
@@ -68,18 +69,18 @@ public struct ContentDetailFeature {
case 삭제_버튼_눌렀을때
case 삭제확인_버튼_눌렀을때
case 즐겨찾기_버튼_눌렀을때
+ case 키보드_취소_버튼_눌렀을때
+ case 키보드_완료_버튼_눌렀울때
+
case 경고시트_해제
case 링크_공유_완료되었을때
}
public enum InnerAction: Equatable {
- case linkPreview
- case 메타데이터_조회_수행(url: URL)
- case 메타데이터_조회_반영(title: String?, imageURL: String?)
- case URL_유효성_확인
case 컨텐츠_상세_조회_API_반영(content: BaseContentDetail)
case 즐겨찾기_API_반영(Bool)
+ case 링크팝업_활성화(PokitLinkPopup.PopupType)
}
public enum AsyncAction: Equatable {
@@ -87,6 +88,7 @@ public struct ContentDetailFeature {
case 즐겨찾기_API(id: Int)
case 즐겨찾기_취소_API(id: Int)
case 컨텐츠_삭제_API(id: Int)
+ case 컨텐츠_수정_API
}
public enum ScopeAction: Equatable { case 없음 }
@@ -129,6 +131,7 @@ public struct ContentDetailFeature {
/// - Reducer body
public var body: some ReducerOf {
+ BindingReducer(action: \.view)
Reduce(self.core)
}
}
@@ -138,14 +141,16 @@ private extension ContentDetailFeature {
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
case .뷰가_나타났을때:
- if let content = state.content {
- state.domain.content = content
- return .send(.inner(.URL_유효성_확인))
- } else if let id = state.domain.contentId {
+ /// - 나중에 공유 받은 컨텐츠인지 확인해야함
+ state.memoTextAreaState = .memo(isReadOnly: false)
+ if let id = state.domain.contentId {
return .send(.async(.컨텐츠_상세_조회_API(id: id)))
- } else {
+ }
+ if let content = state.domain.content {
+ state.memo = content.memo
return .none
}
+ return .none
case .공유_버튼_눌렀을때:
state.showShareSheet = true
return .none
@@ -163,10 +168,10 @@ private extension ContentDetailFeature {
case .binding:
return .none
case .즐겨찾기_버튼_눌렀을때:
- guard let content = state.domain.content,
- let favorites = state.domain.content?.favorites else {
- return .none
- }
+ guard
+ let content = state.domain.content,
+ let favorites = state.domain.content?.favorites
+ else { return .none }
return favorites
? .send(.async(.즐겨찾기_취소_API(id: content.id)))
: .send(.async(.즐겨찾기_API(id: content.id)))
@@ -176,47 +181,29 @@ private extension ContentDetailFeature {
case .경고시트_해제:
state.showAlert = false
return .none
+ case .키보드_취소_버튼_눌렀을때:
+ state.memo = state.domain.content?.memo ?? ""
+ return .none
+ case .키보드_완료_버튼_눌렀울때:
+ let memo = state.memo
+ guard memo != state.domain.content?.memo else { return .none }
+ state.domain.content?.memo = memo
+ return .send(.async(.컨텐츠_수정_API))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .메타데이터_조회_수행(url: let url):
- return .run { send in
- /// - 링크에 대한 메타데이터의 제목 및 썸네일 항목 파싱
- async let title = swiftSoup.parseOGTitle(url)
- async let imageURL = swiftSoup.parseOGImageURL(url)
- try await send(
- .inner(.메타데이터_조회_반영(title: title, imageURL: imageURL)),
- animation: .pokitDissolve
- )
- }
- case let .메타데이터_조회_반영(title: title, imageURL: imageURL):
- state.linkTitle = title
- state.linkImageURL = imageURL
- return .send(.inner(.linkPreview), animation: .pokitDissolve)
- case .URL_유효성_확인:
- guard let urlString = state.domain.content?.data,
- let url = URL(string: urlString) else {
- /// 🚨 Error Case [1]: 올바른 링크가 아닐 때
- state.showLinkPreview = false
- state.linkTitle = nil
- state.linkImageURL = nil
- return .none
- }
- return .send(.inner(.메타데이터_조회_수행(url: url)), animation: .pokitDissolve)
case .컨텐츠_상세_조회_API_반영(content: let content):
state.domain.content = content
- return .merge(
- .send(.delegate(.컨텐츠_조회_완료)),
- .send(.inner(.URL_유효성_확인))
- )
+ state.memo = state.domain.content?.memo ?? ""
+ return .send(.delegate(.컨텐츠_조회_완료))
case .즐겨찾기_API_반영(let favorite):
state.domain.content?.favorites = favorite
return .send(.delegate(.즐겨찾기_갱신_완료))
- case .linkPreview:
- state.showLinkPreview = true
+ case let .링크팝업_활성화(type):
+ state.linkPopup = type
return .none
}
}
@@ -227,17 +214,20 @@ private extension ContentDetailFeature {
case .컨텐츠_상세_조회_API(id: let id):
return .run { send in
let contentResponse = try await contentClient.컨텐츠_상세_조회("\(id)").toDomain()
- await send(.inner(.컨텐츠_상세_조회_API_반영(content: contentResponse)))
+ await send(
+ .inner(.컨텐츠_상세_조회_API_반영(content: contentResponse)),
+ animation: .pokitDissolve
+ )
}
case .즐겨찾기_API(id: let id):
return .run { send in
let _ = try await contentClient.즐겨찾기("\(id)")
- await send(.inner(.즐겨찾기_API_반영(true)))
+ await send(.inner(.즐겨찾기_API_반영(true)), animation: .pokitDissolve)
}
case .즐겨찾기_취소_API(id: let id):
return .run { send in
try await contentClient.즐겨찾기_취소("\(id)")
- await send(.inner(.즐겨찾기_API_반영(false)))
+ await send(.inner(.즐겨찾기_API_반영(false)), animation: .pokitDissolve)
}
case .컨텐츠_삭제_API(id: let id):
return .run { send in
@@ -245,6 +235,37 @@ private extension ContentDetailFeature {
await send(.delegate(.컨텐츠_삭제_완료))
await dismiss()
}
+ case .컨텐츠_수정_API:
+ guard
+ let content = state.domain.content,
+ let url = URL(string: content.data)
+ else { return .none }
+ return .run { send in
+ let imageURL = try? await swiftSoup.parseOGImageURL(url)
+
+ let request = ContentBaseRequest(
+ data: content.data,
+ title: content.title,
+ categoryId: content.category.categoryId,
+ memo: content.memo,
+ alertYn: content.alertYn.rawValue,
+ thumbNail: imageURL
+ )
+ let _ = try await contentClient.컨텐츠_수정(
+ contentId: "\(content.id)",
+ model: request
+ )
+ await send(
+ .inner(.링크팝업_활성화(.success(title: Constants.메모_수정_완료_문구))),
+ animation: .pokitSpring
+ )
+ } catch: { error, send in
+ guard let errorResponse = error as? ErrorResponse else { return }
+ await send(
+ .inner(.링크팝업_활성화(.error(title: errorResponse.message))),
+ animation: .pokitSpring
+ )
+ }
}
}
diff --git a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
index c049946f..b0247281 100644
--- a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
+++ b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
@@ -15,7 +15,8 @@ public struct ContentDetailView: View {
/// - Properties
@Perception.Bindable
public var store: StoreOf
-
+ @FocusState
+ private var isFocused: Bool
/// - Initializer
public init(store: StoreOf) {
self.store = store
@@ -26,17 +27,20 @@ public extension ContentDetailView {
var body: some View {
WithPerceptionTracking {
VStack(spacing: 0) {
- if let content = store.content {
+ if let content = store.content,
+ let favorites = content.favorites {
title(content: content)
ScrollView {
- VStack {
- contentLinkPreview(content: content)
- .padding(.vertical, 24)
+ VStack(spacing: 0) {
+ contentMemo
+
+ Rectangle()
+ .foregroundStyle(.pokit(.border(.tertiary)))
+ .frame(height: 1)
+
+ bottomList(favorites: favorites)
}
}
- .overlay(alignment: .bottom) {
- bottomToolbar(content: content)
- }
} else {
PokitLoading()
}
@@ -47,7 +51,12 @@ public extension ContentDetailView {
.pokitPresentationBackground()
.pokitPresentationCornerRadius()
.presentationDragIndicator(.visible)
- .presentationDetents([.medium, .large])
+ .presentationDetents([.height(588), .large])
+ .overlay(alignment: .bottom) {
+ if store.linkPopup != nil {
+ PokitLinkPopup(type: $store.linkPopup)
+ }
+ }
.sheet(isPresented: $store.showAlert) {
PokitAlert(
"링크를 정말 삭제하시겠습니까?",
@@ -90,7 +99,7 @@ private extension ContentDetailView {
}
}
- PokitBadge(content.category.categoryName, state: .default)
+ PokitBadge(state: .default(content.category.categoryName))
Spacer()
}
@@ -100,8 +109,6 @@ private extension ContentDetailView {
func title(content: BaseContentDetail) -> some View {
VStack(alignment: .leading, spacing: 8) {
Group {
- remindAndBadge(content: content)
-
Text(content.title)
.pokitFont(.title3)
.foregroundStyle(.pokit(.text(.primary)))
@@ -109,6 +116,8 @@ private extension ContentDetailView {
.lineLimit(2)
HStack {
+ remindAndBadge(content: content)
+
Spacer()
Text(content.createdAt)
@@ -118,119 +127,105 @@ private extension ContentDetailView {
}
.padding(.horizontal, 20)
- Divider()
+ Rectangle()
.foregroundStyle(.pokit(.border(.tertiary)))
+ .frame(height: 1)
.padding(.top, 4)
}
}
- @ViewBuilder
- func contentLinkPreview(content: BaseContentDetail) -> some View {
- VStack(spacing: 16) {
- if store.showLinkPreview {
- PokitLinkPreview(
- title: store.linkTitle ?? content.title,
- url: content.data,
- imageURL: store.linkImageURL ?? "https://pokit-storage.s3.ap-northeast-2.amazonaws.com/logo/pokit.png"
- )
- .pokitBlurReplaceTransition(.pokitDissolve)
- }
-
- contentMemo(content: content)
- }
- .padding(.horizontal, 20)
- }
-
- @ViewBuilder
- func contentMemo(content: BaseContentDetail) -> some View {
- let isEmpty = content.memo.isEmpty
-
- HStack {
- VStack {
- Group {
- if isEmpty {
- Text("메모를 작성해보세요.")
- .foregroundStyle(.pokit(.text(.tertiary)))
- } else {
- Text(content.memo)
- .foregroundStyle(.pokit(.text(.primary)))
- }
- }
- .pokitFont(.b3(.r))
- .multilineTextAlignment(.leading)
-
+ var contentMemo: some View {
+ VStack(spacing: 12) {
+ HStack(alignment: .bottom) {
+ Text("메모")
+ .pokitFont(.b1(.m))
+ .foregroundStyle(.pokit(.text(.primary)))
+ .padding(.top, 16)
+
Spacer()
+
+ Image(.icon(.memo))
+ .resizable()
+ .frame(width: 24, height: 24)
+ .foregroundStyle(.pokit(.border(.primary)))
}
- .padding(16)
-
- Spacer()
- }
- .frame(minHeight: 132)
- .background {
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .fill(Color(red: 1, green: 0.96, blue: 0.89))
- }
- }
-
- @ViewBuilder
- func favorite(favorites: Bool) -> some View {
- Button(action: { send(.즐겨찾기_버튼_눌렀을때, animation: .pokitDissolve) }) {
- Image(favorites ? .icon(.starFill) : .icon(.starFill))
- .resizable()
- .scaledToFit()
- .foregroundStyle(.pokit(.icon(favorites ? .brand : .tertiary)))
- .frame(width: 24, height: 24)
+
+ PokitTextArea(
+ text: $store.memo,
+ state: $store.memoTextAreaState,
+ baseState: .memo(isReadOnly: false),
+ placeholder: "메모를 입력해주세요.",
+ maxLetter: 100,
+ focusState: $isFocused,
+ equals: true
+ )
+ .toolbar { keyboardToolBar }
+ .frame(minHeight: isFocused ? 164 : 132)
+ .animation(.pokitDissolve, value: isFocused)
}
+ .padding(.bottom, 24)
+ .padding(.horizontal, 20)
}
-
- @ViewBuilder
- func bottomToolbar(content: BaseContentDetail) -> some View {
- HStack(spacing: 14) {
- if let favorites = content.favorites {
- favorite(favorites: favorites)
+
+ var keyboardToolBar: some ToolbarContent {
+ ToolbarItemGroup(placement: .keyboard) {
+ Button("취소") {
+ isFocused = false
+ send(.키보드_취소_버튼_눌렀을때)
}
-
+
Spacer()
-
- Group {
- toolbarButton(
- .icon(.share),
- action: { send(.공유_버튼_눌렀을때) }
- )
-
- toolbarButton(
- .icon(.edit),
- action: { send(.수정_버튼_눌렀을때) }
- )
-
- toolbarButton(
- .icon(.trash),
- action: { send(.삭제_버튼_눌렀을때) }
- )
+
+ Button("완료") {
+ isFocused = false
+ send(.키보드_완료_버튼_눌렀울때)
}
- .disabled(store.contentId == nil)
- .opacity(store.contentId == nil ? 0 : 1)
- }
- .padding(.top, 12)
- .padding(.bottom, 40)
- .padding(.horizontal, 16)
- .background(.pokit(.bg(.base)))
- .overlay(alignment: .top) {
- Divider()
- .foregroundStyle(.pokit(.border(.tertiary)))
}
}
@ViewBuilder
- func toolbarButton(
- _ icon: PokitImage,
- action: @escaping () -> Void
- ) -> some View {
- Button(action: action) {
- Image(icon)
- .resizable()
- .frame(width: 24, height: 24)
- .foregroundStyle(.pokit(.icon(.secondary)))
+ func bottomList(favorites: Bool) -> some View {
+ VStack(spacing: 0) {
+ PokitListButton(
+ title: "즐겨찾기",
+ type: .bottomSheet(
+ icon: favorites
+ ? .icon(.starFill)
+ : .icon(.star),
+ iconColor: favorites
+ ? .pokit(.icon(.brand))
+ : .pokit(.icon(.primary))
+ ),
+ action: { send(.즐겨찾기_버튼_눌렀을때) }
+ )
+
+ PokitListButton(
+ title: "공유하기",
+ type: .bottomSheet(
+ icon: .icon(.share),
+ iconColor: .pokit(.icon(.primary))
+ ),
+ action: { send(.공유_버튼_눌렀을때) }
+ )
+
+ PokitListButton(
+ title: "수정하기",
+ type: .bottomSheet(
+ icon: .icon(.edit),
+ iconColor: .pokit(.icon(.primary))
+ ),
+ action: { send(.수정_버튼_눌렀을때) }
+ )
+
+ PokitListButton(
+ title: "삭제하기",
+ type: .bottomSheet(
+ icon: .icon(.trash),
+ iconColor: .pokit(.icon(.primary)),
+ isLast: true
+ ),
+ action: { send(.삭제_버튼_눌렀을때) }
+ )
}
}
}
diff --git a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
index dfbf0c2d..48ca2c76 100644
--- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
+++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
@@ -38,10 +38,6 @@ public struct ContentListFeature {
get { domain.contentCount }
}
var isListDescending = true
- /// sheet item
- var bottomSheetItem: BaseContentItem? = nil
- var alertItem: BaseContentItem? = nil
- var shareSheetItem: BaseContentItem? = nil
/// pagenation
var hasNext: Bool {
domain.contentList.hasNext
@@ -64,34 +60,21 @@ public struct ContentListFeature {
case binding(BindingAction)
case pagenation
- /// - Button Tapped
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
- case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
- case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
- case 컨텐츠_삭제_눌렀을때(content: BaseContentItem)
+
case 정렬_버튼_눌렀을때
case dismiss
/// - On Appeared
case 뷰가_나타났을때
-
- case 링크_공유시트_해제
- case 경고시트_해제
}
public enum InnerAction: Equatable {
- case 바텀시트_해제
case 컨텐츠_목록_조회_API_반영(BaseContentListInquiry)
- case 컨텐츠_삭제_API_반영(id: Int)
case 컨텐츠_목록_조회_페이징_API_반영(BaseContentListInquiry)
case 페이징_초기화
case 컨텐츠_개수_업데이트(Int)
}
public enum AsyncAction: Equatable {
- case 컨텐츠_삭제_API(id: Int)
case 컨텐츠_목록_조회_페이징_API
case 컨텐츠_목록_조회_API
case 컨텐츠_개수_조회_API
@@ -99,10 +82,6 @@ public struct ContentListFeature {
}
public enum ScopeAction {
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
case contents(IdentifiedActionOf)
}
@@ -158,19 +137,6 @@ private extension ContentListFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .컨텐츠_항목_케밥_버튼_눌렀을때(let content):
- state.bottomSheetItem = content
- return .none
- case .컨텐츠_항목_눌렀을때(let content):
- return .send(.delegate(.링크상세(content: content)))
- case .bottomSheet(let delegate, let content):
- return .concatenate(
- .send(.inner(.바텀시트_해제)),
- .send(.scope(.bottomSheet(delegate: delegate, content: content)))
- )
- case .컨텐츠_삭제_눌렀을때:
- guard let id = state.alertItem?.id else { return .none }
- return .send(.async(.컨텐츠_삭제_API(id: id)))
case .binding:
return .none
case .정렬_버튼_눌렀을때:
@@ -189,21 +155,12 @@ private extension ContentListFeature {
)
case .pagenation:
return .send(.async(.컨텐츠_목록_조회_페이징_API))
- case .링크_공유시트_해제:
- state.shareSheetItem = nil
- return .none
- case .경고시트_해제:
- state.alertItem = nil
- return .none
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .바텀시트_해제:
- state.bottomSheetItem = nil
- return .none
case .컨텐츠_목록_조회_페이징_API_반영(let contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
@@ -213,11 +170,6 @@ private extension ContentListFeature {
newList.forEach { state.contents.append(.init(content: $0)) }
return .none
- case .컨텐츠_삭제_API_반영(id: let id):
- state.alertItem = nil
- state.domain.contentList.data?.removeAll { $0.id == id }
- state.contents.removeAll { $0.content.id == id }
- return .none
case .컨텐츠_목록_조회_API_반영(let contentList):
state.domain.contentList = contentList
@@ -242,15 +194,6 @@ private extension ContentListFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .컨텐츠_삭제_API(id: let id):
- let count = state.domain.contentCount
- let newCount = count - 1
-
- return .merge(
- .send(.inner(.컨텐츠_개수_업데이트(newCount))),
- contentDelete(contentId: id)
- )
-
case .컨텐츠_목록_조회_페이징_API:
state.domain.pageable.page += 1
return .run { [
@@ -303,25 +246,8 @@ private extension ContentListFeature {
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
/// - 링크에 대한 `공유` / `수정` / `삭제` delegate
switch action {
- case .bottomSheet(let delegate, let content):
- switch delegate {
- case .deleteCellButtonTapped:
- state.alertItem = content
- return .none
- case .editCellButtonTapped:
- return .send(.delegate(.링크수정(contentId: content.id)))
- case .favoriteCellButtonTapped:
- return .none
- case .shareCellButtonTapped:
- state.shareSheetItem = content
- return .none
- }
-
- case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_눌렀을때(content)))):
- return .send(.delegate(.링크상세(content: content)))
case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content)))):
- state.bottomSheetItem = content
- return .none
+ return .send(.delegate(.링크상세(content: content)))
case .contents:
return .none
}
@@ -377,13 +303,6 @@ private extension ContentListFeature {
await send(.inner(.컨텐츠_목록_조회_API_반영(contentItems)), animation: .pokitDissolve)
}
}
-
- func contentDelete(contentId: Int) -> Effect {
- return .run { send in
- let _ = try await contentClient.컨텐츠_삭제("\(contentId)")
- await send(.inner(.컨텐츠_삭제_API_반영(id: contentId)), animation: .pokitSpring)
- }
- }
}
public extension ContentListFeature {
diff --git a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift
index b626fad1..f468d1bd 100644
--- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift
+++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift
@@ -34,33 +34,6 @@ public extension ContentListView {
.padding(.top, 12)
.pokitNavigationBar { toolbar }
.ignoresSafeArea(edges: .bottom)
- .sheet(item: $store.bottomSheetItem) { content in
- PokitBottomSheet(
- items: [.share, .edit, .delete],
- height: 224,
- delegateSend: {
- send(.bottomSheet(delegate: $0, content: content))
- }
- )
- }
- .sheet(item: $store.shareSheetItem) { content in
- if let shareURL = URL(string: content.data) {
- PokitShareSheet(
- items: [shareURL],
- completion: { send(.링크_공유시트_해제) }
- )
- .presentationDetents([.medium, .large])
- }
- }
- .sheet(item: $store.alertItem) { content in
- PokitAlert(
- "링크를 정말 삭제하시겠습니까?",
- message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
- confirmText: "삭제",
- action: { send(.컨텐츠_삭제_눌렀을때(content: content)) },
- cancelAction: { send(.경고시트_해제) }
- )
- }
.task { await send(.뷰가_나타났을때, animation: .pokitDissolve).finish() }
}
}
@@ -97,13 +70,14 @@ private extension ContentListView {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(
- store.scope(state: \.contents, action: \.contents)
+ Array(store.scope(state: \.contents, action: \.contents))
) { store in
let isFirst = store.state.id == self.store.contents.first?.id
let isLast = store.state.id == self.store.contents.last?.id
ContentCardView(
store: store,
+ type: .linkList,
isFirst: isFirst,
isLast: isLast
)
diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
index 089b9fa3..725c53ac 100644
--- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
+++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
@@ -50,10 +50,6 @@ public struct ContentSettingFeature {
get { domain.memo }
set { domain.memo = newValue }
}
- var isRemind: BaseContentDetail.RemindState {
- get { domain.alertYn }
- set { domain.alertYn = newValue }
- }
var content: BaseContentDetail? {
get { domain.content }
}
@@ -68,8 +64,7 @@ public struct ContentSettingFeature {
var selectedPokit: BaseCategoryItem?
var linkTitle: String? = nil
var linkImageURL: String? = nil
- var showMaxCategoryPopup: Bool = false
- var showDetectedURLPopup: Bool = false
+ var linkPopup: PokitLinkPopup.PopupType?
var contentLoading: Bool = false
var saveIsLoading: Bool = false
var link: String?
@@ -95,12 +90,13 @@ public struct ContentSettingFeature {
case 뷰가_나타났을때
case 저장_버튼_눌렀을때
case 포킷추가_버튼_눌렀을때
- case 링크복사_버튼_눌렀을때
-
+ case 링크팝업_버튼_눌렀을때
+ case 링크지우기_버튼_눌렀을때
+ case 제목지우기_버튼_눌렀을때
case 뒤로가기_버튼_눌렀을때
}
- public enum InnerAction: Equatable {
+ public enum InnerAction {
case linkPopup(URL?)
case linkPreview
case 메타데이터_조회_수행(url: URL)
@@ -111,6 +107,8 @@ public struct ContentSettingFeature {
case 카테고리_상세_조회_API_반영(category: BaseCategory)
case 카테고리_목록_조회_API_반영(categoryList: BaseCategoryListInquiry)
case 선택한_포킷_인메모리_삭제
+ case 링크팝업_활성화(PokitLinkPopup.PopupType)
+ case error(Error)
}
public enum AsyncAction: Equatable {
@@ -125,7 +123,7 @@ public struct ContentSettingFeature {
public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
- case 저장하기_완료
+ case 저장하기_완료(category: BaseCategoryItem)
case 포킷추가하기
case dismiss
}
@@ -197,6 +195,10 @@ private extension ContentSettingFeature {
return .merge(mergeEffect)
case .저장_버튼_눌렀을때:
let isEdit = state.domain.categoryId != nil
+ if state.domain.title == Constants.제목을_입력해주세요_문구 {
+ state.domain.title = state.title
+ }
+ state.saveIsLoading = true
return isEdit
? .send(.async(.컨텐츠_수정_API))
@@ -204,7 +206,7 @@ private extension ContentSettingFeature {
case .포킷추가_버튼_눌렀을때:
guard state.domain.categoryTotalCount < 30 else {
/// 🚨 Error Case [1]: 포킷 갯수가 30개 이상일 경우
- state.showMaxCategoryPopup = true
+ state.linkPopup = .text(title: Constants.포킷_최대_갯수_문구)
return .none
}
@@ -213,8 +215,16 @@ private extension ContentSettingFeature {
return state.isShareExtension
? .send(.delegate(.dismiss))
: .run { _ in await dismiss() }
- case .링크복사_버튼_눌렀을때:
+ case .링크팝업_버튼_눌렀을때:
+ guard case .link = state.linkPopup else { return .none }
return .send(.inner(.링크복사_반영(state.link)))
+ case .링크지우기_버튼_눌렀을때:
+ state.domain.data = ""
+ state.domain.title = ""
+ return .none
+ case .제목지우기_버튼_눌렀을때:
+ state.domain.title = ""
+ return .none
}
}
@@ -224,7 +234,10 @@ private extension ContentSettingFeature {
case let .linkPopup(url):
guard let url else { return .none }
state.link = url.absoluteString
- state.showDetectedURLPopup = true
+ state.linkPopup = .link(
+ title: Constants.복사한_링크_저장하기_문구,
+ url: url.absoluteString
+ )
return .none
case .linkPreview:
state.showLinkPreview = true
@@ -239,18 +252,23 @@ private extension ContentSettingFeature {
)
}
case let .메타데이텨_조회_반영(title: title, imageURL: imageURL):
- state.linkTitle = title
- state.linkImageURL = imageURL
+ let contentTitle = state.title.isEmpty
+ ? Constants.제목을_입력해주세요_문구
+ : state.title
+ state.linkImageURL = imageURL ?? Constants.기본_썸네일_주소.absoluteString
+ state.linkTitle = title ?? contentTitle
if let title, state.domain.title.isEmpty {
state.domain.title = title
}
state.domain.thumbNail = imageURL
return .send(.inner(.linkPreview), animation: .pokitDissolve)
case .URL_유효성_확인:
- guard let url = URL(string: state.domain.data),
- !state.domain.data.isEmpty else {
+ guard
+ let url = URL(string: state.domain.data),
+ !state.domain.data.isEmpty
+ else {
/// 🚨 Error Case [1]: 올바른 링크가 아닐 때
- state.showDetectedURLPopup = false
+ state.linkPopup = nil
state.linkTitle = nil
state.domain.title = ""
state.linkImageURL = nil
@@ -259,7 +277,7 @@ private extension ContentSettingFeature {
}
return .send(.inner(.메타데이터_조회_수행(url: url)), animation: .pokitDissolve)
case .링크복사_반영(let urlText):
- state.showDetectedURLPopup = false
+ state.linkPopup = nil
state.link = nil
guard let urlText else { return .none }
state.domain.data = urlText
@@ -293,8 +311,16 @@ private extension ContentSettingFeature {
/// - `카테고리_목록_조회`의 filter 옵션을 `false`로 해두었기 때문에 `미분류` 카테고리 또한 항목에서 조회가 가능함
/// [1]. `미분류`에 해당하는 인덱스 번호와 항목을 체크, 없다면 목록갱신이 불가함
- guard let unclassifiedItemIdx = categoryList.data?.firstIndex(where: { $0.categoryName == "미분류" }) else { return .none }
- guard let unclassifiedItem = categoryList.data?.first(where: { $0.categoryName == "미분류" }) else { return .none }
+ guard
+ let unclassifiedItemIdx = categoryList.data?.firstIndex(where: {
+ $0.categoryName == "미분류"
+ })
+ else { return .none }
+ guard
+ let unclassifiedItem = categoryList.data?.first(where: {
+ $0.categoryName == "미분류"
+ })
+ else { return .none }
/// [2]. 새로운 list변수를 만들어주고 카테고리 항목 순서를 재배치 (최신순 정렬 시 미분류는 항상 맨 마지막)
var list = categoryList
@@ -312,6 +338,16 @@ private extension ContentSettingFeature {
case .선택한_포킷_인메모리_삭제:
state.selectedPokit = nil
return .none
+ case let .링크팝업_활성화(type):
+ state.linkPopup = type
+ state.saveIsLoading = false
+ return .none
+ case let .error(error):
+ guard let errorResponse = error as? ErrorResponse else { return .none }
+ return .send(
+ .inner(.링크팝업_활성화(.error(title: errorResponse.message))),
+ animation: .pokitSpring
+ )
}
}
@@ -322,7 +358,7 @@ private extension ContentSettingFeature {
state.contentLoading = true
return .run { send in
let content = try await contentClient.컨텐츠_상세_조회("\(id)").toDomain()
- await send(.inner(.컨텐츠_상세_조회_API_반영(content: content)))
+ await send(.inner(.컨텐츠_상세_조회_API_반영(content: content)), animation: .pokitDissolve)
}
case let .카테고리_상세_조회_API(id, sharedId):
return .run { send in
@@ -347,10 +383,13 @@ private extension ContentSettingFeature {
categoryListFetch(request: request)
)
case .컨텐츠_수정_API:
- guard let contentId = state.domain.contentId,
- let categoryId = state.selectedPokit?.id else {
- return .none
- }
+ guard
+ let contentId = state.domain.contentId,
+ let categoryId = state.selectedPokit?.id,
+ let category = state.domain.categoryListInQuiry.data?.first(where: {
+ $0.id == categoryId
+ })
+ else { return .none }
let request = ContentBaseRequest(
data: state.domain.data,
title: state.domain.title,
@@ -359,15 +398,23 @@ private extension ContentSettingFeature {
alertYn: state.domain.alertYn.rawValue,
thumbNail: state.domain.thumbNail
)
- return .concatenate(
- contentEdit(request: request, contentId: contentId),
- .send(.inner(.선택한_포킷_인메모리_삭제)),
- .send(.delegate(.저장하기_완료))
- )
- case .컨텐츠_추가_API:
- guard let categoryId = state.selectedPokit?.id else {
- return .none
+ return .run { send in
+ let _ = try await contentClient.컨텐츠_수정(
+ "\(contentId)",
+ request
+ )
+ await send(.inner(.선택한_포킷_인메모리_삭제))
+ await send(.delegate(.저장하기_완료(category: category)))
+ } catch: { error, send in
+ await send(.inner(.error(error)))
}
+ case .컨텐츠_추가_API:
+ guard
+ let categoryId = state.selectedPokit?.id,
+ let category = state.domain.categoryListInQuiry.data?.first(where: {
+ $0.id == categoryId
+ })
+ else { return .none }
let request = ContentBaseRequest(
data: state.domain.data,
title: state.domain.title,
@@ -376,11 +423,13 @@ private extension ContentSettingFeature {
alertYn: state.domain.alertYn.rawValue,
thumbNail: state.domain.thumbNail
)
- return .concatenate(
- .run { _ in let _ = try await contentClient.컨텐츠_추가(request) },
- .send(.inner(.선택한_포킷_인메모리_삭제)),
- .send(.delegate(.저장하기_완료))
- )
+ return .run { send in
+ let content = try await contentClient.컨텐츠_추가(request)
+ await send(.inner(.선택한_포킷_인메모리_삭제))
+ await send(.delegate(.저장하기_완료(category: category)))
+ } catch: { error, send in
+ await send(.inner(.error(error)))
+ }
case .클립보드_감지:
return .run { send in
for await _ in self.pasteboard.changes() {
@@ -390,7 +439,7 @@ private extension ContentSettingFeature {
}
}
}
-
+
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
return .none
@@ -401,15 +450,6 @@ private extension ContentSettingFeature {
return .none
}
- func contentEdit(request: ContentBaseRequest, contentId: Int) -> Effect {
- return .run { _ in
- let _ = try await contentClient.컨텐츠_수정(
- "\(contentId)",
- request
- )
- }
- }
-
func categoryListFetch(request: BasePageableRequest) -> Effect {
return .run { send in
let categoryList = try await categoryClient.카테고리_목록_조회(request, false).toDomain()
diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
index 7e91b2b5..eb67c068 100644
--- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
+++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
@@ -8,6 +8,7 @@ import SwiftUI
import ComposableArchitecture
import DSKit
+import Util
@ViewAction(for: ContentSettingFeature.self)
public struct ContentSettingView: View {
@@ -36,40 +37,27 @@ public extension ContentSettingView {
titleTextField
- HStack(alignment: .bottom, spacing: 8) {
- pokitSelectButton
-
- addPokitButton
- }
+ pokitSelectButton
memoTextArea
-
- remindSwitchRadio
}
.padding(.horizontal, 20)
.padding(.top, 16)
}
.overlay(alignment: .bottom) {
- if store.state.showMaxCategoryPopup {
- PokitLinkPopup(
- "최대 30개의 포킷을 생성할 수 있습니다. \n포킷을 삭제한 뒤에 추가해주세요.",
- isPresented: $store.showMaxCategoryPopup,
- type: .text
- )
- .animation(.pokitSpring, value: store.showMaxCategoryPopup)
- } else if store.state.showDetectedURLPopup {
+ if store.linkPopup != nil {
PokitLinkPopup(
- "복사한 링크 저장하기",
- isPresented: $store.showDetectedURLPopup,
- type: .link(url: store.link ?? ""),
- action: { send(.링크복사_버튼_눌렀을때, animation: .pokitSpring) }
+ type: $store.linkPopup,
+ action: { send(.링크팝업_버튼_눌렀을때, animation: .pokitSpring) }
)
}
}
.pokitMaxWidth()
}
- let isDisable = store.urlText.isEmpty || store.title.isEmpty || store.memoTextAreaState == .error(message: "최대 100자까지 입력가능합니다.")
+ let isDisable = store.urlText.isEmpty ||
+ store.title.isEmpty ||
+ store.memoTextAreaState == .error(message: "최대 100자까지 입력가능합니다.")
PokitBottomButton(
"저장하기",
@@ -104,34 +92,50 @@ private extension ContentSettingView {
}
var linkTextField: some View {
VStack(spacing: 16) {
- if store.showLinkPreview {
+ if !store.urlText.isEmpty {
+ let isParsed = store.linkTitle != nil || store.linkImageURL != nil
+
PokitLinkPreview(
- title: store.linkTitle ?? (
- store.title.isEmpty ? "제목을 입력해주세요" : store.title
- ),
- url: store.urlText,
- imageURL: store.linkImageURL ?? "https://pokit-storage.s3.ap-northeast-2.amazonaws.com/logo/pokit.png"
+ title: store.linkTitle == Constants.제목을_입력해주세요_문구
+ ? store.title.isEmpty ? Constants.제목을_입력해주세요_문구 : store.title
+ : store.linkTitle,
+ url: isParsed ? store.urlText : nil,
+ imageURL: store.linkImageURL
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
PokitTextInput(
text: $store.urlText,
- label: "링크",
+ label: "링크",
+ type: store.urlText.isEmpty ? .text : .iconR(
+ icon: .icon(.x),
+ action: { send(.링크지우기_버튼_눌렀을때) }
+ ),
+ shape: .rectangle,
state: $store.linkTextInputState,
+ placeholder: "링크를 입력해주세요.",
focusState: $focusedType,
equals: .link
)
}
+ .animation(.pokitSpring, value: store.urlText)
}
var titleTextField: some View {
PokitTextInput(
text: $store.title,
- label: "제목",
+ label: "제목",
+ type: store.title.isEmpty ? .text : .iconR(
+ icon: .icon(.x),
+ action: { send(.제목지우기_버튼_눌렀을때) }
+ ),
+ shape: .rectangle,
state: $store.titleTextInpuState,
+ placeholder: "제목을 입력해주세요.",
focusState: $focusedType,
- equals: .title) { }
+ equals: .title
+ )
}
var pokitSelectButton: some View {
@@ -139,62 +143,22 @@ private extension ContentSettingView {
selectedItem: $store.selectedPokit,
label: "포킷",
list: store.pokitList,
- action: { send(.포킷선택_항목_눌렀을때(pokit: $0), animation: .pokitDissolve) }
+ action: { send(.포킷선택_항목_눌렀을때(pokit: $0), animation: .pokitDissolve) },
+ addAction: { send(.포킷추가_버튼_눌렀을때, animation: .pokitSpring) }
)
}
- var addPokitButton: some View {
- PokitIconButton(
- .icon(.plusR),
- state: .filled(.primary),
- size: .large,
- shape: .rectangle
- ) { send(.포킷추가_버튼_눌렀을때, animation: .pokitSpring) }
- }
-
var memoTextArea: some View {
PokitTextArea(
text: $store.memo,
label: "메모",
state: $store.memoTextAreaState,
+ placeholder: "메모를 입력해주세요.",
focusState: $focusedType,
equals: .memo
)
.frame(height: 192)
}
-
- var remindSwitchRadio: some View {
- VStack(alignment: .leading, spacing: 0) {
- Text("리마인드 알림을 보내드릴까요?")
- .pokitFont(.b2(.m))
- .foregroundStyle(.pokit(.text(.secondary)))
- .padding(.bottom, 12)
-
- PokitSwitchRadio {
- PokitPartSwitchRadio(
- labelText: "안받을래요",
- selection: $store.isRemind,
- to: .no,
- style: .stroke
- )
- .background()
-
- PokitPartSwitchRadio(
- labelText: "받을래요",
- selection: $store.isRemind,
- to: .yes,
- style: .stroke
- )
- .background()
- }
- .padding(.bottom, 8)
-
- Text("일주일 후에 알림을 전송해드립니다")
- .pokitFont(.detail1)
- .foregroundStyle(.pokit(.text(.tertiary)))
- }
- .padding(.bottom, 16)
- }
}
private extension ContentSettingView {
enum FocusedType: Equatable {
diff --git a/Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift b/Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift
index 74e865e9..4d0ef886 100644
--- a/Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift
@@ -7,7 +7,6 @@
import SwiftUI
import ComposableArchitecture
-import FeatureLogin
public struct IntroView: View {
/// - Properties
diff --git a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
index 83cf8c24..6ce0890f 100644
--- a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
@@ -7,6 +7,7 @@
import ComposableArchitecture
import SwiftUI
import DSKit
+import Util
@ViewAction(for: RegisterNicknameFeature.self)
public struct RegisterNicknameView: View {
@@ -72,8 +73,9 @@ extension RegisterNicknameView {
private var textField: some View {
PokitTextInput(
text: $store.nicknameText,
+ shape: .rectangle,
state: $store.textfieldState,
- info: "한글, 영어, 숫자로만 입력이 가능합니다.",
+ info: Constants.한글_영어_숫자_입력_문구,
maxLetter: 10,
focusState: $isFocused,
equals: true
diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
index 30b93937..ff05b68c 100644
--- a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
+++ b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
@@ -40,8 +40,6 @@ public struct PokitRootFeature {
var contents: IdentifiedArrayOf = []
var selectedKebobItem: BaseCategoryItem?
- var selectedUnclassifiedItem: BaseContentItem?
- var shareSheetItem: BaseContentItem? = nil
var isKebobSheetPresented: Bool = false
var isPokitDeleteSheetPresented: Bool = false
@@ -71,12 +69,10 @@ public struct PokitRootFeature {
case 필터_버튼_눌렀을때(PokitRootFilterType.Folder)
case 분류_버튼_눌렀을때
case 케밥_버튼_눌렀을때(BaseCategoryItem)
- case 미분류_케밥_버튼_눌렀을때(BaseContentItem)
case 포킷추가_버튼_눌렀을때
case 링크추가_버튼_눌렀을때
case 카테고리_눌렀을때(BaseCategoryItem)
case 컨텐츠_항목_눌렀을때(BaseContentItem)
- case 링크_공유_완료되었을때
case 뷰가_나타났을때
case 페이지_로딩중일때
}
@@ -105,7 +101,6 @@ public struct PokitRootFeature {
case 미분류_카테고리_조회_API
case 미분류_카테고리_페이징_조회_API
case 미분류_카테고리_페이징_재조회_API
- case 미분류_카테고리_컨텐츠_삭제_API(contentId: Int)
}
public enum ScopeAction {
@@ -128,6 +123,7 @@ public struct PokitRootFeature {
case 포킷추가_버튼_눌렀을때
case 링크추가_버튼_눌렀을때
+ case 미분류_카테고리_활성화
}
}
@@ -210,10 +206,6 @@ private extension PokitRootFeature {
case .케밥_버튼_눌렀을때(let selectedItem):
state.selectedKebobItem = selectedItem
return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
-
- case .미분류_케밥_버튼_눌렀을때(let selectedItem):
- state.selectedUnclassifiedItem = selectedItem
- return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
case .포킷추가_버튼_눌렀을때:
return .run { send in await send(.delegate(.포킷추가_버튼_눌렀을때)) }
@@ -255,10 +247,6 @@ private extension PokitRootFeature {
default: return .none
}
-
- case .링크_공유_완료되었을때:
- state.shareSheetItem = nil
- return .none
}
}
@@ -326,9 +314,11 @@ private extension PokitRootFeature {
return .none
case let .미분류_카테고리_컨텐츠_삭제_API_반영(contentId: contentId):
- guard let index = state.domain.unclassifiedContentList.data?.firstIndex(where: { $0.id == contentId }) else {
- return .none
- }
+ guard
+ let index = state.domain.unclassifiedContentList.data?.firstIndex(where: {
+ $0.id == contentId
+ })
+ else { return .none }
state.domain.unclassifiedContentList.data?.remove(at: index)
state.contents.removeAll { $0.content.id == contentId }
state.isPokitDeleteSheetPresented = false
@@ -449,13 +439,6 @@ private extension PokitRootFeature {
guard let categoryItems else { return }
await send(.inner(.카테고리_조회_API_반영(categoryList: categoryItems)), animation: .pokitSpring)
}
-
- case let .미분류_카테고리_컨텐츠_삭제_API(contentId):
- return .run { send in
- let _ = try await contentClient.컨텐츠_삭제("\(contentId)")
- await send(.inner(.미분류_카테고리_컨텐츠_삭제_API_반영(contentId: contentId)), animation: .pokitSpring)
- }
-
}
}
@@ -465,51 +448,26 @@ private extension PokitRootFeature {
/// - Kebob BottomSheet Delegate
case .bottomSheet(.shareCellButtonTapped):
/// Todo: 공유하기
- switch state.folderType {
- case .folder(.미분류):
- guard let selectedItem = state.selectedUnclassifiedItem else {
- /// 🚨 Error Case [1]: 항목을 공유하려는데 항목이 없을 때
- return .none
- }
- state.isKebobSheetPresented = false
- state.shareSheetItem = selectedItem
- return .none
- case .folder(.포킷):
- guard let selectedItem = state.selectedKebobItem else {
- /// 🚨 Error Case [1]: 항목을 공유하려는데 항목이 없을 때
- return .none
- }
- kakaoShareClient.카테고리_카카오톡_공유(
- CategoryKaKaoShareModel(
- categoryName: selectedItem.categoryName,
- categoryId: selectedItem.id,
- imageURL: selectedItem.categoryImage.imageURL
- )
- )
- state.isKebobSheetPresented = false
+ guard let selectedItem = state.selectedKebobItem else {
+ /// 🚨 Error Case [1]: 항목을 공유하려는데 항목이 없을 때
return .none
-
- default: return .none
}
+ kakaoShareClient.카테고리_카카오톡_공유(
+ CategoryKaKaoShareModel(
+ categoryName: selectedItem.categoryName,
+ categoryId: selectedItem.id,
+ imageURL: selectedItem.categoryImage.imageURL
+ )
+ )
+ state.isKebobSheetPresented = false
+ return .none
case .bottomSheet(.editCellButtonTapped):
- switch state.folderType {
- case .folder(.미분류):
- state.isKebobSheetPresented = false
- return .run { [item = state.selectedUnclassifiedItem] send in
- guard let item else { return }
- await send(.delegate(.링크수정하기(id: item.id)))
- }
-
- case .folder(.포킷):
- /// [1] 케밥을 종료
- state.isKebobSheetPresented = false
- /// [2] 수정하기로 이동
- return .run { [item = state.selectedKebobItem] send in
- guard let item else { return }
- await send(.delegate(.수정하기(item)))
- }
- default: return .none
+ state.isKebobSheetPresented = false
+ /// [2] 수정하기로 이동
+ return .run { [item = state.selectedKebobItem] send in
+ guard let item else { return }
+ await send(.delegate(.수정하기(item)))
}
case .bottomSheet(.deleteCellButtonTapped):
@@ -524,35 +482,20 @@ private extension PokitRootFeature {
return .none
case .deleteBottomSheet(.deleteButtonTapped):
- switch state.folderType {
- case .folder(.미분류):
- guard let selectedItem = state.selectedUnclassifiedItem else {
- /// 🚨 Error Case [1]: 항목을 삭제하려는데 항목이 없을 때
- return .none
- }
- return .send(.async(.미분류_카테고리_컨텐츠_삭제_API(contentId: selectedItem.id)), animation: .pokitSpring)
-
- case .folder(.포킷):
- guard let selectedItem = state.selectedKebobItem else {
- /// 🚨 Error Case [1]: 항목을 삭제하려는데 항목이 없을 때
- return .none
- }
- guard let index = state.domain.categoryList.data?.firstIndex(of: selectedItem) else {
- return .none
- }
- state.domain.categoryList.data?.remove(at: index)
- state.isPokitDeleteSheetPresented = false
-
- return .run { send in await send(.async(.카테고리_삭제_API(categoryId: selectedItem.id))) }
-
- default: return .none
+ guard let selectedItem = state.selectedKebobItem else {
+ /// 🚨 Error Case [1]: 항목을 삭제하려는데 항목이 없을 때
+ return .none
}
+ guard let index = state.domain.categoryList.data?.firstIndex(of: selectedItem) else {
+ return .none
+ }
+ state.domain.categoryList.data?.remove(at: index)
+ state.isPokitDeleteSheetPresented = false
+
+ return .run { send in await send(.async(.카테고리_삭제_API(categoryId: selectedItem.id))) }
- case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_눌렀을때(content)))):
- return .send(.delegate(.contentDetailTapped(content)))
case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content)))):
- state.selectedUnclassifiedItem = content
- return .send(.inner(.카테고리_시트_활성화(true)))
+ return .send(.delegate(.contentDetailTapped(content)))
case .contents:
return .none
@@ -573,6 +516,10 @@ private extension PokitRootFeature {
default: return .none
}
+ case .미분류_카테고리_활성화:
+ state.folderType = .folder(.미분류)
+ state.sortType = .sort(.최신순)
+ return .send(.inner(.sort))
default:
return .none
}
diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
index 741c383e..66c4b556 100644
--- a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
+++ b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
@@ -46,15 +46,6 @@ public extension PokitRootView {
delegateSend: { store.send(.scope(.bottomSheet($0))) }
)
}
- .sheet(item: $store.shareSheetItem) { content in
- if let shareURL = URL(string: content.data) {
- PokitShareSheet(
- items: [shareURL],
- completion: { send(.링크_공유_완료되었을때) }
- )
- .presentationDetents([.medium, .large])
- }
- }
.sheet(isPresented: $store.isPokitDeleteSheetPresented) {
PokitDeleteBottomSheet(
type: store.folderType == .folder(.포킷)
@@ -95,13 +86,15 @@ private extension PokitRootView {
Spacer()
- PokitIconLTextLink(
- store.sortType == .sort(.최신순) ?
- "최신순" : store.folderType == .folder(.포킷) ? "이름순" : "오래된순",
- icon: .icon(.align),
- action: { send(.분류_버튼_눌렀을때) }
- )
- .contentTransition(.numericText())
+ if !store.contents.isEmpty {
+ PokitIconLTextLink(
+ store.sortType == .sort(.최신순) ?
+ "최신순" : store.folderType == .folder(.포킷) ? "이름순" : "오래된순",
+ icon: .icon(.align),
+ action: { send(.분류_버튼_눌렀을때) }
+ )
+ .contentTransition(.numericText())
+ }
}
.animation(.snappy(duration: 0.7), value: store.folderType)
}
@@ -190,13 +183,14 @@ private extension PokitRootView {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(
- store.scope(state: \.contents, action: \.contents)
+ Array(store.scope(state: \.contents, action: \.contents))
) { store in
let isFirst = store.state.id == self.store.contents.first?.id
let isLast = store.state.id == self.store.contents.last?.id
ContentCardView(
store: store,
+ type: .linkList,
isFirst: isFirst,
isLast: isLast
)
diff --git a/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift b/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
index 24766a92..4638db6b 100644
--- a/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
+++ b/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
@@ -17,6 +17,8 @@ public struct RemindFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
+ @Dependency(\.openURL)
+ private var openURL
@Dependency(RemindClient.self)
private var remindClient
@Dependency(ContentClient.self)
@@ -53,10 +55,6 @@ public struct RemindFeature {
favoriteList.forEach { identifiedArray.append($0) }
return identifiedArray
}
- /// sheet item
- var bottomSheetItem: BaseContentItem? = nil
- var alertItem: BaseContentItem? = nil
- var shareSheetItem: BaseContentItem? = nil
}
/// - Action
public enum Action: FeatureAction, ViewAction {
@@ -68,10 +66,6 @@ public struct RemindFeature {
public enum View: Equatable, BindableAction {
case binding(BindingAction)
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
/// - Button Tapped
case 알림_버튼_눌렀을때
@@ -80,9 +74,6 @@ public struct RemindFeature {
case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
case 안읽음_목록_버튼_눌렀을때
case 즐겨찾기_목록_버튼_눌렀을때
- case 링크_삭제_눌렀을때(content: BaseContentItem)
-
- case 링크_공유_완료
case 뷰가_나타났을때
case 즐겨찾기_항목_이미지_조회(contentId: Int)
@@ -90,31 +81,24 @@ public struct RemindFeature {
case 리마인드_항목_이미지오류_나타났을때(contentId: Int)
}
public enum InnerAction: Equatable {
- case 바텀시트_해제
case 오늘의_리마인드_조회_API_반영(contents: [BaseContentItem])
case 읽지않음_컨텐츠_조회_API_반영(contentList: BaseContentListInquiry)
case 즐겨찾기_링크모음_조회_API_반영(contentList: BaseContentListInquiry)
case 즐겨찾기_이미지_조회_수행_반영(imageURL: String, index: Int)
case 읽지않음_이미지_조회_수행_반영(imageURL: String, index: Int)
case 리마인드_이미지_조회_수행_반영(imageURL: String, index: Int)
- case 컨텐츠_삭제_API_반영(id: Int)
}
public enum AsyncAction: Equatable {
case 오늘의_리마인드_조회_API
case 읽지않음_컨텐츠_조회_API
case 즐겨찾기_링크모음_조회_API
- case 컨텐츠_삭제_API(id: Int)
+ case 썸네일_수정_API(imageURL: String, contentId: Int)
case 즐겨찾기_이미지_조회_수행(contentId: Int)
case 읽지않음_이미지_조회_수행(contentId: Int)
case 리마인드_이미지_조회_수행(contentId: Int)
}
- public enum ScopeAction: Equatable {
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
- }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case 링크상세(content: BaseContentItem)
case alertButtonTapped
@@ -160,11 +144,6 @@ private extension RemindFeature {
switch action {
case .binding:
return .none
- case .bottomSheet(let delegate, let content):
- return .run { send in
- await send(.inner(.바텀시트_해제))
- await send(.scope(.bottomSheet(delegate: delegate, content: content)))
- }
case .알림_버튼_눌렀을때:
return .send(.delegate(.alertButtonTapped))
case .검색_버튼_눌렀을때:
@@ -174,18 +153,12 @@ private extension RemindFeature {
case .안읽음_목록_버튼_눌렀을때:
return .send(.delegate(.링크목록_안읽음))
case .컨텐츠_항목_케밥_버튼_눌렀을때(let content):
- state.bottomSheetItem = content
- return .none
- case .컨텐츠_항목_눌렀을때(let content):
return .send(.delegate(.링크상세(content: content)))
- case .링크_삭제_눌렀을때:
- guard let id = state.alertItem?.id else { return .none }
- return .send(.async(.컨텐츠_삭제_API(id: id)))
+ case .컨텐츠_항목_눌렀을때(let content):
+ guard let url = URL(string: content.data) else { return .none }
+ return .run { _ in await openURL(url) }
case .뷰가_나타났을때:
return allContentFetch(animation: .pokitDissolve)
- case .링크_공유_완료:
- state.shareSheetItem = nil
- return .none
case let .즐겨찾기_항목_이미지_조회(contentId):
return .send(.async(.즐겨찾기_이미지_조회_수행(contentId: contentId)))
case let .읽지않음_항목_이미지_조회(contentId):
@@ -197,9 +170,6 @@ private extension RemindFeature {
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .바텀시트_해제:
- state.bottomSheetItem = nil
- return .none
case .오늘의_리마인드_조회_API_반영(contents: let contents):
state.domain.recommendedList = contents
return .none
@@ -209,30 +179,24 @@ private extension RemindFeature {
case .즐겨찾기_링크모음_조회_API_반영(contentList: let contentList):
state.domain.favoriteList = contentList
return .none
- case .컨텐츠_삭제_API_반영(id: let contentId):
- state.alertItem = nil
- state.domain.recommendedList?.removeAll { $0.id == contentId }
- state.domain.unreadList.data?.removeAll { $0.id == contentId }
- state.domain.favoriteList.data?.removeAll { $0.id == contentId }
- return .none
case let .즐겨찾기_이미지_조회_수행_반영(imageURL, index):
var content = state.domain.favoriteList.data?.remove(at: index)
content?.thumbNail = imageURL
guard let content else { return .none }
state.domain.favoriteList.data?.insert(content, at: index)
- return .none
+ return .send(.async(.썸네일_수정_API(imageURL: imageURL, contentId: content.id)))
case let .읽지않음_이미지_조회_수행_반영(imageURL, index):
var content = state.domain.unreadList.data?.remove(at: index)
content?.thumbNail = imageURL
guard let content else { return .none }
state.domain.unreadList.data?.insert(content, at: index)
- return .none
+ return .send(.async(.썸네일_수정_API(imageURL: imageURL, contentId: content.id)))
case let .리마인드_이미지_조회_수행_반영(imageURL, index):
var content = state.domain.recommendedList?.remove(at: index)
content?.thumbNail = imageURL
guard let content else { return .none }
state.domain.recommendedList?.insert(content, at: index)
- return .none
+ return .send(.async(.썸네일_수정_API(imageURL: imageURL, contentId: content.id)))
}
}
/// - Async Effect
@@ -265,18 +229,13 @@ private extension RemindFeature {
).toDomain()
await send(.inner(.즐겨찾기_링크모음_조회_API_반영(contentList: contentList)), animation: .pokitDissolve)
}
- case .컨텐츠_삭제_API(id: let id):
- return .run { send in
- let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_API_반영(id: id)), animation: .pokitSpring)
- }
case let .즐겨찾기_이미지_조회_수행(contentId):
return .run { [favoriteContents = state.favoriteContents] send in
- guard let index = favoriteContents?.index(id: contentId),
- let content = favoriteContents?[index],
- let url = URL(string: content.data) else {
- return
- }
+ guard
+ let index = favoriteContents?.index(id: contentId),
+ let content = favoriteContents?[index],
+ let url = URL(string: content.data)
+ else { return }
let imageURL = try await swiftSoupClient.parseOGImageURL(url)
guard let imageURL else { return }
@@ -288,11 +247,11 @@ private extension RemindFeature {
}
case let .읽지않음_이미지_조회_수행(contentId):
return .run { [unreadContents = state.unreadContents] send in
- guard let index = unreadContents?.index(id: contentId),
- let content = unreadContents?[index],
- let url = URL(string: content.data) else {
- return
- }
+ guard
+ let index = unreadContents?.index(id: contentId),
+ let content = unreadContents?[index],
+ let url = URL(string: content.data)
+ else { return }
let imageURL = try await swiftSoupClient.parseOGImageURL(url)
guard let imageURL else { return }
@@ -303,11 +262,11 @@ private extension RemindFeature {
}
case let .리마인드_이미지_조회_수행(contentId):
return .run { [recommendedContents = state.recommendedContents] send in
- guard let index = recommendedContents?.index(id: contentId),
- let content = recommendedContents?[index],
- let url = URL(string: content.data) else {
- return
- }
+ guard
+ let index = recommendedContents?.index(id: contentId),
+ let content = recommendedContents?[index],
+ let url = URL(string: content.data)
+ else { return }
let imageURL = try await swiftSoupClient.parseOGImageURL(url)
guard let imageURL else { return }
@@ -316,26 +275,20 @@ private extension RemindFeature {
index: index
)))
}
+ case let .썸네일_수정_API(imageURL, contentId):
+ return .run { send in
+ let request = ThumbnailRequest(thumbnail: imageURL)
+
+ try await contentClient.썸네일_수정(
+ contentId: "\(contentId)",
+ model: request
+ )
+ }
}
}
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
- /// - 링크에 대한 `공유` / `수정` / `삭제` delegate
- switch action {
- case .bottomSheet(let delegate, let content):
- switch delegate {
- case .deleteCellButtonTapped:
- state.alertItem = content
- return .none
- case .editCellButtonTapped:
- return .send(.delegate(.링크수정(id: content.id)))
- case .favoriteCellButtonTapped:
- return .none
- case .shareCellButtonTapped:
- state.shareSheetItem = content
- return .none
- }
- }
+ return .none
}
/// - Delegate Effect
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
diff --git a/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift b/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
index 98cdf65e..f6400565 100644
--- a/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
+++ b/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
@@ -31,28 +31,6 @@ public extension RemindView {
.background(.pokit(.bg(.base)))
.ignoresSafeArea(edges: .bottom)
.navigationBarBackButtonHidden(true)
- .sheet(item: $store.bottomSheetItem) { content in
- PokitBottomSheet(
- items: [.share, .edit, .delete],
- height: 224
- ) { send(.bottomSheet(delegate: $0, content: content)) }
- }
- .sheet(item: $store.shareSheetItem) { content in
- if let shareURL = URL(string: content.data) {
- PokitShareSheet(
- items: [shareURL],
- completion: { send(.링크_공유_완료) }
- )
- .presentationDetents([.medium, .large])
- }
- }
- .sheet(item: $store.alertItem) { content in
- PokitAlert(
- "링크를 정말 삭제하시겠습니까?",
- message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
- confirmText: "삭제"
- ) { send(.링크_삭제_눌렀을때(content: content)) }
- }
.task { await send(.뷰가_나타났을때, animation: .pokitDissolve).finish() }
}
}
@@ -115,7 +93,6 @@ extension RemindView {
HStack(spacing: 12) {
ForEach(recommendedContents, id: \.id) { content in
recommendedContentCell(content: content)
-
}
}
.padding(.horizontal, 20)
@@ -134,12 +111,6 @@ extension RemindView {
@ViewBuilder
private func recommendedContentCellLabel(content: BaseContentItem) -> some View {
ZStack(alignment: .bottom) {
- if let url = URL(string: content.thumbNail) {
- recommendedContentCellImage(url: url, contentId: content.id)
- } else {
- imagePlaceholder
- }
-
LinearGradient(
stops: [
Gradient.Stop(
@@ -156,7 +127,7 @@ extension RemindView {
)
VStack(alignment: .leading, spacing: 0) {
- PokitBadge(content.categoryName, state: .small)
+ PokitBadge(state: .small(content.categoryName))
HStack(spacing: 4) {
Text(content.title)
@@ -184,6 +155,13 @@ extension RemindView {
.padding(12)
}
.frame(width: 216, height: 194)
+ .background {
+ if let url = URL(string: content.thumbNail) {
+ recommendedContentCellImage(url: url, contentId: content.id)
+ } else {
+ imagePlaceholder
+ }
+ }
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.clipped()
}
@@ -258,16 +236,18 @@ extension RemindView {
}
ForEach(unreadContents, id: \.id) { content in
- let isFirst = content == unreadContents.elements.first
- let isLast = content == unreadContents.elements.last
+ let isFirst = content.id == unreadContents.first?.id
+ let isLast = content.id == unreadContents.last?.id
PokitLinkCard(
link: content,
+ state: isFirst
+ ? .top
+ : isLast ? .bottom : .middle,
action: { send(.컨텐츠_항목_눌렀을때(content: content)) },
kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content)) },
fetchMetaData: { send(.읽지않음_항목_이미지_조회(contentId: content.id)) }
)
- .divider(isFirst: isFirst, isLast: isLast)
}
}
}
@@ -289,16 +269,18 @@ extension RemindView {
.padding(.top, 16)
} else {
ForEach(favoriteContents, id: \.id) { content in
- let isFirst = content == favoriteContents.elements.first
- let isLast = content == favoriteContents.elements.last
+ let isFirst = content.id == favoriteContents.first?.id
+ let isLast = content.id == favoriteContents.last?.id
PokitLinkCard(
link: content,
+ state: isFirst
+ ? .top
+ : isLast ? .bottom : .middle,
action: { send(.컨텐츠_항목_눌렀을때(content: content)) },
kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content)) },
fetchMetaData: { send(.즐겨찾기_항목_이미지_조회(contentId: content.id)) }
)
- .divider(isFirst: isFirst, isLast: isLast)
}
}
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
index ff647e64..5744b502 100644
--- a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
@@ -146,7 +146,11 @@ private extension PokitAlertBoxFeature {
return .none
case let .알람_삭제_API_반영(item):
- guard let idx = state.domain.alertList.data?.firstIndex(where: { $0 == item }) else { return .none }
+ guard
+ let idx = state.domain.alertList.data?.firstIndex(where: {
+ $0 == item
+ })
+ else { return .none }
state.domain.alertList.data?.remove(at: idx)
return .none
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
index 9664bf83..1c991664 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
@@ -72,11 +72,6 @@ public struct PokitSearchFeature {
get { domain.contentList.hasNext }
}
var isLoading: Bool = false
-
- /// sheet item
- var bottomSheetItem: BaseContentItem? = nil
- var alertItem: BaseContentItem? = nil
- var shareSheetItem: BaseContentItem? = nil
}
/// - Action
@@ -93,10 +88,6 @@ public struct PokitSearchFeature {
public enum View: Equatable, BindableAction {
case binding(BindingAction)
case dismiss
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
case 자동저장_버튼_눌렀을때
case 검색_버튼_눌렀을때
case 최근검색_태그_눌렀을때(text: String)
@@ -109,15 +100,10 @@ public struct PokitSearchFeature {
case 즐겨찾기_태그_눌렀을때
case 안읽음_태그_눌렀을때
case 전체_삭제_버튼_눌렀을때
- case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
- case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
case 정렬_버튼_눌렀을때
case 검색_키보드_엔터_눌렀을때
- case 링크_삭제_눌렀을때
- case 링크_공유_완료되었을때
case 뷰가_나타났을때
case 로딩중일때
-
}
public enum InnerAction: Equatable {
@@ -127,10 +113,8 @@ public struct PokitSearchFeature {
case 모아보기_업데이트(favoriteFilter: Bool, unreadFilter: Bool)
case 필터_업데이트
case 카테고리_ID_목록_업데이트
- case 바텀시트_해제
case 컨텐츠_검색_API_반영(BaseContentListInquiry)
case 컨텐츠_검색_페이징_API_반영(BaseContentListInquiry)
- case 컨텐츠_삭제_API_반영(id: Int)
case 최근검색어_불러오기
case 자동저장_불러오기
case 최근검색어_추가
@@ -141,17 +125,12 @@ public struct PokitSearchFeature {
case 컨텐츠_검색_API
case 최근검색어_갱신_수행
case 자동저장_수행
- case 컨텐츠_삭제_API(id: Int)
case 컨텐츠_검색_페이징_API
case 클립보드_감지
}
public enum ScopeAction {
case filterBottomSheet(FilterBottomFeature.Action.DelegateAction)
- case bottomSheet(
- delegate: PokitBottomSheet.Delegate,
- content: BaseContentItem
- )
case contents(IdentifiedActionOf)
}
@@ -257,10 +236,11 @@ private extension PokitSearchFeature {
return .send(.inner(.filterBottomSheet(filterType: .contentType)))
case .기간_버튼_눌렀을때:
- guard state.domain.condition.startDate != nil && state.domain.condition.endDate != nil else {
+ guard
+ state.domain.condition.startDate != nil &&
+ state.domain.condition.endDate != nil
/// - 선택된 기간이 없을 경우
- return .send(.inner(.filterBottomSheet(filterType: .date)))
- }
+ else { return .send(.inner(.filterBottomSheet(filterType: .date))) }
state.domain.condition.startDate = nil
state.domain.condition.endDate = nil
return .run { send in
@@ -282,24 +262,6 @@ private extension PokitSearchFeature {
state.recentSearchTexts.remove(at: predicate)
return .send(.async(.최근검색어_갱신_수행))
- case let .컨텐츠_항목_눌렀을때(content):
- return .send(.delegate(.linkCardTapped(content: content)))
-
- case let .컨텐츠_항목_케밥_버튼_눌렀을때(content):
- state.bottomSheetItem = content
- return .none
-
- case .bottomSheet(delegate: let delegate, content: let content):
- return .run { send in
- await send(.inner(.바텀시트_해제))
- await send(.scope(.bottomSheet(delegate: delegate, content: content)))
- }
-
- case .링크_삭제_눌렀을때:
- guard let id = state.alertItem?.id else { return .none }
- state.alertItem = nil
- return .send(.async(.컨텐츠_삭제_API(id: id)))
-
case .정렬_버튼_눌렀을때:
state.isResultAscending.toggle()
state.domain.pageable.sort = [
@@ -340,9 +302,6 @@ private extension PokitSearchFeature {
state.domain.condition.isRead = false
return .send(.inner(.페이징_초기화))
- case .링크_공유_완료되었을때:
- state.shareSheetItem = nil
- return .none
case .로딩중일때:
return .send(.async(.컨텐츠_검색_페이징_API))
}
@@ -365,8 +324,7 @@ private extension PokitSearchFeature {
state.domain.condition.startDate = startDate
state.domain.condition.endDate = endDate
- guard let startDate,
- let endDate else {
+ guard let startDate, let endDate else {
/// 🚨 Error Case : 날짜 필터가 선택 안되었을 경우
state.dateFilterText = "기간"
return .none
@@ -392,10 +350,6 @@ private extension PokitSearchFeature {
state.domain.condition.isRead = unreadFilter
return .none
- case .바텀시트_해제:
- state.bottomSheetItem = nil
- return .none
-
case .필터_업데이트:
state.isFiltered = !state.categoryFilter.isEmpty
|| state.favoriteFilter
@@ -438,12 +392,6 @@ private extension PokitSearchFeature {
}
return .send(.async(.최근검색어_갱신_수행))
- case let .컨텐츠_삭제_API_반영(id):
- state.alertItem = nil
- state.domain.contentList.data?.removeAll { $0.id == id }
- state.contents.removeAll { $0.content.id == id }
- return .none
-
case let .컨텐츠_검색_페이징_API_반영(contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
@@ -484,12 +432,6 @@ private extension PokitSearchFeature {
await userDefaults.setBool(isAutoSaveSearch, .autoSaveSearch)
}
- case let .컨텐츠_삭제_API(id):
- return .run { send in
- let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_API_반영(id: id)), animation: .pokitSpring)
- }
-
case .컨텐츠_검색_페이징_API:
state.domain.pageable.page += 1
let formatter = DateFormat.yearMonthDate.formatter
@@ -560,25 +502,8 @@ private extension PokitSearchFeature {
await send(.inner(.페이징_초기화))
}
- case .bottomSheet(let delegate, let content):
- switch delegate {
- case .deleteCellButtonTapped:
- state.alertItem = content
- return .none
- case .editCellButtonTapped:
- return .send(.delegate(.링크수정(contentId: content.id)))
- case .favoriteCellButtonTapped:
- return .none
- case .shareCellButtonTapped:
- state.shareSheetItem = content
- return .none
- }
-
- case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_눌렀을때(content)))):
- return .send(.delegate(.linkCardTapped(content: content)))
case let .contents(.element(id: _, action: .delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content)))):
- state.bottomSheetItem = content
- return .none
+ return .send(.delegate(.linkCardTapped(content: content)))
case .contents:
return .none
}
@@ -588,9 +513,10 @@ private extension PokitSearchFeature {
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
switch action {
case .컨텐츠_검색:
- guard let contentList = state.domain.contentList.data, !contentList.isEmpty else {
- return .none
- }
+ guard
+ let contentList = state.domain.contentList.data,
+ !contentList.isEmpty
+ else { return .none }
return .send(.async(.컨텐츠_검색_API), animation: .pokitSpring)
default: return .none
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
index 016dd7fb..2984dd3f 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
@@ -51,28 +51,6 @@ public extension PokitSearchView {
) { store in
FilterBottomSheet(store: store)
}
- .sheet(item: $store.bottomSheetItem) { content in
- PokitBottomSheet(
- items: [.share, .edit, .delete],
- height: 224
- ) { send(.bottomSheet(delegate: $0, content: content)) }
- }
- .sheet(item: $store.shareSheetItem) { content in
- if let shareURL = URL(string: content.data) {
- PokitShareSheet(
- items: [shareURL],
- completion: { send(.링크_공유_완료되었을때) }
- )
- .presentationDetents([.medium, .large])
- }
- }
- .sheet(item: $store.alertItem) { content in
- PokitAlert(
- "링크를 정말 삭제하시겠습니까?",
- message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
- confirmText: "삭제"
- ) { send(.링크_삭제_눌렀을때) }
- }
.task { await send(.뷰가_나타났을때).finish() }
}
}
@@ -86,15 +64,18 @@ private extension PokitSearchView {
action: { send(.dismiss) }
)
- PokitIconRInput(
+ PokitTextInput(
text: $store.searchText,
- icon: store.isSearching ? .icon(.x) : .icon(.search),
- placeholder: "제목, 메모를 검색해보세요.",
+ type: .iconR(
+ icon: store.isSearching ? .icon(.x) : .icon(.search),
+ action: store.isSearching ? { send(.검색_버튼_눌렀을때) } : nil
+ ),
shape: .round,
+ state: .constant(.default),
+ placeholder: "제목, 메모를 검색해보세요.",
focusState: $focused,
equals: true,
- onSubmit: { send(.검색_키보드_엔터_눌렀을때) },
- iconTappedAction: store.isSearching ? { send(.검색_버튼_눌렀을때) } : nil
+ onSubmit: { send(.검색_키보드_엔터_눌렀을때) }
)
}
.padding(.vertical, 8)
@@ -296,13 +277,14 @@ private extension PokitSearchView {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(
- store.scope(state: \.contents, action: \.contents)
+ Array(store.scope(state: \.contents, action: \.contents))
) { store in
let isFirst = store.state.id == self.store.contents.first?.id
let isLast = store.state.id == self.store.contents.last?.id
ContentCardView(
store: store,
+ type: .linkList,
isFirst: isFirst,
isLast: isLast
)
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
index d0810b0b..7d339461 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
@@ -48,6 +48,7 @@ public struct NickNameSettingFeature {
case dismiss
case 저장_버튼_눌렀을때
case 뷰가_나타났을때
+ case 닉네임지우기_버튼_눌렀을때
}
public enum InnerAction: Equatable {
@@ -131,6 +132,9 @@ private extension NickNameSettingFeature {
case .뷰가_나타났을때:
return .send(.async(.닉네임_조회_API))
+ case .닉네임지우기_버튼_눌렀을때:
+ state.domain.nickname = ""
+ return .none
}
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
index f22a4af0..e4b7c249 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
@@ -8,6 +8,7 @@ import SwiftUI
import ComposableArchitecture
import DSKit
+import Util
@ViewAction(for: NickNameSettingFeature.self)
public struct NickNameSettingView: View {
@@ -31,8 +32,13 @@ public extension NickNameSettingView {
} else {
PokitTextInput(
text: $store.text,
+ type: store.text.isEmpty ? .text : .iconR(
+ icon: .icon(.x),
+ action: { send(.닉네임지우기_버튼_눌렀을때) }
+ ),
+ shape: .rectangle,
state: $store.textfieldState,
- info: "한글, 영어, 숫자로만 입력이 가능합니다.",
+ info: Constants.한글_영어_숫자_입력_문구,
maxLetter: 10,
focusState: $isFocused,
equals: true
diff --git a/Projects/Util/Sources/Constants.swift b/Projects/Util/Sources/Constants.swift
index 5b0a0e1d..b9da10f5 100644
--- a/Projects/Util/Sources/Constants.swift
+++ b/Projects/Util/Sources/Constants.swift
@@ -22,7 +22,14 @@ public enum Constants {
public static let 개인정보_처리방침_주소: URL = URL(string: "https://www.notion.so/de3468b3be1744538c22a333ae1d0ec8")!
public static let 마케팅_정보_수신_주소: URL = URL(string: "https://www.notion.so/bb6d0d6569204d5e9a7b67e5825f9d10")!
public static let 고객문의_주소: URL = URL(string: "https://www.instagram.com/pokit.official/")!
+ public static let 기본_썸네일_주소: URL = URL(string: "https://pokit-storage.s3.ap-northeast-2.amazonaws.com/logo/pokit.png")!
+ public static let 포킷_최대_갯수_문구: String = "최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요."
+ public static let 복사한_링크_저장하기_문구: String = "복사한 링크 저장하기"
+ public static let 제목을_입력해주세요_문구: String = "제목을 입력해주세요"
+ public static let 링크_저장_완료_문구: String = "링크 저장 완료"
+ public static let 메모_수정_완료_문구: String = "메모 수정 완료"
+ public static let 한글_영어_숫자_입력_문구: String = "한글, 영어, 숫자로만 입력이 가능합니다."
public static var mockImageUrl: String { "https://picsum.photos/\(Int.random(in: 150...250))" }
}
diff --git a/Projects/Util/Sources/Extension/View+Extension.swift b/Projects/Util/Sources/Extension/View+Extension.swift
new file mode 100644
index 00000000..d0e7c9b0
--- /dev/null
+++ b/Projects/Util/Sources/Extension/View+Extension.swift
@@ -0,0 +1,40 @@
+//
+// View+Extension.swift
+// Util
+//
+// Created by 김도형 on 12/1/24.
+//
+
+import SwiftUI
+
+extension View {
+ public func dismissKeyboard(
+ focused: FocusState.Binding
+ ) -> some View {
+ self
+ .overlay {
+ if focused.wrappedValue {
+ Color.clear
+ .contentShape(Rectangle())
+ .onTapGesture {
+ focused.wrappedValue = false
+ }
+ }
+ }
+ }
+
+ public func dismissKeyboard(
+ focused: FocusState.Binding
+ ) -> some View {
+ self
+ .overlay {
+ if focused.wrappedValue != nil {
+ Color.clear
+ .contentShape(Rectangle())
+ .onTapGesture {
+ focused.wrappedValue = nil
+ }
+ }
+ }
+ }
+}
diff --git a/Projects/Util/Sources/Protocols/PokitLinkCardItem.swift b/Projects/Util/Sources/Protocols/PokitLinkCardItem.swift
index 84d08d43..4f65156e 100644
--- a/Projects/Util/Sources/Protocols/PokitLinkCardItem.swift
+++ b/Projects/Util/Sources/Protocols/PokitLinkCardItem.swift
@@ -9,10 +9,12 @@ import Foundation
public protocol PokitLinkCardItem {
var title: String { get }
+ var memo: String? { get }
var thumbNail: String { get }
var createdAt: String { get }
var categoryName: String { get }
var isRead: Bool? { get }
var data: String { get }
var domain: String { get }
+ var isFavorite: Bool? { get }
}
diff --git a/Projects/Util/Sources/Protocols/PokitSelectItem.swift b/Projects/Util/Sources/Protocols/PokitSelectItem.swift
index e9767209..0fbec838 100644
--- a/Projects/Util/Sources/Protocols/PokitSelectItem.swift
+++ b/Projects/Util/Sources/Protocols/PokitSelectItem.swift
@@ -8,6 +8,9 @@
import Foundation
public protocol PokitSelectItem: Identifiable, Equatable {
+ associatedtype Thumbnail: CategoryImage
+
var categoryName: String { get }
var contentCount: Int { get }
+ var categoryImage: Thumbnail { get }
}