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