diff --git a/.github/workflows/hotfix.yml b/.github/workflows/appstore_release.yml
similarity index 74%
rename from .github/workflows/hotfix.yml
rename to .github/workflows/appstore_release.yml
index 6c7b4ebf..bd63256e 100644
--- a/.github/workflows/hotfix.yml
+++ b/.github/workflows/appstore_release.yml
@@ -1,11 +1,12 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
-name: hotfix
+name: appstore-release
on:
- push:
- branches: [ "hotfix/*" ]
+ pull_request:
+ branches:
+ - main
jobs:
build:
@@ -17,12 +18,12 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '15.4'
+ xcode-version: latest-stable
- # - uses: shimataro/ssh-key-action@v2
- # with:
- # key: ${{ secrets.SSH_KEY }}
- # known_hosts: ${{ secrets.KNOWN_HOSTS }}
+ - uses: shimataro/ssh-key-action@v2
+ with:
+ key: ${{ secrets.SSH_KEY }}
+ known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: initial mise
run: |
@@ -36,10 +37,10 @@ jobs:
- name: Generate Project
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- MATCH_URL: ${{ secrets.MATCH_URL }}
- APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
- run: fastlane generate
+ run: |
+ fastlane appstore_profile
+ make release
- name: Build Archive
env:
@@ -48,9 +49,9 @@ jobs:
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
run: fastlane archive
- - name: Beta Release
+ - name: Appstore Release
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
- run: fastlane release
+ run: fastlane appstore_release
diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml
index e0f7bc91..d27957cc 100644
--- a/.github/workflows/build_test.yml
+++ b/.github/workflows/build_test.yml
@@ -5,7 +5,10 @@ name: build-test
on:
pull_request:
- branches: [ "develop" ]
+ branches:
+ - develop
+
+ workflow_dispatch:
jobs:
build:
@@ -17,7 +20,7 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '15.4'
+ xcode-version: latest-stable
- uses: shimataro/ssh-key-action@v2
with:
@@ -36,10 +39,10 @@ jobs:
- name: Test Generate
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- MATCH_URL: ${{ secrets.MATCH_URL }}
- APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
- run: fastlane test
+ run: |
+ fastlane development_profile
+ make test
- name: Build Test
run: fastlane build
diff --git a/.github/workflows/workflow_test.yml b/.github/workflows/develop_hotfix.yml
similarity index 61%
rename from .github/workflows/workflow_test.yml
rename to .github/workflows/develop_hotfix.yml
index 606764de..01172def 100644
--- a/.github/workflows/workflow_test.yml
+++ b/.github/workflows/develop_hotfix.yml
@@ -1,14 +1,17 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
-name: workflow_test
+name: develop-hotfix
on:
push:
- branches: [ "workflow/*" ]
+ branches: [ "develop" ]
+
+ workflow_dispatch:
jobs:
build:
+ if: startsWith(github.event.head_commit.message, '[hotfix]')
runs-on: macos-latest
steps:
@@ -17,12 +20,12 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '15.4'
+ xcode-version: latest-stable
- # - uses: shimataro/ssh-key-action@v2
- # with:
- # key: ${{ secrets.SSH_KEY }}
- # known_hosts: ${{ secrets.KNOWN_HOSTS }}
+ - uses: shimataro/ssh-key-action@v2
+ with:
+ key: ${{ secrets.SSH_KEY }}
+ known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: initial mise
run: |
@@ -33,24 +36,13 @@ jobs:
- name: initial tuist
run: mise install tuist
- - name: Test Generate
- env:
- MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- MATCH_URL: ${{ secrets.MATCH_URL }}
- APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
- DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
- run: fastlane test
-
- - name: Build Test
- run: fastlane build
-
- - name: Release Generate
+ - name: Generate Project
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- MATCH_URL: ${{ secrets.MATCH_URL }}
- APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
- run: fastlane generate
+ run: |
+ fastlane appstore_profile
+ make release
- name: Build Archive
env:
@@ -59,9 +51,9 @@ jobs:
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
run: fastlane archive
- - name: Beta Release
+ - name: Testflight Release
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
- run: fastlane release
+ run: fastlane testflight_release
diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml
new file mode 100644
index 00000000..8104f099
--- /dev/null
+++ b/.github/workflows/release_update.yml
@@ -0,0 +1,31 @@
+# This workflow will build a Swift project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
+
+name: release-update
+
+on:
+ pull_request:
+ branches:
+ - main
+ types:
+ - closed
+
+jobs:
+ release-update:
+ if: ${{ github.event.pull_request.merged == true }}
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install GitHub CLI
+ run: sudo apt-get install gh
+
+ - name: Update Release
+ env:
+ APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: fastlane update_github_release
diff --git a/.github/workflows/testflight_release.yml b/.github/workflows/testflight_release.yml
index 0ccfd05e..aaf83977 100644
--- a/.github/workflows/testflight_release.yml
+++ b/.github/workflows/testflight_release.yml
@@ -1,14 +1,20 @@
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
-name: testflight_release
+name: testflight-release
on:
- push:
- branches: [ "develop" ]
+ pull_request:
+ branches:
+ - develop
+ types:
+ - closed
+
+ workflow_dispatch:
jobs:
build:
+ if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true }}
runs-on: macos-latest
steps:
@@ -17,7 +23,7 @@ jobs:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '15.4'
+ xcode-version: latest-stable
- uses: shimataro/ssh-key-action@v2
with:
@@ -36,10 +42,10 @@ jobs:
- name: Generate Project
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- MATCH_URL: ${{ secrets.MATCH_URL }}
- APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
- run: fastlane generate
+ run: |
+ fastlane appstore_profile
+ make release
- name: Build Archive
env:
@@ -48,9 +54,9 @@ jobs:
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
run: fastlane archive
- - name: Beta Release
+ - name: Testflight Release
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API: ${{ secrets.APP_STORE_CONNECT_API }}
- run: fastlane release
+ run: fastlane testflight_release
diff --git a/Makefile b/Makefile
index 8a090c56..372403c0 100644
--- a/Makefile
+++ b/Makefile
@@ -52,13 +52,17 @@ download-privates:
@if [ ! -d "Pokit_iOS_Private" ]; then \
git clone git@github.com:stealmh/Pokit_iOS_Private.git; \
fi
- @if [ -f "Pokit_iOS_Private/xcconfig/Secret.xcconfig" ]; then \
+ @if [ -f "Pokit_iOS_Private/xcconfig/Debug.xcconfig" ] && [ -f "Pokit_iOS_Private/xcconfig/Release.xcconfig" ]; then \
mkdir -p xcconfig; \
- cp Pokit_iOS_Private/xcconfig/Secret.xcconfig xcconfig/Secret.xcconfig; \
+ cp Pokit_iOS_Private/xcconfig/Debug.xcconfig xcconfig/Debug.xcconfig; \
+ cp Pokit_iOS_Private/xcconfig/Release.xcconfig xcconfig/Release.xcconfig; \
+ cp Pokit_iOS_Private/xcconfig/Debug-Share.xcconfig xcconfig/Debug-Share.xcconfig; \
+ cp Pokit_iOS_Private/xcconfig/Release-Share.xcconfig xcconfig/Release-Share.xcconfig; \
cp Pokit_iOS_Private/auth/AuthKey.p8 Projects/CoreKit/Resources/AuthKey.p8; \
cp Pokit_iOS_Private/GoogleService-Info.plist Projects/App/Resources/GoogleService-Info.plist; \
+ cp Pokit_iOS_Private/GoogleService-Info.plist Projects/App/ShareExtension/Resources/GoogleService-Info.plist; \
rm -rf Pokit_iOS_Private; \
- echo "✅ Secret 파일을 성공적으로 다운로드하고 Pokit_iOS_Private 폴더를 삭제했습니다."; \
+ echo "✅ Debug.xcconfig와 Release.xcconfig 파일을 성공적으로 다운로드하고 Pokit_iOS_Private 폴더를 삭제했습니다."; \
else \
- echo "❌ Secret.xcconfig 파일을 찾을 수 없습니다."; \
+ echo "❌ Debug.xcconfig 또는 Release.xcconfig 파일을 찾을 수 없습니다." && exit 1; \
fi
diff --git a/Projects/App/AppTests/Resources/info.plist b/Projects/App/AppTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/App/AppTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/App/AppTests/Sources/AppTests.swift b/Projects/App/AppTests/Sources/AppTests.swift
new file mode 100644
index 00000000..edcec259
--- /dev/null
+++ b/Projects/App/AppTests/Sources/AppTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import App
+
+final class AppTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift
index 7b965c96..51ce1d00 100644
--- a/Projects/App/Project.swift
+++ b/Projects/App/Project.swift
@@ -15,6 +15,74 @@ let features: [TargetDependency] = Feature.allCases.map { feature in
.project(target: "Feature\(feature.rawValue)", path: .relativeToRoot("Projects/Feature"))
}
+let shareExtensionTarget: Target = .target(
+ name: "ShareExtension",
+ destinations: .appDestinations,
+ product: .appExtension,
+ bundleId: "com.pokitmons.pokit.ShareExtension",
+ deploymentTargets: .appMinimunTarget,
+ infoPlist: .file(path: .relativeToRoot("Projects/App/ShareExtension/Info.plist")),
+ sources: ["ShareExtension/Sources/**"],
+ resources: ["ShareExtension/Resources/**"],
+ entitlements: .file(path: .relativeToRoot("Projects/App/ShareExtension/ShareExtension.entitlements")),
+ dependencies: [
+ .project(target: "FeatureLogin", path: .relativeToRoot("Projects/Feature")),
+ .project(target: "FeatureContentSetting", path: .relativeToRoot("Projects/Feature")),
+ .project(target: "FeatureCategorySetting", path: .relativeToRoot("Projects/Feature"))
+ ],
+ settings: .settings(
+ base: [
+ "OTHER_LDFLAGS": "$(inherited) -ObjC",
+ "CODE_SIGN_STYLE": "Manual"
+ ],
+ configurations: [
+ .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfig/Debug-Share.xcconfig")),
+ .release(
+ name: "Release",
+ settings: ["CODE_SIGN_IDENTITY": "Apple Distribution"],
+ xcconfig: .relativeToRoot("xcconfig/Release-Share.xcconfig")
+ )
+ ]
+ )
+)
+
+let projectTarget: Target = .target(
+ name: "App",
+ destinations: .appDestinations,
+ product: .app,
+ bundleId: "com.pokitmons.pokit",
+ deploymentTargets: .appMinimunTarget,
+ infoPlist: .file(path: .relativeToRoot("Projects/App/Resources/Pokit-info.plist")),
+ sources: ["Sources/**"],
+ resources: ["Resources/**"],
+ entitlements: .file(path: .relativeToRoot("Projects/App/Resources/Pokit-iOS.entitlements")),
+ dependencies: features + [
+ // TODO: 의존성 추가
+ .external(name: "FirebaseMessaging"),
+ .target(shareExtensionTarget)
+ ],
+ settings: .settings(
+ .release(
+ name: "Release",
+ settings: [
+ "CODE_SIGN_IDENTITY": "Apple Distribution"
+ ],
+ xcconfig: .relativeToRoot("xcconfig/Release.xcconfig")
+ )
+ )
+)
+
+let appTestTarget: Target = .makeTarget(
+ name: "AppTests",
+ product: .unitTests,
+ bundleName: "AppTests",
+ infoPlist: .dictionary(["ENABLE_TESTING_SEARCH_PATHS": "YES"]),
+ resources: ["AppTests/Resources/**"],
+ dependencies: [
+ .target(projectTarget)
+ ]
+)
+
let project = Project(
name: "App",
options: .options(
@@ -22,33 +90,8 @@ let project = Project(
developmentRegion: "ko"
),
targets: [
- .target(
- name: "App",
- destinations: .appDestinations,
- product: .app,
- bundleId: "com.pokitmons.pokit",
- deploymentTargets: .appMinimunTarget,
- infoPlist: .file(path: .relativeToRoot("Projects/App/Resources/Pokit-info.plist")),
- sources: ["Sources/**"],
- resources: ["Resources/**"],
- entitlements: .file(path: .relativeToRoot("Projects/App/Resources/Pokit-iOS.entitlements")),
- dependencies: features + [
- // TODO: 의존성 추가
- .external(name: "FirebaseMessaging")
- ],
- settings: .settings(
- base: [
- "OTHER_LDFLAGS": "$(inherited) -ObjC",
- "CODE_SIGN_IDENTITY": "Apple Distribution",
- "PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.pokitmons.pokit 1721720816",
- "PROVISIONING_PROFILE": "match AppStore com.pokitmons.pokit 1721720816",
- "DEVELOPMENT_TEAM": "\(developmentTeam ?? "")"
- ],
- configurations: [
- .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfig/Secret.xcconfig")),
- .release(name: "Release", xcconfig: .relativeToRoot("xcconfig/Secret.xcconfig"))
- ]
- )
- )
+ projectTarget,
+ appTestTarget,
+ shareExtensionTarget
]
)
diff --git a/Projects/App/Resources/Pokit-iOS.entitlements b/Projects/App/Resources/Pokit-iOS.entitlements
index 80b5221d..11bd0e65 100644
--- a/Projects/App/Resources/Pokit-iOS.entitlements
+++ b/Projects/App/Resources/Pokit-iOS.entitlements
@@ -8,5 +8,13 @@
Default
+ com.apple.security.application-groups
+
+ group.com.pokitmons.pokit
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)group.com.pokitmons.pokit
+
diff --git a/Projects/App/Resources/Pokit-info.plist b/Projects/App/Resources/Pokit-info.plist
index 7368d7ef..af5f75f4 100644
--- a/Projects/App/Resources/Pokit-info.plist
+++ b/Projects/App/Resources/Pokit-info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.3
+ 1.0.4
CFBundleURLTypes
diff --git a/Projects/App/ShareExtension/Info.plist b/Projects/App/ShareExtension/Info.plist
new file mode 100644
index 00000000..8e36808f
--- /dev/null
+++ b/Projects/App/ShareExtension/Info.plist
@@ -0,0 +1,54 @@
+
+
+
+
+ AppleKeyID
+ $(AppleKeyID)
+ CFBundleDisplayName
+ Pokit
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleName
+ Pokit
+ CFBundleShortVersionString
+ 1.0.4
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ $(GIDClientID_reversed)
+
+
+
+ CFBundleVersion
+ 1
+ GIDClientID
+ $(GIDClientID)
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsText
+
+ NSExtensionActivationSupportsWebPageWithMaxCount
+ 1000
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1000
+
+
+ NSExtensionPointIdentifier
+ com.apple.share-services
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ShareViewController
+
+ TeamID
+ $(TeamID)
+
+
diff --git a/Projects/App/ShareExtension/Resources/Resource.swift b/Projects/App/ShareExtension/Resources/Resource.swift
new file mode 100644
index 00000000..3780fc99
--- /dev/null
+++ b/Projects/App/ShareExtension/Resources/Resource.swift
@@ -0,0 +1,8 @@
+//
+// Source.stencil.swift
+// Manifests
+//
+// Created by 김도형 on 6/16/24.
+//
+
+import Foundation
diff --git a/Projects/App/ShareExtension/ShareExtension.entitlements b/Projects/App/ShareExtension/ShareExtension.entitlements
new file mode 100644
index 00000000..17445d40
--- /dev/null
+++ b/Projects/App/ShareExtension/ShareExtension.entitlements
@@ -0,0 +1,18 @@
+
+
+
+
+ com.apple.developer.applesignin
+
+ Default
+
+ com.apple.security.application-groups
+
+ group.com.pokitmons.pokit
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)group.com.pokitmons.pokit
+
+
+
diff --git a/Projects/App/ShareExtension/Sources/Base.lproj/MainInterface.storyboard b/Projects/App/ShareExtension/Sources/Base.lproj/MainInterface.storyboard
new file mode 100644
index 00000000..286a5089
--- /dev/null
+++ b/Projects/App/ShareExtension/Sources/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Projects/App/ShareExtension/Sources/ShareRootFeature.swift b/Projects/App/ShareExtension/Sources/ShareRootFeature.swift
new file mode 100644
index 00000000..b63b2e44
--- /dev/null
+++ b/Projects/App/ShareExtension/Sources/ShareRootFeature.swift
@@ -0,0 +1,211 @@
+//
+// ShareRootFeature.swift
+// ShareExtension
+//
+// Created by 김도형 on 10/17/24.
+//
+
+import UIKit
+import UniformTypeIdentifiers
+
+import ComposableArchitecture
+import FeatureLogin
+import FeatureContentSetting
+import FeatureCategorySetting
+import CoreKit
+import Util
+import Social
+
+@Reducer
+struct ShareRootFeature {
+ @Dependency(SocialLoginClient.self)
+ private var socialLogin
+
+ @ObservableState
+ struct State {
+ var intro: IntroFeature.State? = .init()
+ var contentSetting: ContentSettingFeature.State? = nil
+ var path = StackState()
+
+ var url: URL?
+ var context: NSExtensionContext?
+ var controller: SLComposeServiceViewController?
+ }
+
+ enum Action {
+ case intro(IntroFeature.Action)
+ case contentSetting(ContentSettingFeature.Action)
+ case inner(InnerAction)
+ case async(AsyncAction)
+ case scope(ScopeAction)
+ case controller(ControllerAction)
+ case path(StackActionOf)
+
+ enum InnerAction {
+ case dismiss
+ case URL_파싱_수행_반영(URL?)
+ }
+ enum AsyncAction {
+ case URL_파싱_수행
+ }
+ enum ScopeAction {
+ case intro(IntroFeature.Action)
+ case contentSetting(ContentSettingFeature.Action)
+ case categorySetting(PokitCategorySettingFeature.Action)
+ }
+ enum ControllerAction {
+ case viewDidLoad(SLComposeServiceViewController?, NSExtensionContext?)
+ case viewDidAppear
+ case presentationControllerDidDismiss
+ }
+ }
+
+ private func core(into state: inout State, action: Action) -> Effect {
+ switch action {
+ case .intro(let introAction):
+ return .send(.scope(.intro(introAction)))
+ case .contentSetting(let contentSettingAction):
+ return .send(.scope(.contentSetting(contentSettingAction)))
+ /// - Inner
+ case .inner(let innerAction):
+ return handleInnerAction(innerAction, state: &state)
+ /// - Async
+ case .async(let asyncAction):
+ return handleAsyncAction(asyncAction, state: &state)
+ /// - Scope
+ case .scope(let scopeAction):
+ return handleScopeAction(scopeAction, state: &state)
+ /// - Controller
+ case .controller(let controllerAction):
+ return handleControllerAction(controllerAction, state: &state)
+ /// - Path
+ case .path(let pathAction):
+ return handlePathAction(pathAction, state: &state)
+ }
+ }
+
+ func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
+ switch action {
+ case let .URL_파싱_수행_반영(url):
+ state.url = url
+ return .none
+ case .dismiss:
+ state.controller?.dismiss(animated: true) { [context = state.context] in
+ /// 🚨 Error Case [1]: 사용자가 취소한 경우
+ let error = NSError(
+ domain: "com.pokitmons.pokit.ShareExtension",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "User cancelled the action."]
+ )
+ context?.cancelRequest(withError: error)
+ }
+ return .none
+ }
+ }
+
+ 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
+ }
+
+ return .run { send in
+ var urlItem: (any NSSecureCoding)? = nil
+
+ if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
+ urlItem = try await itemProvider.loadItem(
+ forTypeIdentifier: UTType.url.identifier
+ )
+ let url = urlItem as? URL
+ await send(.inner(.URL_파싱_수행_반영(url)))
+
+ } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
+ /// 🚨 Error Case [1]: 유튜브 링크같이 url자체로 파싱이 안되는 경우
+ urlItem = try await itemProvider.loadItem(
+ forTypeIdentifier: UTType.plainText.identifier
+ )
+ guard let urlString = urlItem as? String else { return }
+ await send(.inner(.URL_파싱_수행_반영(URL(string: urlString))))
+ }
+ }
+ }
+ }
+
+ func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
+ switch action {
+ case .intro(.delegate(.moveToTab)):
+ state.contentSetting = .init(
+ urlText: state.url?.absoluteString,
+ isShareExtension: true
+ )
+ state.intro = nil
+ return .none
+ case .intro:
+ return .none
+ case .contentSetting(.delegate(.저장하기_완료)):
+ state.controller?.dismiss(animated: true) { [context = state.context] in
+ context?.completeRequest(returningItems: [])
+ }
+ return .none
+ case .contentSetting(.delegate(.dismiss)):
+ return .send(.inner(.dismiss))
+ case .contentSetting(.delegate(.포킷추가하기)):
+ state.path.append(.categorySetting(PokitCategorySettingFeature.State(type: .추가)))
+ return .none
+ case .contentSetting:
+ return .none
+ case .categorySetting(.delegate(.settingSuccess)):
+ state.path.removeLast()
+ return .none
+ case .categorySetting:
+ return .none
+ }
+ }
+
+ func handleControllerAction(_ action: Action.ControllerAction, state: inout State) -> Effect {
+ switch action {
+ case let .viewDidLoad(controller, context):
+ state.context = context
+ state.controller = controller
+ socialLogin.setRootViewController(controller)
+ return .send(.async(.URL_파싱_수행))
+ case .viewDidAppear:
+ state.controller?.textView.resignFirstResponder()
+ return .none
+ case .presentationControllerDidDismiss:
+ /// 🚨 Error Case [2]: 사용자가 시트를 내려서 취소한 경우
+ let error = NSError(
+ domain: "com.pokitmons.pokit.ShareExtension",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "User cancelled the action."]
+ )
+ state.context?.cancelRequest(withError: error)
+ return .none
+ }
+ }
+
+ func handlePathAction(_ action: StackActionOf, state: inout State) -> Effect {
+ switch action {
+ case let .element(id: _, action: .categorySetting(categorySettingAction)):
+ return .send(.scope(.categorySetting(categorySettingAction)))
+ case .element, .popFrom, .push:
+ return .none
+ }
+ }
+
+ public var body: some ReducerOf {
+ Reduce(self.core)
+ .ifLet(\.intro, action: \.intro) { IntroFeature() }
+ .ifLet(\.contentSetting, action: \.contentSetting) { ContentSettingFeature() }
+ .forEach(\.path, action: \.path)
+ }
+}
+
+extension ShareRootFeature {
+ @Reducer
+ enum Path {
+ case categorySetting(PokitCategorySettingFeature)
+ }
+}
diff --git a/Projects/App/ShareExtension/Sources/ShareRootView.swift b/Projects/App/ShareExtension/Sources/ShareRootView.swift
new file mode 100644
index 00000000..f538be53
--- /dev/null
+++ b/Projects/App/ShareExtension/Sources/ShareRootView.swift
@@ -0,0 +1,52 @@
+//
+// ShareRootView.swift
+// ShareExtension
+//
+// Created by 김도형 on 10/17/24.
+//
+
+import SwiftUI
+
+import ComposableArchitecture
+import FeatureLogin
+import FeatureContentSetting
+import FeatureCategorySetting
+import DSKit
+
+struct ShareRootView: View {
+ /// - Properties
+ @Perception.Bindable
+ private var store: StoreOf
+
+ init(store: StoreOf) {
+ self.store = store
+ }
+
+ var body: some View {
+ WithPerceptionTracking {
+ Group {
+ if let store = store.scope(state: \.intro, action: \.intro) {
+ IntroView(store: store)
+ } else if let store = store.scope(state: \.contentSetting, action: \.contentSetting) {
+ NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
+ ContentSettingView(store: store)
+ } destination: { path in
+ switch path.case {
+ case let .categorySetting(store):
+ PokitCategorySettingView(store: store)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#Preview {
+ ShareRootView(
+ store: Store(
+ initialState: .init(),
+ reducer: { ShareRootFeature() }
+ )
+ )
+}
diff --git a/Projects/App/ShareExtension/Sources/ShareViewController.swift b/Projects/App/ShareExtension/Sources/ShareViewController.swift
new file mode 100644
index 00000000..a7e218bf
--- /dev/null
+++ b/Projects/App/ShareExtension/Sources/ShareViewController.swift
@@ -0,0 +1,41 @@
+//
+// ShareViewController.swift
+// ShareExtension
+//
+// Created by 김도형 on 10/16/24.
+//
+
+import SwiftUI
+import Social
+
+import ComposableArchitecture
+import FeatureContentSetting
+
+class ShareViewController: SLComposeServiceViewController {
+ let store = Store(initialState: ShareRootFeature.State()) {
+ ShareRootFeature()
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let hostingController = UIHostingController(
+ rootView: ShareRootView(store: store)
+ )
+ hostingController.presentationController?.delegate = self
+ hostingController.modalPresentationStyle = .automatic
+ present(hostingController, animated: true)
+
+ store.send(.controller(.viewDidLoad(self, extensionContext)))
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ store.send(.controller(.viewDidAppear))
+ }
+}
+
+extension ShareViewController: UIAdaptivePresentationControllerDelegate {
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ store.send(.controller(.presentationControllerDidDismiss))
+ }
+}
diff --git a/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift b/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
index d5fdbcdd..ace38842 100644
--- a/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
+++ b/Projects/App/Sources/AppDelegate/AppDelegateFeature.swift
@@ -14,9 +14,9 @@ import KakaoSDKCommon
@Reducer
public struct AppDelegateFeature {
- @Dependency(\.userNotifications) var userNotifications
- @Dependency(\.remoteNotifications.register) var registerForRemoteNotifications
- @Dependency(\.userDefaults) var userDefaults
+ @Dependency(UserNotificationClient.self) var userNotifications
+ @Dependency(RemoteNotificationsClient.self) var registerForRemoteNotifications
+ @Dependency(UserDefaultsClient.self) var userDefaults
@ObservableState
public struct State {
@@ -64,7 +64,7 @@ public struct AppDelegateFeature {
else { return }
default: return
}
- await self.registerForRemoteNotifications()
+ await self.registerForRemoteNotifications.register()
}
}
}
diff --git a/Projects/App/Sources/MainTab/MainTabFeature.swift b/Projects/App/Sources/MainTab/MainTabFeature.swift
index c76bacb4..3da32829 100644
--- a/Projects/App/Sources/MainTab/MainTabFeature.swift
+++ b/Projects/App/Sources/MainTab/MainTabFeature.swift
@@ -17,11 +17,11 @@ import CoreKit
@Reducer
public struct MainTabFeature {
/// - Dependency
- @Dependency(\.pasteboard)
+ @Dependency(PasteboardClient.self)
private var pasteBoard
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
- @Dependency(\.userDefaults)
+ @Dependency(UserDefaultsClient.self)
private var userDefaults
/// - State
@ObservableState
diff --git a/Projects/App/Sources/MainTab/MainTabFeatureView.swift b/Projects/App/Sources/MainTab/MainTabFeatureView.swift
index 5fcc9cee..abe6d5d6 100644
--- a/Projects/App/Sources/MainTab/MainTabFeatureView.swift
+++ b/Projects/App/Sources/MainTab/MainTabFeatureView.swift
@@ -159,15 +159,15 @@ private extension MainTabView {
PokitHeaderItems(placement: .trailing) {
PokitToolbarButton(
.icon(.search),
- action: { store.send(.pokit(.view(.searchButtonTapped))) }
+ action: { store.send(.pokit(.view(.검색_버튼_눌렀을때))) }
)
PokitToolbarButton(
.icon(.bell),
- action: { store.send(.pokit(.view(.alertButtonTapped))) }
+ action: { store.send(.pokit(.view(.알람_버튼_눌렀을때))) }
)
PokitToolbarButton(
.icon(.setup),
- action: { store.send(.pokit(.view(.settingButtonTapped))) }
+ action: { store.send(.pokit(.view(.설정_버튼_눌렀을때))) }
)
}
}
@@ -185,11 +185,11 @@ private extension MainTabView {
PokitHeaderItems(placement: .trailing) {
PokitToolbarButton(
.icon(.search),
- action: { store.send(.remind(.view(.searchButtonTapped))) }
+ action: { store.send(.remind(.view(.검색_버튼_눌렀을때))) }
)
PokitToolbarButton(
.icon(.bell),
- action: { store.send(.remind(.view(.bellButtonTapped))) }
+ action: { store.send(.remind(.view(.알림_버튼_눌렀을때))) }
)
}
}
diff --git a/Projects/App/Sources/MainTab/MainTabPath.swift b/Projects/App/Sources/MainTab/MainTabPath.swift
index d8bea9c7..d61bc262 100644
--- a/Projects/App/Sources/MainTab/MainTabPath.swift
+++ b/Projects/App/Sources/MainTab/MainTabPath.swift
@@ -145,7 +145,7 @@ public extension MainTabFeature {
case .pokit:
return .send(.pokit(.delegate(.미분류_카테고리_컨텐츠_조회)))
case .remind:
- return .send(.remind(.delegate(.컨텐츠목록_조회)))
+ return .send(.remind(.delegate(.컨텐츠_상세보기_delegate_위임)))
}
}
switch lastPath {
diff --git a/Projects/App/Sources/Root/RootFeature.swift b/Projects/App/Sources/Root/RootFeature.swift
index 38310860..a31a69de 100644
--- a/Projects/App/Sources/Root/RootFeature.swift
+++ b/Projects/App/Sources/Root/RootFeature.swift
@@ -8,12 +8,13 @@
import Foundation
import ComposableArchitecture
+import FeatureLogin
import CoreKit
@Reducer
public struct RootFeature {
- @Dependency(\.userDefaults) var userDefaults
- @Dependency(\.userClient) var userClient
+ @Dependency(UserDefaultsClient.self) var userDefaults
+ @Dependency(UserClient.self) var userClient
@Reducer(state: .equatable)
public enum Destination {
diff --git a/Projects/App/Sources/Root/RootView.swift b/Projects/App/Sources/Root/RootView.swift
index 0f0024d4..748e5057 100644
--- a/Projects/App/Sources/Root/RootView.swift
+++ b/Projects/App/Sources/Root/RootView.swift
@@ -8,6 +8,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureLogin
import DSKit
public struct RootView: View {
diff --git a/Projects/CoreKit/Project.swift b/Projects/CoreKit/Project.swift
index ea0dd934..2679bc2b 100644
--- a/Projects/CoreKit/Project.swift
+++ b/Projects/CoreKit/Project.swift
@@ -37,19 +37,10 @@ let coreKit: Target = .target(
.external(name: "KakaoSDKShare"),
.external(name: "KakaoSDKTemplate"),
],
- settings: .settings(
- base: [
- "OTHER_LDFLAGS": "$(inherited) -ObjC",
- ],
- configurations: [
- .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfig/Secret.xcconfig")),
- .release(name: "Release", xcconfig: .relativeToRoot("xcconfig/Secret.xcconfig"))
- ]
- )
+ settings: .settings()
)
let project = Project(
name: "CoreKit",
- settings: .settings,
targets: [coreKit]
)
diff --git a/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift b/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
index ea05b643..037792af 100644
--- a/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
+++ b/Projects/CoreKit/Sources/CoreNetwork/TokenInterceptor.swift
@@ -13,8 +13,8 @@ import Dependencies
/// AccessToken값을 헤더에 넣어주거나, refreshToken으로 Intercept해주는 Interceptor
public final class TokenInterceptor: RequestInterceptor {
- @Dependency(\.keychain) var keychain
- @Dependency(\.authClient) var authClient
+ @Dependency(KeychainClient.self) var keychain
+ @Dependency(AuthClient.self) var authClient
static let shared = TokenInterceptor()
private init() {}
diff --git a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift
new file mode 100644
index 00000000..a27d8a0a
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient+LiveKey.swift
@@ -0,0 +1,82 @@
+//
+// KakaoShareClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import UIKit.UIApplication
+
+import Dependencies
+import KakaoSDKCommon
+import KakaoSDKShare
+import KakaoSDKTemplate
+
+extension KakaoShareClient: DependencyKey {
+ public static var liveValue: KakaoShareClient {
+ return Self(
+ 카테고리_카카오톡_공유: { model in
+ /// 딥링크
+ let appLink = Link(
+ androidExecutionParams: [
+ "categoryId": "\(model.categoryId)"
+ ], iosExecutionParams: [
+ "categoryId": "\(model.categoryId)"
+ ]
+ )
+
+ /// 카카오톡 메세지의 앱 이동 버튼
+ let button = Button(
+ title: "앱에서 보기",
+ link: appLink
+ )
+
+ /// 카카오톡 메세지 내용
+ let content = Content(
+ title: "\(model.categoryName) 포킷을 공유받았어요!",
+ imageUrl: URL(string: model.imageURL),
+ description: "소중한 링크들이 담긴 포킷을 Pokit 앱에서 지금 바로 확인해보세요!",
+ link: appLink
+ )
+
+ /// 피드 템플릿
+ let template = FeedTemplate(
+ content: content,
+ buttons: [button]
+ )
+
+ 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
+ }
+
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
+ return
+ }
+
+ let serverCallbackArgs = ["categoryId": "\(model.categoryId)"]
+
+ ShareApi.shared.shareDefault(
+ templateObject: templateJsonObject,
+ serverCallbackArgs: serverCallbackArgs
+ ) { linkResult, error in
+ if let error = error {
+ print("error : \(error)")
+ } else {
+ print("defaultLink(templateObject:templateJsonObject) success.")
+ guard let linkResult else { return }
+ UIApplication.shared.open(
+ linkResult.url,
+ options: [:],
+ completionHandler: nil
+ )
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient.swift b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient.swift
index d1adb5f8..27c47c98 100644
--- a/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/KakaoSDK/Share/KakaoShareClient.swift
@@ -7,91 +7,11 @@
import UIKit
-import Dependencies
-import KakaoSDKCommon
-import KakaoSDKShare
-import KakaoSDKTemplate
+import DependenciesMacros
+@DependencyClient
public struct KakaoShareClient {
- public var 카테고리_카카오톡_공유: (
- _ model: CategoryKaKaoShareModel
- ) -> Void
-}
-
-extension KakaoShareClient: DependencyKey {
- public static var liveValue: KakaoShareClient {
- return Self(
- 카테고리_카카오톡_공유: { model in
- /// 딥링크
- let appLink = Link(
- androidExecutionParams: [
- "categoryId": "\(model.categoryId)"
- ], iosExecutionParams: [
- "categoryId": "\(model.categoryId)"
- ]
- )
-
- /// 카카오톡 메세지의 앱 이동 버튼
- let button = Button(
- title: "앱에서 보기",
- link: appLink
- )
-
- /// 카카오톡 메세지 내용
- let content = Content(
- title: "\(model.categoryName) 포킷을 공유받았어요!",
- imageUrl: URL(string: model.imageURL),
- description: "소중한 링크들이 담긴 포킷을 Pokit 앱에서 지금 바로 확인해보세요!",
- link: appLink
- )
-
- /// 피드 템플릿
- let template = FeedTemplate(
- content: content,
- buttons: [button]
- )
-
- 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
- }
-
- UIApplication.shared.open(url, options: [:], completionHandler: nil)
- return
- }
-
- let serverCallbackArgs = ["categoryId": "\(model.categoryId)"]
-
- ShareApi.shared.shareDefault(
- templateObject: templateJsonObject,
- serverCallbackArgs: serverCallbackArgs
- ) { linkResult, error in
- if let error = error {
- print("error : \(error)")
- } else {
- print("defaultLink(templateObject:templateJsonObject) success.")
- guard let linkResult else { return }
- UIApplication.shared.open(
- linkResult.url,
- options: [:],
- completionHandler: nil
- )
- }
- }
- }
- )
- }
-}
-
-extension DependencyValues {
- public var kakaoShareClient: KakaoShareClient {
- get { self[KakaoShareClient.self] }
- set { self[KakaoShareClient.self] = newValue }
- }
+ public var 카테고리_카카오톡_공유: (_ model: CategoryKaKaoShareModel) -> Void
}
diff --git a/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient+LiveKey.swift
new file mode 100644
index 00000000..a447f9aa
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient+LiveKey.swift
@@ -0,0 +1,131 @@
+//
+// KeychainClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension KeychainClient: DependencyKey {
+ public static let liveValue: Self = {
+ let controller = KeychainController()
+ return Self(
+ save: {
+ let data = $1.data(using: .utf8)
+ if let _ = controller.read($0) {
+ controller.update(data, key: $0)
+ return
+ }
+ controller.create(data, key: $0)
+ },
+ read: {
+ guard let tokenData = controller.read($0) else { return nil }
+ return String(data: tokenData, encoding: .utf8)
+ },
+ delete: {
+ controller.delete($0)
+ }
+ )
+ }()
+}
+private struct KeychainController {
+ let service: String = "Pokit"
+ let group: String = "group.com.pokitmons.pokit"
+
+ func create(_ data: Data?, key: KeychainKey) {
+ guard let data = data else {
+ print("🗝️ '\(key)' 값이 없어요.")
+ return
+ }
+
+ let query: NSDictionary = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: service,
+ kSecAttrAccount: key.rawValue,
+ kSecValueData: data,
+ kSecAttrAccessGroup as String: group
+ ]
+
+ let status = SecItemAdd(query, nil)
+ guard status == errSecSuccess else {
+ print("🗝️ '\(key)' 상태 = \(status)")
+ return
+ }
+ print("🗝️ '\(key)' 성공!")
+ }
+
+ // MARK: Read Item
+
+ func read(_ key: KeychainKey) -> Data? {
+ let query: NSDictionary = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: service,
+ kSecAttrAccount: key.rawValue,
+ kSecMatchLimit: kSecMatchLimitOne,
+ kSecReturnData: true,
+ kSecAttrAccessGroup as String: group
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query, &result)
+ guard status != errSecItemNotFound else {
+ print("🗝️ '\(key)' 항목을 찾을 수 없어요.")
+ return nil
+ }
+ guard status == errSecSuccess else {
+ return nil
+ }
+ print("🗝️ '\(key)' 성공!")
+ return result as? Data
+ }
+
+ // MARK: Update Item
+
+ func update(_ data: Data?, key: KeychainKey) {
+ guard let data = data else {
+ print("🗝️ '\(key)' 값이 없어요.")
+ return
+ }
+
+ let query: NSDictionary = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: service,
+ kSecAttrAccount: key.rawValue,
+ kSecAttrAccessGroup as String: group
+ ]
+ let attributes: NSDictionary = [
+ kSecValueData: data
+ ]
+
+ let status = SecItemUpdate(query, attributes)
+ guard status == errSecSuccess else {
+ print("🗝️ '\(key)' 상태 = \(status)")
+ return
+ }
+ print("🗝️ '\(key)' 성공!")
+ }
+
+ // MARK: Delete Item
+
+ func delete(_ key: KeychainKey) {
+ let query: NSDictionary = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: service,
+ kSecAttrAccount: key.rawValue,
+ kSecAttrAccessGroup as String: group
+ ]
+
+ let status = SecItemDelete(query)
+ guard status != errSecItemNotFound else {
+ print("🗝️ '\(key)' 항목을 찾을 수 없어요.")
+ return
+ }
+ guard status == errSecSuccess else {
+ return
+ }
+ print("🗝️ '\(key)' 성공!")
+ }
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient.swift b/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient.swift
index 85a2b72f..7688f463 100644
--- a/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/Keychain/KeychainClient.swift
@@ -7,7 +7,7 @@
import Foundation
-import Dependencies
+import DependenciesMacros
public enum KeychainKey: String {
case accessToken
@@ -15,134 +15,10 @@ public enum KeychainKey: String {
case serverRefresh
}
-// MARK: - Dependency Values
-extension DependencyValues {
- public var keychain: KeychainClient {
- get { self[KeychainClient.self] }
- set { self[KeychainClient.self] = newValue }
- }
-}
-
-// MARK: - KeychainClient Client
+@DependencyClient
public struct KeychainClient {
public var save: @Sendable (KeychainKey, String) -> Void
- public var read: @Sendable (KeychainKey) -> (String?)
+ public var read: @Sendable (KeychainKey) -> (String?) = { _ in nil }
public var delete: @Sendable (KeychainKey) -> Void
}
-extension KeychainClient: DependencyKey {
- public static let liveValue: Self = {
- let controller = KeychainController()
- return Self(
- save: {
- let data = $1.data(using: .utf8)
- if let _ = controller.read($0) {
- controller.update(data, key: $0)
- return
- }
- controller.create(data, key: $0)
- },
- read: {
- guard let tokenData = controller.read($0) else { return nil }
- return String(data: tokenData, encoding: .utf8)
- },
- delete: {
- controller.delete($0)
- }
- )
- }()
-}
-
-private struct KeychainController {
- let service: String = "Pokit"
-
- func create(_ data: Data?, key: KeychainKey) {
- guard let data = data else {
- print("🗝️ '\(key)' 값이 없어요.")
- return
- }
-
- let query: NSDictionary = [
- kSecClass: kSecClassGenericPassword,
- kSecAttrService: service,
- kSecAttrAccount: key.rawValue,
- kSecValueData: data
- ]
-
- let status = SecItemAdd(query, nil)
- guard status == errSecSuccess else {
- print("🗝️ '\(key)' 상태 = \(status)")
- return
- }
- print("🗝️ '\(key)' 성공!")
- }
-
- // MARK: Read Item
-
- func read(_ key: KeychainKey) -> Data? {
- let query: NSDictionary = [
- kSecClass: kSecClassGenericPassword,
- kSecAttrService: service,
- kSecAttrAccount: key.rawValue,
- kSecMatchLimit: kSecMatchLimitOne,
- kSecReturnData: true
- ]
-
- var result: AnyObject?
- let status = SecItemCopyMatching(query, &result)
- guard status != errSecItemNotFound else {
- print("🗝️ '\(key)' 항목을 찾을 수 없어요.")
- return nil
- }
- guard status == errSecSuccess else {
- return nil
- }
- print("🗝️ '\(key)' 성공!")
- return result as? Data
- }
-
- // MARK: Update Item
-
- func update(_ data: Data?, key: KeychainKey) {
- guard let data = data else {
- print("🗝️ '\(key)' 값이 없어요.")
- return
- }
-
- let query: NSDictionary = [
- kSecClass: kSecClassGenericPassword,
- kSecAttrService: service,
- kSecAttrAccount: key.rawValue
- ]
- let attributes: NSDictionary = [
- kSecValueData: data
- ]
-
- let status = SecItemUpdate(query, attributes)
- guard status == errSecSuccess else {
- print("🗝️ '\(key)' 상태 = \(status)")
- return
- }
- print("🗝️ '\(key)' 성공!")
- }
-
- // MARK: Delete Item
-
- func delete(_ key: KeychainKey) {
- let query: NSDictionary = [
- kSecClass: kSecClassGenericPassword,
- kSecAttrService: service,
- kSecAttrAccount: key.rawValue
- ]
-
- let status = SecItemDelete(query)
- guard status != errSecItemNotFound else {
- print("🗝️ '\(key)' 항목을 찾을 수 없어요.")
- return
- }
- guard status == errSecSuccess else {
- return
- }
- print("🗝️ '\(key)' 성공!")
- }
-}
diff --git a/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+LiveKey.swift
index b3598b56..7c8dc941 100644
--- a/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+LiveKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+LiveKey.swift
@@ -1,6 +1,6 @@
//
-// Live.swift
-// App
+// PasteboardClient+LiveKey.swift
+// CoreKit
//
// Created by 김민호 on 8/2/24.
//
@@ -13,48 +13,38 @@ import Dependencies
extension PasteboardClient: DependencyKey {
public static let liveValue: Self = {
.init(
- changes: { UIPasteboard.general.changes },
- probableWebURL: { () -> URL? in
- do {
- let pasteboard = UIPasteboard.general
-
- guard pasteboard.hasURLs else { return nil }
-
- let detectedPatterns = try await pasteboard
- .detectedPatterns(for: [\.probableWebURL])
-
- guard detectedPatterns.contains(\.probableWebURL) else { return nil }
-
- return pasteboard.url
- } catch { throw error }
- }
+ changes: { UIPasteboard.general.changes },
+ probableWebURL: { () -> URL? in
+ do {
+ let pasteboard = UIPasteboard.general
+
+ guard pasteboard.hasURLs else { return nil }
+
+ let detectedPatterns = try await pasteboard
+ .detectedPatterns(for: [\.probableWebURL])
+
+ guard detectedPatterns.contains(\.probableWebURL) else { return nil }
+
+ return pasteboard.url
+ } catch { throw error }
+ }
)
- }()
-
- public static let previewValue = Self.noop
-}
-
-extension PasteboardClient {
- public static let noop = Self(
- changes: { .finished },
- probableWebURL: { nil }
-
- )
+ }()
}
extension UIPasteboard {
- var changes: AsyncStream {
- Publishers.Merge(
- NotificationCenter.default
- .publisher(for: UIPasteboard.changedNotification)
- .compactMap { [weak self] _ in self?.changeCount },
- NotificationCenter.default
- .publisher(for: UIApplication.didBecomeActiveNotification)
- .compactMap { [weak self] _ in self?.changeCount }
- )
- .removeDuplicates()
- .map { _ in }
- .values
- .eraseToStream()
- }
+ var changes: AsyncStream {
+ Publishers.Merge(
+ NotificationCenter.default
+ .publisher(for: UIPasteboard.changedNotification)
+ .compactMap { [weak self] _ in self?.changeCount },
+ NotificationCenter.default
+ .publisher(for: UIApplication.didBecomeActiveNotification)
+ .compactMap { [weak self] _ in self?.changeCount }
+ )
+ .removeDuplicates()
+ .map { _ in }
+ .values
+ .eraseToStream()
+ }
}
diff --git a/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+TestKey.swift
new file mode 100644
index 00000000..8ab35a87
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient+TestKey.swift
@@ -0,0 +1,22 @@
+//
+// PasteboardClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension PasteboardClient: TestDependencyKey {
+ public static let previewValue = Self.noop
+}
+
+extension PasteboardClient {
+ public static let noop = Self(
+ changes: { .finished },
+ probableWebURL: { nil }
+
+ )
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient.swift b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient.swift
index 936a48c9..e6d7790c 100644
--- a/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/PasteboardClient/PasteboardClient.swift
@@ -1,21 +1,15 @@
//
-// Interface.swift
-// App
+// PasteboardClient.swift
+// CoreKit
//
// Created by 김민호 on 8/2/24.
//
import Foundation
-import Dependencies
-
-extension DependencyValues {
- public var pasteboard: PasteboardClient {
- get { self[PasteboardClient.self] }
- set { self[PasteboardClient.self] = newValue }
- }
-}
+import DependenciesMacros
+@DependencyClient
public struct PasteboardClient: Sendable {
- public var changes: @Sendable () -> AsyncStream
- public var probableWebURL: @Sendable () async throws -> URL?
+ public var changes: @Sendable () -> AsyncStream = { .finished }
+ public var probableWebURL: @Sendable () async throws -> URL?
}
diff --git a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+LiveKey.swift
index a57391fc..47035e79 100644
--- a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+LiveKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+LiveKey.swift
@@ -5,14 +5,14 @@
// Created by 김민호 on 7/5/24.
//
-//import Dependencies
-//import UIKit
-//
-//@available(iOSApplicationExtension, unavailable)
-//extension RemoteNotificationsClient: DependencyKey {
-// public static let liveValue = Self(
-// isRegistered: { await UIApplication.shared.isRegisteredForRemoteNotifications },
-// register: { await UIApplication.shared.registerForRemoteNotifications() },
-// unregister: { await UIApplication.shared.unregisterForRemoteNotifications() }
-// )
-//}
+import UIKit
+
+import Dependencies
+
+extension RemoteNotificationsClient: DependencyKey {
+ public static let liveValue = Self(
+ isRegistered: { await UIApplication.shared.isRegisteredForRemoteNotifications },
+ register: { await UIApplication.shared.registerForRemoteNotifications() },
+ unregister: { await UIApplication.shared.unregisterForRemoteNotifications() }
+ )
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+TestKey.swift
index 3a86b3ff..7b234733 100644
--- a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+TestKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient+TestKey.swift
@@ -8,31 +8,15 @@
import UIKit
import Dependencies
-extension DependencyValues {
- public var remoteNotifications: RemoteNotificationsClient {
- get { self[RemoteNotificationsClient.self] }
- set { self[RemoteNotificationsClient.self] = newValue }
- }
-}
-
-@available(iOSApplicationExtension, unavailable)
-extension RemoteNotificationsClient: DependencyKey {
- public static let liveValue = Self(
- isRegistered: { await UIApplication.shared.isRegisteredForRemoteNotifications },
- register: { await UIApplication.shared.registerForRemoteNotifications() },
- unregister: { await UIApplication.shared.unregisterForRemoteNotifications() }
- )
-}
-
extension RemoteNotificationsClient: TestDependencyKey {
- public static let previewValue = Self.noop
- public static let testValue = Self()
+ public static let previewValue = Self.noop
+ public static let testValue = Self()
}
extension RemoteNotificationsClient {
- public static let noop = Self(
- isRegistered: { true },
- register: {},
- unregister: {}
- )
+ public static let noop = Self(
+ isRegistered: { true },
+ register: {},
+ unregister: {}
+ )
}
diff --git a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient.swift b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient.swift
index 49929ea1..27196f00 100644
--- a/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/RemoteNotificationClient/RemoteNotificationsClient.swift
@@ -9,7 +9,7 @@ import DependenciesMacros
@DependencyClient
public struct RemoteNotificationsClient {
- public var isRegistered: @Sendable () async -> Bool = { false }
- public var register: @Sendable () async -> Void
- public var unregister: @Sendable () async -> Void
+ public var isRegistered: @Sendable () async -> Bool = { false }
+ public var register: @Sendable () async -> Void
+ public var unregister: @Sendable () async -> Void
}
diff --git a/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/GoogleLoginController.swift b/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/GoogleLoginController.swift
index 90ed61df..cea8809b 100644
--- a/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/GoogleLoginController.swift
+++ b/Projects/CoreKit/Sources/Data/Client/SocialLogin/Controller/GoogleLoginController.swift
@@ -10,23 +10,29 @@ import GoogleSignIn
public final class GoogleLoginController {
private var continuation: CheckedContinuation?
+ private var root: UIViewController?
@MainActor
public func login() async throws -> SocialLoginInfo {
-
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
googleSignIn()
}
}
+ public func setRootViewController(_ root: UIViewController?) {
+ self.root = root
+ }
+
private func googleSignIn() {
- guard let root = UIApplication.shared.rootViewController else {
+ let rootViewController = root ?? UIApplication.shared.rootViewController
+
+ guard let rootViewController else {
continuation?.resume(throwing: SocialLoginError.transferError("Root view does not exist."))
return
}
- GIDSignIn.sharedInstance.signIn(withPresenting: root) { [weak self] signInResult, error in
+ GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { [weak self] signInResult, error in
guard let self else { return }
if let error {
continuation?.resume(throwing: error)
diff --git a/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+LiveKey.swift
new file mode 100644
index 00000000..8b2e4c3c
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+LiveKey.swift
@@ -0,0 +1,30 @@
+//
+// SocialLoginClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+
+extension SocialLoginClient: DependencyKey {
+ public static let liveValue: Self = {
+ let appleLoginController = AppleLoginController()
+ let googleLoginController = GoogleLoginController()
+
+ return Self(
+ appleLogin: {
+ try await appleLoginController.login()
+ },
+ googleLogin: {
+ try await googleLoginController.login()
+ },
+ getClientSceret: {
+ return appleLoginController.makeJWT()
+ },
+ setRootViewController: { rootViewController in
+ return googleLoginController.setRootViewController(rootViewController)
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+TestKey.swift
new file mode 100644
index 00000000..1a116c61
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient+TestKey.swift
@@ -0,0 +1,22 @@
+//
+// SocialLoginClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension SocialLoginClient: TestDependencyKey {
+ public static let testValue = Self()
+ public static let previewValue: Self = {
+ Self(
+ appleLogin: { .appleMock },
+ googleLogin: { .googleMock },
+ getClientSceret: { "" },
+ setRootViewController: { _ in }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient.swift b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient.swift
index 3663de96..b6bee519 100644
--- a/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/SocialLogin/SocialLoginClient.swift
@@ -6,44 +6,13 @@
//
import Foundation
-import Dependencies
+import DependenciesMacros
+import UIKit
+@DependencyClient
public struct SocialLoginClient {
public var appleLogin: @Sendable () async throws -> SocialLoginInfo
public var googleLogin: @Sendable () async throws -> SocialLoginInfo
- public var getClientSceret: @Sendable () -> String
-}
-
-extension SocialLoginClient: DependencyKey {
- public static let liveValue: Self = {
- let appleLoginController = AppleLoginController()
- let googleLoginController = GoogleLoginController()
-
- return Self(
- appleLogin: {
- try await appleLoginController.login()
- },
- googleLogin: {
- try await googleLoginController.login()
- },
- getClientSceret: {
- return appleLoginController.makeJWT()
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- appleLogin: { .appleMock },
- googleLogin: { .googleMock },
- getClientSceret: { "" }
- )
- }()
-}
-
-extension DependencyValues {
- public var socialLogin: SocialLoginClient {
- get { self[SocialLoginClient.self] }
- set { self[SocialLoginClient.self] = newValue }
- }
+ public var getClientSceret: @Sendable () -> String = { "" }
+ public var setRootViewController: @Sendable (UIViewController?) -> Void
}
diff --git a/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+LiveKey.swift
new file mode 100644
index 00000000..f4649b8d
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+LiveKey.swift
@@ -0,0 +1,26 @@
+//
+// SwiftSoupClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import SwiftSoup
+
+extension SwiftSoupClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = SwiftSoupProvider()
+
+ return Self(
+ parseOGTitle: { url in
+ try await provider.parseOGTitle(url)
+ },
+ parseOGImageURL: { url in
+ try await provider.parseOGImageURL(url)
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+TestKey.swift
new file mode 100644
index 00000000..b940b95b
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient+TestKey.swift
@@ -0,0 +1,14 @@
+//
+// SwiftSoupClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension SwiftSoupClient: TestDependencyKey {
+ public static let testValue = Self()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient.swift b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient.swift
index 60e70d18..6f2a18ab 100644
--- a/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupClient.swift
@@ -7,63 +7,15 @@
import Foundation
-import SwiftSoup
-import Dependencies
+import DependenciesMacros
+@DependencyClient
public struct SwiftSoupClient {
- public var parseOGTitleAndImage: @Sendable (
- _ url: URL,
- _ completion: @Sendable () async -> Void
- ) async -> (String?, String?)
-}
-
-extension SwiftSoupClient: DependencyKey {
- public static let liveValue: Self = {
- return Self(
- parseOGTitleAndImage: { url, completion in
- guard let html = try? String(contentsOf: url),
- let document = try? SwiftSoup.parse(html) else {
- await completion()
- return (nil, nil)
- }
-
- let title = try? document.select("meta[property=og:title]").first()?.attr("content")
- let imageURL = try? document.select("meta[property=og:image]").first()?.attr("content")
-
- guard title != nil || imageURL != nil else {
- var request = URLRequest(url: url)
- request.setValue(
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
- forHTTPHeaderField: "User-Agent"
- )
-
- guard let data = try? await URLSession.shared.data(for: request).0,
- let html = String(data: data, encoding: .utf8),
- let document = try? SwiftSoup.parse(html) else {
- return (nil, nil)
- }
-
- let title = try? document.select("meta[property=og:title]").first()?.attr("content")
- let imageURL = try? document.select("meta[property=og:image]").first()?.attr("content")
-
- await completion()
-
- return (title, imageURL)
- }
-
- await completion()
-
- return (title, imageURL)
- }
- )
- }()
+ public var parseOGTitle: @Sendable (
+ _ url: URL
+ ) async throws -> String? = { _ in nil }
- public static let previewValue: Self = liveValue
-}
-
-public extension DependencyValues {
- var swiftSoup: SwiftSoupClient {
- get { self[SwiftSoupClient.self] }
- set { self[SwiftSoupClient.self] = newValue }
- }
+ public var parseOGImageURL: @Sendable (
+ _ url: URL
+ ) async throws -> String? = { _ in nil }
}
diff --git a/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupProvider.swift b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupProvider.swift
new file mode 100644
index 00000000..8a4e1613
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/SwiftSoupClient/SwiftSoupProvider.swift
@@ -0,0 +1,43 @@
+//
+// SwiftSoupProvider.swift
+// CoreKit
+//
+// Created by 김도형 on 11/17/24.
+//
+
+import SwiftUI
+import SwiftSoup
+
+final class SwiftSoupProvider {
+ func parseOGTitle(_ url: URL) async throws -> String? {
+ try await parseOGMeta(url: url, type: "og:title")
+ }
+
+ func parseOGImageURL(_ url: URL) async throws -> String? {
+ try await parseOGMeta(url: url, type: "og:image")
+ }
+
+ func parseOGMeta(url: URL, type: String) async throws -> String? {
+ let html = try String(contentsOf: url)
+ let document = try SwiftSoup.parse(html)
+
+ if let metaData = try document.select("meta[property=\(type)]").first()?.attr("content") {
+ return metaData
+ } else {
+ var request = URLRequest(url: url)
+ request.setValue(
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
+ forHTTPHeaderField: "User-Agent"
+ )
+
+ let (data, _) = try await URLSession.shared.data(for: request)
+ guard let html = String(data: data, encoding: .utf8) else {
+ return nil
+ }
+ let document = try SwiftSoup.parse(html)
+ let metaData = try document.select("meta[property=\(type)]").first()?.attr("content")
+
+ return metaData
+ }
+ }
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift
deleted file mode 100644
index 46fa0066..00000000
--- a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaults.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-//
-// UserDefaults.swift
-// Data
-//
-// Created by 김민호 on 7/30/24.
-//
-
-import Foundation
-
-import Dependencies
-
-extension DependencyValues {
- public var userDefaults: UserDefaultsClient {
- get { self[UserDefaultsClient.self] }
- set { self[UserDefaultsClient.self] = newValue }
- }
-}
-
-public struct UserDefaultsClient {
- public var boolKey: @Sendable (UserDefaultsKey.BoolKey) -> Bool = { _ in false }
- public var stringKey: @Sendable (UserDefaultsKey.StringKey) -> String? = { _ in "" }
- public var stringArrayKey: @Sendable (UserDefaultsKey.ArrayKey) -> [String]? = { _ in [] }
- public var removeBool: @Sendable (UserDefaultsKey.BoolKey) async -> Void
- public var removeString: @Sendable (UserDefaultsKey.StringKey) async -> Void
- public var removeStringArray: @Sendable (UserDefaultsKey.ArrayKey) async -> Void
- public var setBool: @Sendable (Bool, UserDefaultsKey.BoolKey) async -> Void
- public var setString: @Sendable (String, UserDefaultsKey.StringKey) async -> Void
- public var setStringArray: @Sendable ([String], UserDefaultsKey.ArrayKey) async -> Void
-}
-
-extension UserDefaultsClient: DependencyKey {
- public static var liveValue: Self = {
- let defaults = { UserDefaults.standard }
-
- return Self(
- boolKey: { defaults().bool(forKey: $0.rawValue) },
- stringKey: { defaults().string(forKey: $0.rawValue) },
- stringArrayKey: { defaults().stringArray(forKey: $0.rawValue) },
-
- removeBool: { defaults().removeObject(forKey: $0.rawValue) },
- removeString: { defaults().removeObject(forKey: $0.rawValue) },
- removeStringArray: { defaults().removeObject(forKey: $0.rawValue) },
-
- setBool: { defaults().set($0, forKey: $1.rawValue) },
- setString: { defaults().set($0, forKey: $1.rawValue) },
- setStringArray: { defaults().set($0, forKey: $1.rawValue) }
- )
- }()
-
- public static let testValue: Self = {
- Self(
- removeBool: { _ in },
- removeString: { _ in },
- removeStringArray: { _ in },
- setBool: { _, _ in },
- setString: { _, _ in },
- setStringArray: {_, _ in }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+LiveKey.swift
new file mode 100644
index 00000000..e2d01633
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+LiveKey.swift
@@ -0,0 +1,30 @@
+//
+// UserDefaultsClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension UserDefaultsClient: DependencyKey {
+ public static var liveValue: Self = {
+ let defaults = { UserDefaults(suiteName: "group.com.pokitmons.pokit") }
+
+ return Self(
+ boolKey: { defaults()?.bool(forKey: $0.rawValue) ?? false },
+ stringKey: { defaults()?.string(forKey: $0.rawValue) },
+ stringArrayKey: { defaults()?.stringArray(forKey: $0.rawValue) },
+
+ removeBool: { defaults()?.removeObject(forKey: $0.rawValue) },
+ removeString: { defaults()?.removeObject(forKey: $0.rawValue) },
+ removeStringArray: { defaults()?.removeObject(forKey: $0.rawValue) },
+
+ setBool: { defaults()?.set($0, forKey: $1.rawValue) },
+ setString: { defaults()?.set($0, forKey: $1.rawValue) },
+ setStringArray: { defaults()?.set($0, forKey: $1.rawValue) }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+TestKey.swift
new file mode 100644
index 00000000..bf168731
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient+TestKey.swift
@@ -0,0 +1,14 @@
+//
+// UserDefaultsClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension UserDefaultsClient: TestDependencyKey {
+ public static var testValue = Self()
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient.swift
new file mode 100644
index 00000000..951d766e
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsClient.swift
@@ -0,0 +1,23 @@
+//
+// UserDefaultsClient.swift
+// CoreKit
+//
+// Created by 김민호 on 7/30/24.
+//
+
+import Foundation
+
+import DependenciesMacros
+
+@DependencyClient
+public struct UserDefaultsClient {
+ public var boolKey: @Sendable (UserDefaultsKey.BoolKey) -> Bool = { _ in false }
+ public var stringKey: @Sendable (UserDefaultsKey.StringKey) -> String? = { _ in "" }
+ public var stringArrayKey: @Sendable (UserDefaultsKey.ArrayKey) -> [String]? = { _ in [] }
+ public var removeBool: @Sendable (UserDefaultsKey.BoolKey) async -> Void
+ public var removeString: @Sendable (UserDefaultsKey.StringKey) async -> Void
+ public var removeStringArray: @Sendable (UserDefaultsKey.ArrayKey) async -> Void
+ public var setBool: @Sendable (Bool, UserDefaultsKey.BoolKey) async -> Void
+ public var setString: @Sendable (String, UserDefaultsKey.StringKey) async -> Void
+ public var setStringArray: @Sendable ([String], UserDefaultsKey.ArrayKey) async -> Void
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift
index 51360900..3a894784 100644
--- a/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/UserDefaults/UserDefaultsKey.swift
@@ -1,6 +1,6 @@
//
// UserDefaultsKey.swift
-// Data
+// CoreKit
//
// Created by 김민호 on 7/30/24.
//
diff --git a/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+LiveKey.swift
new file mode 100644
index 00000000..e249da8a
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+LiveKey.swift
@@ -0,0 +1,99 @@
+//
+// UserNotificationClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import UserNotifications
+import Dependencies
+
+extension UserNotificationClient: DependencyKey {
+ public static let liveValue = Self(
+ add: { try await UNUserNotificationCenter.current().add($0) },
+ delegate: {
+ AsyncStream { continuation in
+ let delegate = Delegate(continuation: continuation)
+ UNUserNotificationCenter.current().delegate = delegate
+ continuation.onTermination = { _ in
+ _ = delegate
+ }
+ }
+ },
+ getNotificationSettings: {
+ await Notification.Settings(
+ rawValue: UNUserNotificationCenter.current().notificationSettings()
+ )
+ },
+ removeDeliveredNotificationsWithIdentifiers: {
+ UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: $0)
+ },
+ removePendingNotificationRequestsWithIdentifiers: {
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: $0)
+ },
+ requestAuthorization: {
+ try await UNUserNotificationCenter.current().requestAuthorization(options: $0)
+ }
+ )
+}
+
+extension UserNotificationClient.Notification {
+ public init(rawValue: UNNotification) {
+ self.date = rawValue.date
+ self.request = rawValue.request
+ }
+}
+
+extension UserNotificationClient.Notification.Response {
+ public init(rawValue: UNNotificationResponse) {
+ self.notification = .init(rawValue: rawValue.notification)
+ }
+}
+
+extension UserNotificationClient.Notification.Settings {
+ public init(rawValue: UNNotificationSettings) {
+ self.authorizationStatus = rawValue.authorizationStatus
+ }
+}
+
+extension UserNotificationClient {
+ fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate {
+ let continuation: AsyncStream.Continuation
+
+ init(continuation: AsyncStream.Continuation) {
+ self.continuation = continuation
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ didReceive response: UNNotificationResponse,
+ withCompletionHandler completionHandler: @escaping () -> Void
+ ) {
+ self.continuation.yield(
+ .didReceiveResponse(.init(rawValue: response)) { completionHandler() }
+ )
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ openSettingsFor notification: UNNotification?
+ ) {
+ self.continuation.yield(
+ .openSettingsForNotification(notification.map(Notification.init(rawValue:)))
+ )
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler:
+ @escaping (UNNotificationPresentationOptions) -> Void
+ ) {
+ self.continuation.yield(
+ .willPresentNotification(.init(rawValue: notification)) { completionHandler($0) }
+ )
+ }
+ }
+}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+TestKey.swift
index ec844eaf..ea7b74c1 100644
--- a/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+TestKey.swift
+++ b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient+TestKey.swift
@@ -6,15 +6,10 @@
//
import Dependencies
-//import Combine
-import ComposableArchitecture
-import UserNotifications
-extension DependencyValues {
- public var userNotifications: UserNotificationClient {
- get { self[UserNotificationClient.self] }
- set { self[UserNotificationClient.self] = newValue }
- }
+extension UserNotificationClient: TestDependencyKey {
+ public static let previewValue = Self.noop
+ public static let testValue = Self()
}
extension UserNotificationClient {
@@ -28,95 +23,4 @@ extension UserNotificationClient {
)
}
-extension UserNotificationClient: DependencyKey {
- public static let liveValue = Self(
- add: { try await UNUserNotificationCenter.current().add($0) },
- delegate: {
- AsyncStream { continuation in
- let delegate = Delegate(continuation: continuation)
- UNUserNotificationCenter.current().delegate = delegate
- continuation.onTermination = { _ in
- _ = delegate
- }
- }
- },
- getNotificationSettings: {
- await Notification.Settings(
- rawValue: UNUserNotificationCenter.current().notificationSettings()
- )
- },
- removeDeliveredNotificationsWithIdentifiers: {
- UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: $0)
- },
- removePendingNotificationRequestsWithIdentifiers: {
- UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: $0)
- },
- requestAuthorization: {
- try await UNUserNotificationCenter.current().requestAuthorization(options: $0)
- }
- )
-}
-
-extension UserNotificationClient: TestDependencyKey {
- public static let previewValue = Self.noop
- public static let testValue = Self()
-}
-
-extension UserNotificationClient.Notification {
- public init(rawValue: UNNotification) {
- self.date = rawValue.date
- self.request = rawValue.request
- }
-}
-
-extension UserNotificationClient.Notification.Response {
- public init(rawValue: UNNotificationResponse) {
- self.notification = .init(rawValue: rawValue.notification)
- }
-}
-extension UserNotificationClient.Notification.Settings {
- public init(rawValue: UNNotificationSettings) {
- self.authorizationStatus = rawValue.authorizationStatus
- }
-}
-
-extension UserNotificationClient {
- fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate {
- let continuation: AsyncStream.Continuation
-
- init(continuation: AsyncStream.Continuation) {
- self.continuation = continuation
- }
-
- func userNotificationCenter(
- _ center: UNUserNotificationCenter,
- didReceive response: UNNotificationResponse,
- withCompletionHandler completionHandler: @escaping () -> Void
- ) {
- self.continuation.yield(
- .didReceiveResponse(.init(rawValue: response)) { completionHandler() }
- )
- }
-
- func userNotificationCenter(
- _ center: UNUserNotificationCenter,
- openSettingsFor notification: UNNotification?
- ) {
- self.continuation.yield(
- .openSettingsForNotification(notification.map(Notification.init(rawValue:)))
- )
- }
-
- func userNotificationCenter(
- _ center: UNUserNotificationCenter,
- willPresent notification: UNNotification,
- withCompletionHandler completionHandler:
- @escaping (UNNotificationPresentationOptions) -> Void
- ) {
- self.continuation.yield(
- .willPresentNotification(.init(rawValue: notification)) { completionHandler($0) }
- )
- }
- }
-}
diff --git a/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient.swift b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient.swift
index 971be1ea..64d8e411 100644
--- a/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient.swift
+++ b/Projects/CoreKit/Sources/Data/Client/UserNotificationClient/UserNotificationClient.swift
@@ -5,6 +5,7 @@
// Created by 김민호 on 7/4/24.
//
+import CasePaths
import ComposableArchitecture
import UserNotifications
diff --git a/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+LiveKey.swift
new file mode 100644
index 00000000..a1b723d4
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+LiveKey.swift
@@ -0,0 +1,26 @@
+//
+// AlertClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import Moya
+
+extension AlertClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = MoyaProvider.build()
+
+ return Self(
+ 알람_목록_조회: { model in
+ try await provider.request(.알람_목록_조회(model: model))
+ },
+ 알람_삭제: { alertId in
+ try await provider.requestNoBody(.알람_삭제(alertId: alertId))
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+TestKey.swift
new file mode 100644
index 00000000..30333db8
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient+TestKey.swift
@@ -0,0 +1,20 @@
+//
+// AlertClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import Moya
+
+extension AlertClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 알람_목록_조회: { _ in .mock },
+ 알람_삭제: { _ in }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient.swift b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient.swift
index 31639ebf..3eb885f5 100644
--- a/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Alert/AlertClient.swift
@@ -5,43 +5,12 @@
// Created by 김민호 on 8/17/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-// MARK: - Dependency Values
-extension DependencyValues {
- public var alertClient: AlertClient {
- get { self[AlertClient.self] }
- set { self[AlertClient.self] = newValue }
- }
-}
-/// 알람에 관련한 API를 처리하는 Client
+@DependencyClient
public struct AlertClient {
public var 알람_목록_조회: @Sendable (BasePageableRequest) async throws -> AlertListInquiryResponse
public var 알람_삭제: @Sendable (_ alertId: String) async throws -> Void
}
-extension AlertClient: DependencyKey {
- public static let liveValue: Self = {
- let provider = MoyaProvider.build()
-
- return Self(
- 알람_목록_조회: { model in
- try await provider.request(.알람_목록_조회(model: model))
- },
- 알람_삭제: { alertId in
- try await provider.requestNoBody(.알람_삭제(alertId: alertId))
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 알람_목록_조회: { _ in .mock },
- 알람_삭제: { _ in }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+LiveKey.swift
new file mode 100644
index 00000000..36b79dae
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+LiveKey.swift
@@ -0,0 +1,34 @@
+//
+// AuthClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+import Moya
+
+extension AuthClient: DependencyKey {
+ public static let liveValue: Self = {
+ let nonTokenProvider = MoyaProvider.buildNonToken()
+ let provider = MoyaProvider.build()
+
+ return Self(
+ 로그인: { model in
+ try await nonTokenProvider.request(.로그인(model))
+ },
+ 회원탈퇴: { model in
+ try await provider.requestNoBody(.회원탈퇴(model))
+ },
+ 토큰재발급: { model in
+ try await nonTokenProvider.request(.토큰재발급(model))
+ },
+ apple: { model in
+ try await nonTokenProvider.request(.apple(model))
+ },
+ appleRevoke: { refreshToken, model in
+ try await nonTokenProvider.requestNoBody(.appleRevoke(refreshToken, model))
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+TestKey.swift
new file mode 100644
index 00000000..a63b85a4
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient+TestKey.swift
@@ -0,0 +1,22 @@
+//
+// AuthClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension AuthClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 로그인: { _ in .mock },
+ 회원탈퇴: { _ in },
+ 토큰재발급: { _ in .mock },
+ apple: { _ in .init(refresh_token: "") },
+ appleRevoke: { _, _ in }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient.swift b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient.swift
index b0f40d8c..68b9bf55 100644
--- a/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Auth/AuthClient.swift
@@ -5,19 +5,9 @@
// Created by 김민호 on 7/30/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-// MARK: - Dependency Values
-extension DependencyValues {
- public var authClient: AuthClient {
- get { self[AuthClient.self] }
- set { self[AuthClient.self] = newValue }
- }
-}
-/// 유저정보에 관련한 API를 처리하는 Client
+@DependencyClient
public struct AuthClient {
public var 로그인: @Sendable (SignInRequest) async throws -> TokenResponse
public var 회원탈퇴: @Sendable (WithdrawRequest) async throws -> Void
@@ -26,38 +16,3 @@ public struct AuthClient {
public var appleRevoke: @Sendable (String, AppleTokenRequest) async throws -> Void
}
-extension AuthClient: DependencyKey {
- public static let liveValue: Self = {
- /// 로그인 api (자동로그인 / 로그인)용 access token이 필요없을 때 사용하는 provider
- let nonTokenProvider = MoyaProvider.buildNonToken()
- let provider = MoyaProvider.build()
-
- return Self(
- 로그인: { model in
- try await nonTokenProvider.request(.로그인(model))
- },
- 회원탈퇴: { model in
- try await provider.requestNoBody(.회원탈퇴(model))
- },
- 토큰재발급: { model in
- try await nonTokenProvider.request(.토큰재발급(model))
- },
- apple: { model in
- try await nonTokenProvider.request(.apple(model))
- },
- appleRevoke: { refreshToken, model in
- try await nonTokenProvider.requestNoBody(.appleRevoke(refreshToken, model))
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 로그인: { _ in .mock },
- 회원탈퇴: { _ in },
- 토큰재발급: { _ in .mock },
- apple: { _ in .init(refresh_token: "") },
- appleRevoke: { _, _ in }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift
new file mode 100644
index 00000000..58bb09c2
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+LiveKey.swift
@@ -0,0 +1,47 @@
+//
+// CategoryClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import Moya
+
+extension CategoryClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = MoyaProvider.build()
+
+ return Self(
+ 카테고리_삭제: { id in
+ try await provider.requestNoBody(.카테고리_삭제(categoryId: id))
+ },
+ 카테고리_수정: { id, model in
+ try await provider.request(.카테고리_수정(categoryId: id, model: model))
+ },
+ 카테고리_목록_조회: { model, categoryFilter in
+ try await provider.request(.카테고리_목록_조회(model: model, filterUncategorized: categoryFilter))
+ },
+ 카테고리_생성: { model in
+ try await provider.request(.카테고리생성(model: model))
+ },
+ 카테고리_프로필_목록_조회: {
+ try await provider.request(.카테고리_프로필_목록_조회)
+ },
+ 유저_카테고리_개수_조회: {
+ try await provider.request(.유저_카테고리_개수_조회)
+ },
+ 카테고리_상세_조회: { id in
+ try await provider.request(.카테고리_상세_조회(categoryId: id))
+ },
+ 공유받은_카테고리_조회: { id, model in
+ try await provider.request(.공유받은_카테고리_조회(categoryId: id, model: model))
+ },
+ 공유받은_카테고리_저장: { model in
+ try await provider.requestNoBody(.공유받은_카테고리_저장(model: model))
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift
new file mode 100644
index 00000000..6798f1b1
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient+TestKey.swift
@@ -0,0 +1,24 @@
+//
+// CategoryClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+
+extension CategoryClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 카테고리_삭제: { _ in },
+ 카테고리_수정: { _, _ in .mock },
+ 카테고리_목록_조회: { _, _ in .mock },
+ 카테고리_생성: { _ in .mock },
+ 카테고리_프로필_목록_조회: { CategoryImageResponse.mock },
+ 유저_카테고리_개수_조회: { .mock },
+ 카테고리_상세_조회: { _ in .mock },
+ 공유받은_카테고리_조회: { _, _ in .mock },
+ 공유받은_카테고리_저장: { _ in }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift
index ee31ac13..3bb41482 100644
--- a/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Category/CategoryClient.swift
@@ -5,18 +5,9 @@
// Created by 김민호 on 7/31/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-extension DependencyValues {
- public var categoryClient: CategoryClient {
- get { self[CategoryClient.self] }
- set { self[CategoryClient.self] = newValue }
- }
-}
-/// Category에 관련한 API를 처리하는 Client
+@DependencyClient
public struct CategoryClient {
public var 카테고리_삭제: @Sendable (_ categoryId: Int) async throws -> Void
public var 카테고리_수정: @Sendable (
@@ -43,52 +34,3 @@ public struct CategoryClient {
) async throws -> Void
}
-extension CategoryClient: DependencyKey {
- public static let liveValue: Self = {
- let provider = MoyaProvider.build()
-
- return Self(
- 카테고리_삭제: { id in
- try await provider.requestNoBody(.카테고리_삭제(categoryId: id))
- },
- 카테고리_수정: { id, model in
- try await provider.request(.카테고리_수정(categoryId: id, model: model))
- },
- 카테고리_목록_조회: { model, categoryFilter in
- try await provider.request(.카테고리_목록_조회(model: model, filterUncategorized: categoryFilter))
- },
- 카테고리_생성: { model in
- try await provider.request(.카테고리생성(model: model))
- },
- 카테고리_프로필_목록_조회: {
- try await provider.request(.카테고리_프로필_목록_조회)
- },
- 유저_카테고리_개수_조회: {
- try await provider.request(.유저_카테고리_개수_조회)
- },
- 카테고리_상세_조회: { id in
- try await provider.request(.카테고리_상세_조회(categoryId: id))
- },
- 공유받은_카테고리_조회: { id, model in
- try await provider.request(.공유받은_카테고리_조회(categoryId: id, model: model))
- },
- 공유받은_카테고리_저장: { model in
- try await provider.requestNoBody(.공유받은_카테고리_저장(model: model))
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 카테고리_삭제: { _ in },
- 카테고리_수정: { _, _ in .mock },
- 카테고리_목록_조회: { _, _ in .mock },
- 카테고리_생성: { _ in .mock },
- 카테고리_프로필_목록_조회: { CategoryImageResponse.mock },
- 유저_카테고리_개수_조회: { .mock },
- 카테고리_상세_조회: { _ in .mock },
- 공유받은_카테고리_조회: { _, _ in .mock },
- 공유받은_카테고리_저장: { _ in }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift
new file mode 100644
index 00000000..d5027625
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+LiveKey.swift
@@ -0,0 +1,56 @@
+//
+// ContentClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+import Moya
+
+extension ContentClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = MoyaProvider.build()
+
+ return Self(
+ 컨텐츠_삭제: { id in
+ try await provider.requestNoBody(.컨텐츠_삭제(contentId: id))
+ },
+ 컨텐츠_상세_조회: { id in
+ try await provider.request(.컨텐츠_상세_조회(contentId: id))
+ },
+ 컨텐츠_수정: { id, model in
+ try await provider.request(.컨텐츠_수정(contentId: id, model: model))
+ },
+ 컨텐츠_추가: { model in
+ try await provider.request(.컨텐츠_추가(model: model))
+ },
+ 즐겨찾기: { id in
+ try await provider.request(.즐겨찾기(contentId: id))
+ },
+ 즐겨찾기_취소: { id in
+ try await provider.requestNoBody(.즐겨찾기_취소(contentId: id))
+ },
+ 카테고리_내_컨텐츠_목록_조회: { id, pageable, condition in
+ try await provider.request(
+ .카태고리_내_컨텐츠_목록_조회(
+ contentId: id,
+ pageable: pageable,
+ condition: condition
+ )
+ )
+ },
+ 미분류_카테고리_컨텐츠_조회: { model in
+ try await provider.request(.미분류_카테고리_컨텐츠_조회(model: model))
+ },
+ 컨텐츠_검색: { pageable, condition in
+ try await provider.request(
+ .컨텐츠_검색(
+ pageable: pageable,
+ condition: condition
+ )
+ )
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift
new file mode 100644
index 00000000..7b22cf86
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient+TestKey.swift
@@ -0,0 +1,23 @@
+//
+// ContentClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+import Dependencies
+
+extension ContentClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 컨텐츠_삭제: { _ in },
+ 컨텐츠_상세_조회: { _ in .mock },
+ 컨텐츠_수정: { _, _ in .mock },
+ 컨텐츠_추가: { _ in .mock },
+ 즐겨찾기: { _ in .mock },
+ 즐겨찾기_취소: { _ in },
+ 카테고리_내_컨텐츠_목록_조회: { _, _, _ in .mock },
+ 미분류_카테고리_컨텐츠_조회: { _ in .mock },
+ 컨텐츠_검색: { _, _ in .mock }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
index 8c90e961..c7210f37 100644
--- a/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Content/ContentClient.swift
@@ -5,18 +5,9 @@
// Created by 김민호 on 7/31/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-extension DependencyValues {
- public var contentClient: ContentClient {
- get { self[ContentClient.self] }
- set { self[ContentClient.self] = newValue }
- }
-}
-/// Category에 관련한 API를 처리하는 Client
+@DependencyClient
public struct ContentClient {
public var 컨텐츠_삭제: @Sendable (
_ categoryId: String
@@ -51,63 +42,3 @@ public struct ContentClient {
) async throws -> ContentListInquiryResponse
}
-extension ContentClient: DependencyKey {
- public static let liveValue: Self = {
- let provider = MoyaProvider.build()
-
- return Self(
- 컨텐츠_삭제: { id in
- try await provider.requestNoBody(.컨텐츠_삭제(contentId: id))
- },
- 컨텐츠_상세_조회: { id in
- try await provider.request(.컨텐츠_상세_조회(contentId: id))
- },
- 컨텐츠_수정: { id, model in
- try await provider.request(.컨텐츠_수정(contentId: id, model: model))
- },
- 컨텐츠_추가: { model in
- try await provider.request(.컨텐츠_추가(model: model))
- },
- 즐겨찾기: { id in
- try await provider.request(.즐겨찾기(contentId: id))
- },
- 즐겨찾기_취소: { id in
- try await provider.requestNoBody(.즐겨찾기_취소(contentId: id))
- },
- 카테고리_내_컨텐츠_목록_조회: { id, pageable, condition in
- try await provider.request(
- .카태고리_내_컨텐츠_목록_조회(
- contentId: id,
- pageable: pageable,
- condition: condition
- )
- )
- },
- 미분류_카테고리_컨텐츠_조회: { model in
- try await provider.request(.미분류_카테고리_컨텐츠_조회(model: model))
- },
- 컨텐츠_검색: { pageable, condition in
- try await provider.request(
- .컨텐츠_검색(
- pageable: pageable,
- condition: condition
- )
- )
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 컨텐츠_삭제: { _ in },
- 컨텐츠_상세_조회: { _ in .mock },
- 컨텐츠_수정: { _, _ in .mock },
- 컨텐츠_추가: { _ in .mock },
- 즐겨찾기: { _ in .mock },
- 즐겨찾기_취소: { _ in },
- 카테고리_내_컨텐츠_목록_조회: { _, _, _ in .mock },
- 미분류_카테고리_컨텐츠_조회: { _ in .mock },
- 컨텐츠_검색: { _, _ in .mock }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+LiveKey.swift
new file mode 100644
index 00000000..cdf25659
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+LiveKey.swift
@@ -0,0 +1,35 @@
+//
+// RemindClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import Moya
+
+extension RemindClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = MoyaProvider.build()
+
+ return .init(
+ 오늘의_리마인드_조회: {
+ try await provider.request(.오늘의_리마인드_조회)
+ },
+ 읽지않음_컨텐츠_조회: { model in
+ try await provider.request(.읽지않음_컨텐츠_조회(model: model))
+ },
+ 즐겨찾기_링크모음_조회: { model in
+ try await provider.request(.즐겨찾기_링크모음_조회(model: model))
+ },
+ 읽지않음_컨텐츠_개수_조회: {
+ try await provider.request(.읽지않음_컨텐츠_개수_조회)
+ },
+ 즐겨찾기_컨텐츠_개수_조회: {
+ try await provider.request(.즐겨찾기_컨텐츠_개수_조회)
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+TestKey.swift
new file mode 100644
index 00000000..a5045376
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient+TestKey.swift
@@ -0,0 +1,20 @@
+//
+// RemindClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+
+extension RemindClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ .init(
+ 오늘의_리마인드_조회: { [.mock(id: 0), .mock(id: 1), .mock(id: 2)]},
+ 읽지않음_컨텐츠_조회: { _ in .mock },
+ 즐겨찾기_링크모음_조회: { _ in .mock },
+ 읽지않음_컨텐츠_개수_조회: { .mock },
+ 즐겨찾기_컨텐츠_개수_조회: { .mock }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient.swift b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient.swift
index ba3ea10f..b7ee3c8c 100644
--- a/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Remind/RemindClient.swift
@@ -5,11 +5,9 @@
// Created by 김도형 on 8/8/24.
//
-import Foundation
-
-import Dependencies
-import Moya
+import DependenciesMacros
+@DependencyClient
public struct RemindClient {
public var 오늘의_리마인드_조회: @Sendable ()
async throws -> [ContentBaseResponse]
@@ -23,46 +21,4 @@ public struct RemindClient {
async throws -> UnreadCountResponse
public var 즐겨찾기_컨텐츠_개수_조회: @Sendable ()
async throws -> BookmarkCountResponse
-
-}
-
-extension RemindClient: DependencyKey {
- public static let liveValue: Self = {
- let provider = MoyaProvider.build()
-
- return .init(
- 오늘의_리마인드_조회: {
- try await provider.request(.오늘의_리마인드_조회)
- },
- 읽지않음_컨텐츠_조회: { model in
- try await provider.request(.읽지않음_컨텐츠_조회(model: model))
- },
- 즐겨찾기_링크모음_조회: { model in
- try await provider.request(.즐겨찾기_링크모음_조회(model: model))
- },
- 읽지않음_컨텐츠_개수_조회: {
- try await provider.request(.읽지않음_컨텐츠_개수_조회)
- },
- 즐겨찾기_컨텐츠_개수_조회: {
- try await provider.request(.즐겨찾기_컨텐츠_개수_조회)
- }
- )
- }()
-
- public static let previewValue: Self = {
- .init(
- 오늘의_리마인드_조회: { [.mock(id: 0), .mock(id: 1), .mock(id: 2)]},
- 읽지않음_컨텐츠_조회: { _ in .mock },
- 즐겨찾기_링크모음_조회: { _ in .mock },
- 읽지않음_컨텐츠_개수_조회: { .mock },
- 즐겨찾기_컨텐츠_개수_조회: { .mock }
- )
- }()
-}
-
-extension DependencyValues {
- public var remindClient: RemindClient {
- get { self[RemindClient.self] }
- set { self[RemindClient.self] = newValue }
- }
}
diff --git a/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift
new file mode 100644
index 00000000..a268c605
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/User/UserClient+LiveKey.swift
@@ -0,0 +1,38 @@
+//
+// UserClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+import Moya
+
+extension UserClient: DependencyKey {
+ public static let liveValue: Self = {
+ let provider = MoyaProvider.build()
+
+ return Self(
+ 닉네임_수정: { model in
+ try await provider.request(.닉네임_수정(model: model))
+ },
+ 회원등록: { model in
+ try await provider.request(.회원등록(model: model))
+ },
+ 닉네임_중복_체크: { nickname in
+ try await provider.request(.닉네임_중복_체크(nickname: nickname))
+ },
+ 관심사_목록_조회: {
+ try await provider.request(.관심사_목록_조회)
+ },
+ 닉네임_조회: {
+ try await provider.request(.닉네임_조회)
+ },
+ fcm_토큰_저장: { model in
+ try await provider.request(.fcm_토큰_저장(model: model))
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/User/UserClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/User/UserClient+TestKey.swift
new file mode 100644
index 00000000..2f3d37c8
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/User/UserClient+TestKey.swift
@@ -0,0 +1,23 @@
+//
+// UserClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Foundation
+
+import Dependencies
+
+extension UserClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 닉네임_수정: { _ in .mock },
+ 회원등록: { _ in .mock },
+ 닉네임_중복_체크: { _ in .mock },
+ 관심사_목록_조회: { InterestResponse.mock },
+ 닉네임_조회: { .mock },
+ fcm_토큰_저장: { _ in .mock }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/User/UserClient.swift b/Projects/CoreKit/Sources/Data/Network/User/UserClient.swift
index 64fab6cc..7ccb2149 100644
--- a/Projects/CoreKit/Sources/Data/Network/User/UserClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/User/UserClient.swift
@@ -5,18 +5,9 @@
// Created by 김민호 on 7/31/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-extension DependencyValues {
- public var userClient: UserClient {
- get { self[UserClient.self] }
- set { self[UserClient.self] = newValue }
- }
-}
-/// Category에 관련한 API를 처리하는 Client
+@DependencyClient
public struct UserClient {
public var 닉네임_수정: @Sendable (_ model: NicknameEditRequest) async throws -> BaseUserResponse
public var 회원등록: @Sendable (_ model: SignupRequest) async throws -> BaseUserResponse
@@ -25,41 +16,3 @@ public struct UserClient {
public var 닉네임_조회: @Sendable () async throws -> BaseUserResponse
public var fcm_토큰_저장: @Sendable (_ model: FCMRequest) async throws -> FCMResponse
}
-
-extension UserClient: DependencyKey {
- public static let liveValue: Self = {
- let provider = MoyaProvider.build()
-
- return Self(
- 닉네임_수정: { model in
- try await provider.request(.닉네임_수정(model: model))
- },
- 회원등록: { model in
- try await provider.request(.회원등록(model: model))
- },
- 닉네임_중복_체크: { nickname in
- try await provider.request(.닉네임_중복_체크(nickname: nickname))
- },
- 관심사_목록_조회: {
- try await provider.request(.관심사_목록_조회)
- },
- 닉네임_조회: {
- try await provider.request(.닉네임_조회)
- },
- fcm_토큰_저장: { model in
- try await provider.request(.fcm_토큰_저장(model: model))
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 닉네임_수정: { _ in .mock },
- 회원등록: { _ in .mock },
- 닉네임_중복_체크: { _ in .mock },
- 관심사_목록_조회: { InterestResponse.mock },
- 닉네임_조회: { .mock },
- fcm_토큰_저장: { _ in .mock }
- )
- }()
-}
diff --git a/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+LiveKey.swift b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+LiveKey.swift
new file mode 100644
index 00000000..f0010c4e
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+LiveKey.swift
@@ -0,0 +1,21 @@
+//
+// VersionClient+LiveKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+import Moya
+
+extension VersionClient: DependencyKey {
+ public static let liveValue: Self = {
+ let nonProvider = MoyaProvider.buildNonToken()
+
+ return Self(
+ 버전체크: {
+ try await nonProvider.request(.버전체크)
+ }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+TestKey.swift b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+TestKey.swift
new file mode 100644
index 00000000..f470384b
--- /dev/null
+++ b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient+TestKey.swift
@@ -0,0 +1,16 @@
+//
+// VersionClient+TestKey.swift
+// CoreKit
+//
+// Created by 김민호 on 9/30/24.
+//
+
+import Dependencies
+
+extension VersionClient: TestDependencyKey {
+ public static let previewValue: Self = {
+ Self(
+ 버전체크: { .mock }
+ )
+ }()
+}
diff --git a/Projects/CoreKit/Sources/Data/Network/Version/VersionClient.swift b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient.swift
index ba97bf3b..ef3d6d6c 100644
--- a/Projects/CoreKit/Sources/Data/Network/Version/VersionClient.swift
+++ b/Projects/CoreKit/Sources/Data/Network/Version/VersionClient.swift
@@ -5,38 +5,10 @@
// Created by 김민호 on 9/10/24.
//
-import Foundation
+import DependenciesMacros
-import Dependencies
-import Moya
-
-// MARK: - Dependency Values
-extension DependencyValues {
- public var versionClient: VersionClient {
- get { self[VersionClient.self] }
- set { self[VersionClient.self] = newValue }
- }
-}
-/// 알람에 관련한 API를 처리하는 Client
+@DependencyClient
public struct VersionClient {
public var 버전체크: @Sendable () async throws -> VersionResponse
}
-extension VersionClient: DependencyKey {
- public static let liveValue: Self = {
- let nonProvider = MoyaProvider.buildNonToken()
-
- return Self(
- 버전체크: {
- try await nonProvider.request(.버전체크)
- }
- )
- }()
-
- public static let previewValue: Self = {
- Self(
- 버전체크: { .mock }
- )
- }()
-}
-
diff --git a/Projects/DSKit/Project.swift b/Projects/DSKit/Project.swift
index 0c0dd71c..eddcf7c3 100644
--- a/Projects/DSKit/Project.swift
+++ b/Projects/DSKit/Project.swift
@@ -26,7 +26,8 @@ let project = Project(
// TODO: 의존성 추가
.project(target: "Util", path: .relativeToRoot("Projects/Util")),
.external(name: "NukeUI")
- ]
+ ],
+ settings: .settings()
)
]
)
diff --git a/Projects/DSKit/Sources/Components/PokitLinkCard.swift b/Projects/DSKit/Sources/Components/PokitLinkCard.swift
index 5dad4313..a3d5a5be 100644
--- a/Projects/DSKit/Sources/Components/PokitLinkCard.swift
+++ b/Projects/DSKit/Sources/Components/PokitLinkCard.swift
@@ -14,15 +14,18 @@ public struct PokitLinkCard: View {
private let link: Item
private let action: () -> Void
private let kebabAction: (() -> Void)?
+ private let fetchMetaData: (() -> Void)?
public init(
link: Item,
action: @escaping () -> Void,
- kebabAction: (() -> Void)? = nil
+ kebabAction: (() -> Void)? = nil,
+ fetchMetaData: (() -> Void)? = nil
) {
self.link = link
self.action = action
self.kebabAction = kebabAction
+ self.fetchMetaData = fetchMetaData
}
public var body: some View {
@@ -34,7 +37,11 @@ public struct PokitLinkCard: View {
@MainActor
private var buttonLabel: some View {
HStack(spacing: 12) {
- thumbleNail
+ if let url = URL(string: link.thumbNail) {
+ thumbleNail(url: url)
+ } else {
+ placeholder
+ }
VStack(spacing: 8) {
HStack {
@@ -105,27 +112,24 @@ public struct PokitLinkCard: View {
}
@MainActor
- private var thumbleNail: some View {
- LazyImage(url: .init(string: link.thumbNail)) { phase in
+ private func thumbleNail(url: URL) -> some View {
+ LazyImage(url: url) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
+ } else if phase.error != nil {
+ placeholder
+ .onAppear { fetchMetaData?() }
} else {
- ZStack {
- Color.pokit(.bg(.disable))
-
- PokitSpinner()
- .foregroundStyle(.pokit(.icon(.brand)))
- .frame(width: 48, height: 48)
- }
+ placeholder
}
}
.animation(.pokitDissolve, value: phase.image)
+ .frame(width: 124, height: 94)
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
- .frame(width: 124, height: 94)
- .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
private var divider: some View {
@@ -134,6 +138,18 @@ public struct PokitLinkCard: View {
.frame(height: 1)
}
+ private var placeholder: some View {
+ ZStack {
+ Color.pokit(.bg(.disable))
+
+ PokitSpinner()
+ .foregroundStyle(.pokit(.icon(.brand)))
+ .frame(width: 48, height: 48)
+ }
+ .frame(width: 124, height: 94)
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+ }
+
@ViewBuilder
public func divider(isFirst: Bool, isLast: Bool) -> some View {
let edge: Edge.Set = isFirst ? .bottom : isLast ? .top : .vertical
diff --git a/Projects/DSKit/Sources/Components/PokitLinkPreview.swift b/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
index 406ccb98..ea17a12c 100644
--- a/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
+++ b/Projects/DSKit/Sources/Components/PokitLinkPreview.swift
@@ -41,6 +41,7 @@ public struct PokitLinkPreview: View {
if let image = phase.image {
image
.resizable()
+ .aspectRatio(contentMode: .fill)
} else {
PokitSpinner()
.foregroundStyle(.pokit(.icon(.brand)))
@@ -50,6 +51,7 @@ public struct PokitLinkPreview: View {
.animation(.pokitDissolve, value: phase.image)
}
.frame(width: 124, height: 108)
+ .clipped()
info(title: title)
diff --git a/Projects/Domain/Project.swift b/Projects/Domain/Project.swift
index 02662fef..baec5764 100644
--- a/Projects/Domain/Project.swift
+++ b/Projects/Domain/Project.swift
@@ -26,7 +26,7 @@ let project = Project(
.project(target: "SharedThirdPartyLib", path: .relativeToRoot("Projects/SharedThirdPartyLib")),
.project(target: "CoreKit", path: .relativeToRoot("Projects/CoreKit"))
],
- settings: .settings
+ settings: .settings()
)
]
)
diff --git a/Projects/Domain/Sources/Base/BaseContentItem.swift b/Projects/Domain/Sources/Base/BaseContentItem.swift
index f9ed88f1..59c93b12 100644
--- a/Projects/Domain/Sources/Base/BaseContentItem.swift
+++ b/Projects/Domain/Sources/Base/BaseContentItem.swift
@@ -14,7 +14,7 @@ public struct BaseContentItem: Identifiable, Equatable, PokitLinkCardItem, Sorta
public let categoryName: String
public let categoryId: Int
public let title: String
- public let thumbNail: String
+ public var thumbNail: String
public let data: String
public let domain: String
public let createdAt: String
diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
index 66a5bb7b..5de0c984 100644
--- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
+++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailFeature.swift
@@ -7,6 +7,7 @@
import Foundation
import ComposableArchitecture
+import FeatureContentCard
import Domain
import CoreKit
import DSKit
@@ -17,13 +18,13 @@ public struct CategoryDetailFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.pasteboard)
+ @Dependency(PasteboardClient.self)
private var pasteboard
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
- @Dependency(\.kakaoShareClient)
+ @Dependency(KakaoShareClient.self)
private var kakaoShareClient
/// - State
@ObservableState
@@ -51,16 +52,7 @@ public struct CategoryDetailFeature {
}
return identifiedArray
}
- var contents: IdentifiedArrayOf? {
- guard let contentList = domain.contentList.data else {
- return nil
- }
- var identifiedArray = IdentifiedArrayOf()
- contentList.forEach { content in
- identifiedArray.append(content)
- }
- return identifiedArray
- }
+ var contents: IdentifiedArrayOf = []
var kebobSelectedType: PokitDeleteBottomSheet.SheetType?
var selectedContentItem: BaseContentItem?
var shareSheetItem: BaseContentItem? = nil
@@ -73,6 +65,7 @@ public struct CategoryDetailFeature {
var hasNext: Bool {
domain.contentList.hasNext
}
+ var isLoading: Bool = true
public init(category: BaseCategoryItem) {
self.domain = .init(categpry: category)
@@ -86,45 +79,47 @@ public struct CategoryDetailFeature {
case async(AsyncAction)
case scope(ScopeAction)
case delegate(DelegateAction)
+ case contents(IdentifiedActionOf)
@CasePathable
public enum View: BindableAction, Equatable {
- /// - Binding
case binding(BindingAction)
- /// - Button Tapped
- case categoryKebobButtonTapped(PokitDeleteBottomSheet.SheetType, selectedItem: BaseContentItem?)
- case categorySelectButtonTapped
- case categorySelected(BaseCategoryItem)
- case filterButtonTapped
- case contentItemTapped(BaseContentItem)
case dismiss
- case onAppear
case pagenation
- case 링크_공유_완료
+ case 카테고리_케밥_버튼_눌렀을때(PokitDeleteBottomSheet.SheetType, selectedItem: BaseContentItem?)
+ case 카테고리_선택_버튼_눌렀을때
+ case 카테고리_선택했을때(BaseCategoryItem)
+ case 필터_버튼_눌렀을때
+ case 컨텐츠_항목_눌렀을때(BaseContentItem)
+ case 뷰가_나타났을때
+ case 링크_공유_완료되었을때
}
public enum InnerAction: Equatable {
- case pokitCategorySheetPresented(Bool)
- case pokitCategorySelectSheetPresented(Bool)
- case pokitDeleteSheetPresented(Bool)
- case 카테고리_목록_조회_결과(BaseCategoryListInquiry)
- case 카테고리_내_컨텐츠_목록_갱신(BaseContentListInquiry)
- case 컨텐츠_삭제_반영(id: Int)
- case pagenation_네트워크_결과(BaseContentListInquiry)
+ case 카테고리_시트_활성화(Bool)
+ case 카테고리_선택_시트_활성화(Bool)
+ case 카테고리_삭제_시트_활성화(Bool)
+
+ case 카테고리_목록_조회_API_반영(BaseCategoryListInquiry)
+ case 카테고리_내_컨텐츠_목록_조회_API_반영(BaseContentListInquiry)
+ case 컨텐츠_삭제_API_반영(id: Int)
+ case pagenation_API_반영(BaseContentListInquiry)
case pagenation_초기화
}
public enum AsyncAction: Equatable {
- case 카테고리_내_컨텐츠_목록_조회
- case 컨텐츠_삭제(id: Int)
- case pagenation_네트워크
+ case 카테고리_내_컨텐츠_목록_조회_API
+ case 카테고리_목록_조회_API
+ case 컨텐츠_삭제_API(id: Int)
case 페이징_재조회
+ case 클립보드_감지
}
- public enum ScopeAction: Equatable {
+ public enum ScopeAction {
case categoryBottomSheet(PokitBottomSheet.Delegate)
case categoryDeleteBottomSheet(PokitDeleteBottomSheet.Delegate)
case filterBottomSheet(CategoryFilterSheet.Delegate)
+ case contents(IdentifiedActionOf)
}
public enum DelegateAction: Equatable {
@@ -163,6 +158,9 @@ public struct CategoryDetailFeature {
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
+
+ case .contents(let contentsAction):
+ return .send(.scope(.contents(contentsAction)))
}
}
@@ -170,6 +168,9 @@ public struct CategoryDetailFeature {
public var body: some ReducerOf {
BindingReducer(action: \.view)
Reduce(self.core)
+ .forEach(\.contents, action: \.contents) {
+ ContentCardFeature()
+ }
}
}
//MARK: - FeatureAction Effect
@@ -180,47 +181,44 @@ private extension CategoryDetailFeature {
case .binding:
return .none
- case let .categoryKebobButtonTapped(selectedType, selectedItem):
+ case let .카테고리_케밥_버튼_눌렀을때(selectedType, selectedItem):
state.kebobSelectedType = selectedType
state.selectedContentItem = selectedItem
- return .run { send in await send(.inner(.pokitCategorySheetPresented(true))) }
+ return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
- case .categorySelectButtonTapped:
- return .send(.inner(.pokitCategorySelectSheetPresented(true)))
+ case .카테고리_선택_버튼_눌렀을때:
+ return .send(.inner(.카테고리_선택_시트_활성화(true)))
- case .categorySelected(let item):
- state.domain.contentList.data = nil
+ case .카테고리_선택했을때(let item):
state.domain.category = item
return .run { send in
- await send(.inner(.pagenation_초기화))
- await send(.inner(.pokitCategorySelectSheetPresented(false)))
+ await send(.inner(.pagenation_초기화), animation: .pokitDissolve)
+ await send(.async(.카테고리_내_컨텐츠_목록_조회_API))
+ await send(.inner(.카테고리_선택_시트_활성화(false)))
}
- case .filterButtonTapped:
+ case .필터_버튼_눌렀을때:
state.isFilterSheetPresented.toggle()
return .none
- case .contentItemTapped(let selectedItem):
+ case .컨텐츠_항목_눌렀을때(let selectedItem):
return .run { send in await send(.delegate(.contentItemTapped(selectedItem))) }
case .dismiss:
return .run { _ in await dismiss() }
- case .onAppear:
- return .run { send in
- let request = BasePageableRequest(page: 0, size: 30, sort: ["createdAt,desc"])
- let response = try await categoryClient.카테고리_목록_조회(request, true).toDomain()
- await send(.async(.카테고리_내_컨텐츠_목록_조회))
- await send(.inner(.카테고리_목록_조회_결과(response)))
-
- for await _ in self.pasteboard.changes() {
- let url = try await pasteboard.probableWebURL()
- await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
- }
- }
+ case .뷰가_나타났을때:
+ /// 단순 조회 액션들의 나열이기 때문에 merge로 우선 처리
+ return .merge(
+ .send(.async(.카테고리_내_컨텐츠_목록_조회_API)),
+ .send(.async(.카테고리_목록_조회_API)),
+ .send(.async(.클립보드_감지))
+ )
case .pagenation:
- return .run { send in await send(.async(.pagenation_네트워크)) }
- case .링크_공유_완료:
+ state.domain.pageable.page += 1
+ return .send(.async(.카테고리_내_컨텐츠_목록_조회_API))
+
+ case .링크_공유_완료되었을때:
state.shareSheetItem = nil
return .none
}
@@ -229,85 +227,98 @@ private extension CategoryDetailFeature {
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case let .pokitCategorySheetPresented(presented):
+ case let .카테고리_시트_활성화(presented):
state.isCategorySheetPresented = presented
return .none
- case let .pokitDeleteSheetPresented(presented):
+ case let .카테고리_삭제_시트_활성화(presented):
state.isPokitDeleteSheetPresented = presented
return .none
- case let .pokitCategorySelectSheetPresented(presented):
+ case let .카테고리_선택_시트_활성화(presented):
state.isCategorySelectSheetPresented = presented
return .none
- case let .카테고리_목록_조회_결과(response):
+ 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 }
state.domain.category = first
return .none
- case .카테고리_내_컨텐츠_목록_갱신(let contentList):
+
+ case .카테고리_내_컨텐츠_목록_조회_API_반영(let contentList):
state.domain.contentList = contentList
+
+ var identifiedArray = IdentifiedArrayOf()
+ contentList.data?.forEach { identifiedArray.append(.init(content: $0)) }
+ state.contents = identifiedArray
+
+ state.isLoading = false
return .none
- case .컨텐츠_삭제_반영(id: let id):
+
+ 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_네트워크_결과(let contentList):
+ case .pagenation_API_반영(let contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
state.domain.contentList = contentList
state.domain.contentList.data = list + newList
+ newList.forEach { state.contents.append(.init(content: $0)) }
return .none
+
case .pagenation_초기화:
state.domain.pageable.page = 0
state.domain.contentList.data = nil
- return .send(.async(.카테고리_내_컨텐츠_목록_조회))
+ state.isLoading = true
+ state.contents.removeAll()
+ return .none
}
}
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .카테고리_내_컨텐츠_목록_조회:
+ case .카테고리_목록_조회_API:
+ return .run { send in
+ let request = BasePageableRequest(page: 0, size: 30, sort: ["createdAt,desc"])
+ let response = try await categoryClient.카테고리_목록_조회(request, true).toDomain()
+ await send(.inner(.카테고리_목록_조회_API_반영(response)))
+ }
+
+ case .카테고리_내_컨텐츠_목록_조회_API:
return .run { [
id = state.domain.category.id,
pageable = state.domain.pageable,
condition = state.domain.condition
] send in
+ let request = BasePageableRequest(
+ page: pageable.page,
+ size: pageable.size,
+ sort: pageable.sort
+ )
+ let conditionRequest = BaseConditionRequest(categoryIds: condition.categoryIds, isRead: condition.isUnreadFlitered, favorites: condition.isFavoriteFlitered)
let contentList = try await contentClient.카테고리_내_컨텐츠_목록_조회(
- "\(id)",
- BasePageableRequest(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- ),
- BaseConditionRequest(
- categoryIds: condition.categoryIds,
- isRead: condition.isUnreadFlitered,
- favorites: condition.isFavoriteFlitered
- )
+ "\(id)", request, conditionRequest
).toDomain()
- if pageable.page == 0 {
- await send(.inner(.카테고리_내_컨텐츠_목록_갱신(contentList)), animation: .pokitDissolve)
- } else {
- await send(.inner(.pagenation_네트워크_결과(contentList)))
- }
+ pageable.page == 0
+ ? await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentList)), animation: .pokitDissolve)
+ : await send(.inner(.pagenation_API_반영(contentList)))
}
- case .컨텐츠_삭제(id: let id):
- return .run { [id] send in
- let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring)
+
+ case let .컨텐츠_삭제_API(contentId):
+ return .run { send in
+ let _ = try await contentClient.컨텐츠_삭제("\(contentId)")
+ await send(.inner(.컨텐츠_삭제_API_반영(id: contentId)), animation: .pokitSpring)
}
- case .pagenation_네트워크:
- state.domain.pageable.page += 1
- return .send(.async(.카테고리_내_컨텐츠_목록_조회))
+
case .페이징_재조회:
return .run { [
pageable = state.domain.pageable,
@@ -345,7 +356,15 @@ private extension CategoryDetailFeature {
contentItems?.data = items + newItems
}
guard let contentItems else { return }
- await send(.inner(.카테고리_내_컨텐츠_목록_갱신(contentItems)), animation: .pokitSpring)
+ await send(.inner(.카테고리_내_컨텐츠_목록_조회_API_반영(contentItems)), animation: .pokitSpring)
+ }
+
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteboard.changes() {
+ let url = try await pasteboard.probableWebURL()
+ await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
+ }
}
}
}
@@ -384,18 +403,18 @@ private extension CategoryDetailFeature {
switch type {
case .링크삭제:
guard let content else { return }
- await send(.inner(.pokitCategorySheetPresented(false)))
+ await send(.inner(.카테고리_시트_활성화(false)))
await send(.delegate(.링크수정(contentId: content.id)))
case .포킷삭제:
- await send(.inner(.pokitCategorySheetPresented(false)))
+ await send(.inner(.카테고리_시트_활성화(false)))
await send(.delegate(.포킷수정(category)))
}
}
case .deleteCellButtonTapped:
return .run { send in
- await send(.inner(.pokitCategorySheetPresented(false)))
- await send(.inner(.pokitDeleteSheetPresented(true)))
+ await send(.inner(.카테고리_시트_활성화(false)))
+ await send(.inner(.카테고리_삭제_시트_활성화(true)))
}
default: return .none
@@ -404,7 +423,7 @@ private extension CategoryDetailFeature {
case .categoryDeleteBottomSheet(let delegateAction):
switch delegateAction {
case .cancelButtonTapped:
- return .run { send in await send(.inner(.pokitDeleteSheetPresented(false))) }
+ return .run { send in await send(.inner(.카테고리_삭제_시트_활성화(false))) }
case .deleteButtonTapped:
guard let selectedType = state.kebobSelectedType else {
@@ -419,12 +438,12 @@ private extension CategoryDetailFeature {
state.isPokitDeleteSheetPresented = false
return .none
}
- return .send(.async(.컨텐츠_삭제(id: selectedItem.id)))
+ 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(.pokitDeleteSheetPresented(false)))
+ await send(.inner(.카테고리_삭제_시트_활성화(false)))
await send(.delegate(.포킷삭제))
try await categoryClient.카테고리_삭제(categoryId)
}
@@ -433,10 +452,10 @@ private extension CategoryDetailFeature {
/// - 필터 버튼을 눌렀을 때
case .filterBottomSheet(let delegateAction):
switch delegateAction {
- case .dismissButtonTapped:
+ case .dismiss:
state.isFilterSheetPresented.toggle()
return .none
- case let .okButtonTapped(type, bookMarkSelected, unReadSelected):
+ case let .확인_버튼_눌렀을때(type, bookMarkSelected, unReadSelected):
state.isFilterSheetPresented.toggle()
state.domain.pageable.sort = [
type == .최신순 ? "createdAt,desc" : "createdAt,asc"
@@ -444,8 +463,20 @@ private extension CategoryDetailFeature {
state.sortType = type
state.domain.condition.isFavoriteFlitered = bookMarkSelected
state.domain.condition.isUnreadFlitered = unReadSelected
- return .send(.inner(.pagenation_초기화), animation: .pokitDissolve)
+ return .concatenate(
+ .send(.inner(.pagenation_초기화), animation: .pokitDissolve),
+ .send(.async(.카테고리_내_컨텐츠_목록_조회_API))
+ )
}
+
+ 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)))
+ case .contents:
+ return .none
}
}
diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
index 1cbc0fb5..5a066f7e 100644
--- a/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
+++ b/Projects/Feature/FeatureCategoryDetail/Sources/CategoryDetailView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureContentCard
import Domain
import DSKit
import Util
@@ -45,7 +46,7 @@ public extension CategoryDetailView {
if let shareURL = URL(string: content.data) {
PokitShareSheet(
items: [shareURL],
- completion: { send(.링크_공유_완료) }
+ completion: { send(.링크_공유_완료되었을때) }
)
.presentationDetents([.medium, .large])
}
@@ -55,7 +56,7 @@ public extension CategoryDetailView {
PokitCategorySheet(
selectedItem: nil,
list: categories.elements,
- action: { send(.categorySelected($0)) }
+ action: { send(.카테고리_선택했을때($0)) }
)
.presentationDragIndicator(.visible)
} else {
@@ -77,7 +78,7 @@ public extension CategoryDetailView {
delegateSend: { store.send(.scope(.filterBottomSheet($0))) }
)
}
- .task { await send(.onAppear).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
@@ -92,7 +93,10 @@ private extension CategoryDetailView {
)
}
PokitHeaderItems(placement: .trailing) {
- PokitToolbarButton(.icon(.kebab), action: { send(.categoryKebobButtonTapped(.포킷삭제, selectedItem: nil)) })
+ PokitToolbarButton(
+ .icon(.kebab),
+ action: { send(.카테고리_케밥_버튼_눌렀을때(.포킷삭제, selectedItem: nil)) }
+ )
}
}
.padding(.top, 8)
@@ -102,7 +106,7 @@ private extension CategoryDetailView {
VStack(spacing: 4) {
HStack(spacing: 8) {
/// cateogry title
- Button(action: { send(.categorySelectButtonTapped) }) {
+ Button(action: { send(.카테고리_선택_버튼_눌렀을때) }) {
Text(store.category.categoryName)
.foregroundStyle(.pokit(.text(.primary)))
.pokitFont(.title1)
@@ -125,7 +129,7 @@ private extension CategoryDetailView {
state: .filled(.primary),
size: .small,
shape: .round,
- action: { send(.filterButtonTapped) }
+ action: { send(.필터_버튼_눌렀을때) }
)
}
}
@@ -133,8 +137,8 @@ private extension CategoryDetailView {
var contentScrollView: some View {
Group {
- if let contents = store.contents {
- if contents.isEmpty {
+ if !store.isLoading {
+ if store.contents.isEmpty {
VStack {
PokitCaution(
image: .empty,
@@ -148,17 +152,17 @@ private extension CategoryDetailView {
} else {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 0) {
- ForEach(contents) { content in
- let isFirst = content == contents.first
- let isLast = content == contents.last
+ ForEach(
+ 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
- PokitLinkCard(
- link: content,
- action: { send(.contentItemTapped(content)) },
- kebabAction: { send(.categoryKebobButtonTapped(.링크삭제, selectedItem: content)) }
+ ContentCardView(
+ store: store,
+ isFirst: isFirst,
+ isLast: isLast
)
- .divider(isFirst: isFirst, isLast: isLast)
- .pokitScrollTransition(.opacity)
}
if store.hasNext {
diff --git a/Projects/Feature/FeatureCategoryDetail/Sources/Sheet/CategoryFilterSheet.swift b/Projects/Feature/FeatureCategoryDetail/Sources/Sheet/CategoryFilterSheet.swift
index 7eee5210..110c63e8 100644
--- a/Projects/Feature/FeatureCategoryDetail/Sources/Sheet/CategoryFilterSheet.swift
+++ b/Projects/Feature/FeatureCategoryDetail/Sources/Sheet/CategoryFilterSheet.swift
@@ -66,7 +66,7 @@ private extension CategoryFilterSheet {
Spacer()
}
.overlay(alignment: .topTrailing) {
- Button(action: { delegateSend?(.dismissButtonTapped) }) {
+ Button(action: { delegateSend?(.dismiss) }) {
Image(.icon(.x))
}
.buttonStyle(.plain)
@@ -105,7 +105,7 @@ private extension CategoryFilterSheet {
"확인",
state: .filled(.primary),
action: { delegateSend?(
- .okButtonTapped(
+ .확인_버튼_눌렀을때(
self.sortType,
bookMarkSelected: self.isBookMarkSelected,
unReadSelected: self.isUnReadSelected
@@ -174,8 +174,8 @@ private extension CategoryFilterSheet {
//MARK: - Delegate
public extension CategoryFilterSheet {
enum Delegate: Equatable {
- case dismissButtonTapped
- case okButtonTapped(SortType, bookMarkSelected: Bool, unReadSelected: Bool)
+ case dismiss
+ case 확인_버튼_눌렀을때(SortType, bookMarkSelected: Bool, unReadSelected: Bool)
}
}
//MARK: - Preview
diff --git a/Projects/Feature/FeatureCategoryDetailTests/Resources/info.plist b/Projects/Feature/FeatureCategoryDetailTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureCategoryDetailTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureCategoryDetailTests/Sources/FeatureCategoryDetailTests.swift b/Projects/Feature/FeatureCategoryDetailTests/Sources/FeatureCategoryDetailTests.swift
new file mode 100644
index 00000000..8a0522f1
--- /dev/null
+++ b/Projects/Feature/FeatureCategoryDetailTests/Sources/FeatureCategoryDetailTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureCategoryDetail
+
+final class FeatureCategoryDetailTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
index 0ab139b2..b6413230 100644
--- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
+++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingFeature.swift
@@ -12,15 +12,15 @@ import Domain
import CoreKit
import Util
-/// - 사용되는 API 목록
-/// 1. Profile 🎨
-/// 2. 포킷 생성 🖨️
@Reducer
public struct PokitCategorySettingFeature {
/// - Dependency
- @Dependency(\.dismiss) var dismiss
- @Dependency(\.pasteboard) var pasteboard
- @Dependency(\.categoryClient) var categoryClient
+ @Dependency(\.dismiss)
+ var dismiss
+ @Dependency(PasteboardClient.self)
+ var pasteboard
+ @Dependency(CategoryClient.self)
+ var categoryClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -84,20 +84,22 @@ public struct PokitCategorySettingFeature {
public enum View: BindableAction, Equatable {
case binding(BindingAction)
case dismiss
- case profileSettingButtonTapped
- case saveButtonTapped
- case onAppear
+ case 프로필_설정_버튼_눌렀을때
+ case 저장_버튼_눌렀을때
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
- case 카테고리_목록_조회_결과(BaseCategoryListInquiry)
- case 프로필_목록_조회_결과(images: [BaseCategoryImage])
+ case 카테고리_목록_조회_API_반영(BaseCategoryListInquiry)
+ case 프로필_목록_조회_API_반영(images: [BaseCategoryImage])
case 포킷_오류_핸들링(BaseError)
case 카테고리_인메모리_저장(BaseCategoryItem)
}
public enum AsyncAction: Equatable {
- case 프로필_목록_조회
+ case 카테고리_목록_조회_API
+ case 프로필_목록_조회_API
+ case 클립보드_감지
}
public enum ScopeAction: Equatable {
@@ -143,7 +145,6 @@ public struct PokitCategorySettingFeature {
public var body: some ReducerOf {
BindingReducer(action: \.view)
Reduce(self.core)
- ._printChanges()
}
}
//MARK: - FeatureAction Effect
@@ -157,12 +158,11 @@ private extension PokitCategorySettingFeature {
case .dismiss:
return .run { _ in await dismiss() }
- case .profileSettingButtonTapped:
- /// [Profile 🎨]1. 프로필 목록 조회 API 호출
+ case .프로필_설정_버튼_눌렀을때:
state.isProfileSheetPresented.toggle()
return .none
- case .saveButtonTapped:
+ case .저장_버튼_눌렀을때:
return .run { [domain = state.domain,
type = state.type] send in
switch type {
@@ -183,12 +183,14 @@ private extension PokitCategorySettingFeature {
)
await send(.inner(.카테고리_인메모리_저장(responseToCategoryDomain)))
await send(.delegate(.settingSuccess))
+
case .수정:
guard let categoryId = domain.categoryId else { return }
guard let image = domain.categoryImage else { return }
let request = CategoryEditRequest(categoryName: domain.categoryName, categoryImageId: image.id)
let _ = try await categoryClient.카테고리_수정(categoryId, request)
await send(.delegate(.settingSuccess))
+
case .공유추가:
guard let categoryId = domain.categoryId else { return }
guard let image = domain.categoryImage else { return }
@@ -202,31 +204,24 @@ private extension PokitCategorySettingFeature {
await send(.delegate(.settingSuccess))
}
} catch: { error, send in
- guard let errorResponse = error as? ErrorResponse else {
- return
- }
+ guard let errorResponse = error as? ErrorResponse else { return }
await send(.inner(.포킷_오류_핸들링(BaseError(response: errorResponse))))
}
- case .onAppear:
- return .run { send in
- let pageRequest = BasePageableRequest(page: 0, size: 100, sort: ["desc"])
- let response = try await categoryClient.카테고리_목록_조회(pageRequest, true).toDomain()
- await send(.inner(.카테고리_목록_조회_결과(response)))
- await send(.async(.프로필_목록_조회))
-
- for await _ in self.pasteboard.changes() {
- let url = try await pasteboard.probableWebURL()
- await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
- }
- }
+ case .뷰가_나타났을때:
+ /// 단순 조회API들의 나열이라 merge사용
+ return .merge(
+ .send(.async(.카테고리_목록_조회_API)),
+ .send(.async(.프로필_목록_조회_API)),
+ .send(.async(.클립보드_감지))
+ )
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case let .프로필_목록_조회_결과(images):
+ case let .프로필_목록_조회_API_반영(images):
state.domain.imageList = images
guard let _ = state.selectedProfile else {
@@ -234,9 +229,11 @@ private extension PokitCategorySettingFeature {
return .none
}
return .none
- case let .카테고리_목록_조회_결과(response):
+
+ case let .카테고리_목록_조회_API_반영(response):
state.domain.categoryListInQuiry = response
return .none
+
case let .포킷_오류_핸들링(baseError):
state.pokitNameTextInpuState = .error(message: baseError.message)
return .none
@@ -250,11 +247,26 @@ private extension PokitCategorySettingFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .프로필_목록_조회:
+ case .카테고리_목록_조회_API:
return .run { send in
- let a = try await categoryClient.카테고리_프로필_목록_조회()
- let b = a.map { $0.toDomain() }
- await send(.inner(.프로필_목록_조회_결과(images: b)))
+ let pageRequest = BasePageableRequest(page: 0, size: 100, sort: ["desc"])
+ let response = try await categoryClient.카테고리_목록_조회(pageRequest, true).toDomain()
+ await send(.inner(.카테고리_목록_조회_API_반영(response)))
+ }
+
+ case .프로필_목록_조회_API:
+ return .run { send in
+ let response = try await categoryClient.카테고리_프로필_목록_조회()
+ let images = response.map { $0.toDomain() }
+ await send(.inner(.프로필_목록_조회_API_반영(images: images)))
+ }
+
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteboard.changes() {
+ let url = try await pasteboard.probableWebURL()
+ await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
+ }
}
}
}
@@ -262,7 +274,7 @@ private extension PokitCategorySettingFeature {
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
switch action {
- case .profile(.imageSelected(let imageInfo)):
+ case .profile(.이미지_선택했을때(let imageInfo)):
state.isProfileSheetPresented = false
state.selectedProfile = imageInfo
return .none
diff --git a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
index 4e92795b..e0244982 100644
--- a/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
+++ b/Projects/Feature/FeatureCategorySetting/Sources/PokitCategorySettingView.swift
@@ -45,7 +45,7 @@ public extension PokitCategorySettingView {
delegateSend: { store.send(.scope(.profile($0))) }
)
}
- .task { await send(.onAppear).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
@@ -101,7 +101,7 @@ private extension PokitCategorySettingView {
.foregroundStyle(
.pokit(.icon(.inverseWh))
)
- Button(action: { send(.profileSettingButtonTapped) }) {
+ Button(action: { send(.프로필_설정_버튼_눌렀을때) }) {
Image(.icon(.edit))
.resizable()
.frame(width: 18, height: 18)
@@ -166,7 +166,7 @@ private extension PokitCategorySettingView {
state: !store.categoryName.isEmpty && store.selectedProfile != nil
? .filled(.primary)
: .disable,
- action: { send(.saveButtonTapped) }
+ action: { send(.저장_버튼_눌렀을때) }
)
}
/// 내포킷 Item
diff --git a/Projects/Feature/FeatureCategorySetting/Sources/Sheet/ProfileBottomSheet.swift b/Projects/Feature/FeatureCategorySetting/Sources/Sheet/ProfileBottomSheet.swift
index c1d8dc26..6807985f 100644
--- a/Projects/Feature/FeatureCategorySetting/Sources/Sheet/ProfileBottomSheet.swift
+++ b/Projects/Feature/FeatureCategorySetting/Sources/Sheet/ProfileBottomSheet.swift
@@ -45,7 +45,7 @@ public extension ProfileBottomSheet {
transaction: .init(animation: .pokitDissolve)
) { phase in
if let image = phase.image {
- Button(action: { delegateSend?(.imageSelected(item)) }) {
+ Button(action: { delegateSend?(.이미지_선택했을때(item)) }) {
image
.resizable()
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
@@ -91,7 +91,7 @@ public extension ProfileBottomSheet {
//MARK: - Delegate
public extension ProfileBottomSheet {
enum Delegate: Equatable {
- case imageSelected(BaseCategoryImage)
+ case 이미지_선택했을때(BaseCategoryImage)
}
}
//MARK: - Preview
diff --git a/Projects/Feature/FeatureCategorySettingTests/Resources/info.plist b/Projects/Feature/FeatureCategorySettingTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureCategorySettingTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureCategorySettingTests/Sources/FeatureCategorySettingTests.swift b/Projects/Feature/FeatureCategorySettingTests/Sources/FeatureCategorySettingTests.swift
new file mode 100644
index 00000000..b4eb7253
--- /dev/null
+++ b/Projects/Feature/FeatureCategorySettingTests/Sources/FeatureCategorySettingTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureCategorySetting
+
+final class FeatureCategorySettingTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
index 31d42f88..9f97170e 100644
--- a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
+++ b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingFeature.swift
@@ -7,6 +7,7 @@
import Foundation
import ComposableArchitecture
+import FeatureContentCard
import Domain
import CoreKit
import Util
@@ -16,30 +17,21 @@ public struct CategorySharingFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
/// - State
@ObservableState
public struct State: Equatable {
fileprivate var domain: CategorySharing
- var category: CategorySharing.Category {
- get { domain.sharedCategory.category }
- }
- var contents: IdentifiedArrayOf? {
- var identifiedArray = IdentifiedArrayOf()
- domain.sharedCategory.contentList.data.forEach { content in
- identifiedArray.append(content)
- }
- return identifiedArray
- }
- var hasNext: Bool {
- get { domain.sharedCategory.contentList.hasNext }
- }
+ var category: CategorySharing.Category { domain.sharedCategory.category }
+ var contents: IdentifiedArrayOf = []
+ var hasNext: Bool { domain.sharedCategory.contentList.hasNext }
var error: BaseError?
var isErrorSheetPresented: Bool = false
+ var isLoading: Bool = true
public init(sharedCategory: CategorySharing.SharedCategory) {
domain = .init(
@@ -60,30 +52,32 @@ public struct CategorySharingFeature {
case async(AsyncAction)
case scope(ScopeAction)
case delegate(DelegateAction)
+ case contents(IdentifiedActionOf)
@CasePathable
public enum View: Equatable, BindableAction {
- case 저장버튼_클릭
- case 컨텐츠_아이템_클릭(CategorySharing.Content)
- case 뒤로가기버튼_클릭
- case 경고_확인버튼_클릭
-
- case 다음페이지_로딩_onAppear
-
case binding(BindingAction)
+ case dismiss
+
+ case 저장_버튼_눌렀을때
+ case 경고_확인버튼_눌렀을때
+ case 페이지_로딩중일때
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
- case 공유받은_카테고리_갱신(CategorySharing.SharedCategory)
+ case 공유받은_카테고리_API_반영(CategorySharing.SharedCategory)
case 경고_닫음
case 경고_띄움(BaseError)
}
public enum AsyncAction: Equatable {
- case 공유받은_카테고리_조회
+ case 공유받은_카테고리_조회_API
}
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction {
+ case contents(IdentifiedActionOf)
+ }
public enum DelegateAction: Equatable {
case 컨텐츠_아이템_클릭(categoryId: Int, content: CategorySharing.Content)
@@ -116,6 +110,9 @@ public struct CategorySharingFeature {
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
+
+ case .contents(let contentsAction):
+ return .send(.scope(.contents(contentsAction)))
}
}
@@ -123,6 +120,9 @@ public struct CategorySharingFeature {
public var body: some ReducerOf {
BindingReducer(action: \.view)
Reduce(self.core)
+ .forEach(\.contents, action: \.contents) {
+ ContentCardFeature()
+ }
}
}
//MARK: - FeatureAction Effect
@@ -130,32 +130,67 @@ private extension CategorySharingFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .저장버튼_클릭:
+ case .binding:
+ return .none
+
+ case .dismiss:
+ return .run { _ in await dismiss() }
+
+ case .저장_버튼_눌렀을때:
let sharedCategory = state.domain.sharedCategory.category
return .send(.delegate(.공유받은_카테고리_추가(sharedCategory: sharedCategory)))
- case let .컨텐츠_아이템_클릭(content):
- return .send(.delegate(.컨텐츠_아이템_클릭(categoryId: state.category.categoryId , content: content)))
- case .뒤로가기버튼_클릭:
- return .run { _ in await dismiss() }
- case .경고_확인버튼_클릭:
+
+ case .경고_확인버튼_눌렀을때:
return .none
- case .binding:
+
+ case .페이지_로딩중일때:
+ return .send(.async(.공유받은_카테고리_조회_API))
+ case .뷰가_나타났을때:
+ state.domain.sharedCategory.contentList.data.forEach { content in
+ state.contents.append(.init(content: .init(
+ id: content.id,
+ categoryName: content.categoryName,
+ categoryId: state.category.categoryId,
+ title: content.title,
+ thumbNail: content.thumbNail,
+ data: content.data,
+ domain: content.domain,
+ createdAt: content.createdAt,
+ isRead: content.isRead
+ )))
+ }
+ state.isLoading = false
return .none
- case .다음페이지_로딩_onAppear:
- return .send(.async(.공유받은_카테고리_조회))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case let .공유받은_카테고리_갱신(sharedCategory):
+ case let .공유받은_카테고리_API_반영(sharedCategory):
state.domain.sharedCategory = sharedCategory
+
+ sharedCategory.contentList.data.forEach { content in
+ state.contents.append(.init(content: .init(
+ id: content.id,
+ categoryName: content.categoryName,
+ categoryId: state.category.categoryId,
+ title: content.title,
+ thumbNail: content.thumbNail,
+ data: content.data,
+ domain: content.domain,
+ createdAt: content.createdAt,
+ isRead: content.isRead
+ )))
+ }
+ state.isLoading = false
return .none
+
case let .경고_띄움(baseError):
state.error = baseError
state.isErrorSheetPresented = true
return .none
+
case .경고_닫음:
state.isErrorSheetPresented = false
state.error = nil
@@ -166,28 +201,38 @@ private extension CategorySharingFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .공유받은_카테고리_조회:
+ case .공유받은_카테고리_조회_API:
state.domain.pageable.page += 1
return .run { [
categoryId = state.domain.sharedCategory.category.categoryId,
pageable = state.domain.pageable
] send in
+ let request = BasePageableRequest(page: pageable.page, size: pageable.size, sort: pageable.sort)
let sharedCategory = try await categoryClient.공유받은_카테고리_조회(
"\(categoryId)",
- BasePageableRequest(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- )
+ request
).toDomain()
- await send(.inner(.공유받은_카테고리_갱신(sharedCategory)), animation: .pokitDissolve)
+ await send(.inner(.공유받은_카테고리_API_반영(sharedCategory)), animation: .pokitDissolve)
}
}
}
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
- return .none
+ 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
+ }
}
/// - Delegate Effect
diff --git a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
index b00047d8..53e2844f 100644
--- a/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
+++ b/Projects/Feature/FeatureCategorySharing/Sources/CategorySharing/CategorySharingView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureContentCard
import Domain
import DSKit
@@ -34,12 +35,13 @@ public extension CategorySharingView {
.padding(.top, 12)
.pokitNavigationBar { navigationBar }
.ignoresSafeArea(edges: .bottom)
+ .onAppear { send(.뷰가_나타났을때, animation: .pokitDissolve) }
.sheet(isPresented: $store.isErrorSheetPresented) {
PokitAlert(
store.error?.title ?? "에러",
message: store.error?.message,
confirmText: "확인",
- action: { send(.경고_확인버튼_클릭) }
+ action: { send(.경고_확인버튼_눌렀을때) }
)
}
}
@@ -52,7 +54,7 @@ private extension CategorySharingView {
PokitHeaderItems(placement: .leading) {
PokitToolbarButton(
.icon(.arrowLeft),
- action: { send(.뒤로가기버튼_클릭) }
+ action: { send(.dismiss) }
)
}
}
@@ -79,15 +81,15 @@ private extension CategorySharingView {
state: .filled(.primary),
size: .medium,
shape: .rectangle,
- action: { send(.저장버튼_클릭) }
+ action: { send(.저장_버튼_눌렀을때) }
)
}
}
var contentScrollView: some View {
Group {
- if let contents = store.contents {
- if contents.isEmpty {
+ if !store.isLoading {
+ if store.contents.isEmpty {
VStack {
PokitCaution(
image: .empty,
@@ -101,22 +103,23 @@ private extension CategorySharingView {
} else {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 0) {
- ForEach(contents) { content in
- let isFirst = content == contents.first
- let isLast = content == contents.last
+ ForEach(
+ 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
- PokitLinkCard(
- link: content,
- action: { send(.컨텐츠_아이템_클릭(content)) }
+ ContentCardView(
+ store: store,
+ isFirst: isFirst,
+ isLast: isLast
)
- .divider(isFirst: isFirst, isLast: isLast)
- .pokitScrollTransition(.opacity)
}
if store.hasNext {
PokitLoading()
.padding(.top, 12)
- .onAppear { send(.다음페이지_로딩_onAppear) }
+ .onAppear { send(.페이지_로딩중일때) }
}
}
}
diff --git a/Projects/Feature/FeatureCategorySharingTests/Resources/info.plist b/Projects/Feature/FeatureCategorySharingTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureCategorySharingTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureCategorySharingTests/Sources/FeatureCategorySharingTests.swift b/Projects/Feature/FeatureCategorySharingTests/Sources/FeatureCategorySharingTests.swift
new file mode 100644
index 00000000..e9e2b090
--- /dev/null
+++ b/Projects/Feature/FeatureCategorySharingTests/Sources/FeatureCategorySharingTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureCategorySharing
+
+final class FeatureCategorySharingTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureContentCard/Resources/Resource.swift b/Projects/Feature/FeatureContentCard/Resources/Resource.swift
new file mode 100644
index 00000000..43790c92
--- /dev/null
+++ b/Projects/Feature/FeatureContentCard/Resources/Resource.swift
@@ -0,0 +1,8 @@
+//
+// Dummy.stencil.swift
+// ProjectDescriptionHelpers
+//
+// Created by 김도형 on 6/16/24.
+//
+
+import Foundation
diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift
new file mode 100644
index 00000000..e2480df9
--- /dev/null
+++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardFeature.swift
@@ -0,0 +1,141 @@
+//
+// LinkCardFeature.swift
+// Feature
+//
+// Created by 김도형 on 11/17/24.
+
+import Foundation
+
+import ComposableArchitecture
+import Domain
+import CoreKit
+import Util
+
+@Reducer
+public struct ContentCardFeature {
+ /// - Dependency
+ @Dependency(SwiftSoupClient.self)
+ private var swiftSoupClient
+ /// - State
+ @ObservableState
+ public struct State: Equatable, Identifiable {
+ public let id = UUID()
+ public var content: BaseContentItem
+
+ public init(content: BaseContentItem) {
+ self.content = content
+ }
+ }
+
+ /// - Action
+ public enum Action: FeatureAction, ViewAction {
+ case view(View)
+ case inner(InnerAction)
+ case async(AsyncAction)
+ case scope(ScopeAction)
+ case delegate(DelegateAction)
+
+ @CasePathable
+ public enum View: Equatable {
+ case 컨텐츠_항목_눌렀을때
+ case 컨텐츠_항목_케밥_버튼_눌렀을때
+ case 메타데이터_조회
+ }
+
+ public enum InnerAction: Equatable {
+ case 메타데이터_조회_수행_반영(String)
+ }
+
+ public enum AsyncAction: Equatable {
+ case 메타데이터_조회_수행
+ }
+
+ public enum ScopeAction: Equatable { case doNothing }
+
+ public enum DelegateAction: Equatable {
+ case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
+ case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
+ }
+ }
+
+ /// - Initiallizer
+ public init() {}
+
+ /// - Reducer Core
+ private func core(into state: inout State, action: Action) -> Effect {
+ switch action {
+ /// - View
+ case .view(let viewAction):
+ return handleViewAction(viewAction, state: &state)
+
+ /// - Inner
+ case .inner(let innerAction):
+ return handleInnerAction(innerAction, state: &state)
+
+ /// - Async
+ case .async(let asyncAction):
+ return handleAsyncAction(asyncAction, state: &state)
+
+ /// - Scope
+ case .scope(let scopeAction):
+ return handleScopeAction(scopeAction, state: &state)
+
+ /// - Delegate
+ case .delegate(let delegateAction):
+ return handleDelegateAction(delegateAction, state: &state)
+ }
+ }
+
+ /// - Reducer body
+ public var body: some ReducerOf {
+ Reduce(self.core)
+ }
+}
+//MARK: - FeatureAction Effect
+private extension ContentCardFeature {
+ /// - View Effect
+ func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
+ switch action {
+ case .컨텐츠_항목_눌렀을때:
+ return .send(.delegate(.컨텐츠_항목_눌렀을때(content: state.content)))
+ case .컨텐츠_항목_케밥_버튼_눌렀을때:
+ return .send(.delegate(.컨텐츠_항목_케밥_버튼_눌렀을때(content: state.content)))
+ case .메타데이터_조회:
+ return .send(.async(.메타데이터_조회_수행))
+ }
+ }
+
+ /// - Inner Effect
+ func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
+ switch action {
+ case let .메타데이터_조회_수행_반영(imageURL):
+ state.content.thumbNail = imageURL
+ return .none
+ }
+ }
+
+ /// - Async Effect
+ func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
+ switch action {
+ case .메타데이터_조회_수행:
+ guard let url = URL(string: state.content.data) else {
+ return .none
+ }
+ return .run { send in
+ let imageURL = try await swiftSoupClient.parseOGImageURL(url)
+ guard let imageURL else { return }
+ await send(.inner(.메타데이터_조회_수행_반영(imageURL)))
+ }
+ }
+ }
+
+ /// - Scope Effect
+ func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
+ return .none
+ }
+
+ /// - Delegate Effect
+ func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
+ return .none
+ }
+}
diff --git a/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift
new file mode 100644
index 00000000..086a4be0
--- /dev/null
+++ b/Projects/Feature/FeatureContentCard/Sources/ContentCard/ContentCardView.swift
@@ -0,0 +1,69 @@
+//
+// LinkCardView.swift
+// Feature
+//
+// Created by 김도형 on 11/17/24.
+
+import SwiftUI
+
+import ComposableArchitecture
+import Domain
+import DSKit
+
+@ViewAction(for: ContentCardFeature.self)
+public struct ContentCardView: View {
+ /// - Properties
+ public var store: StoreOf
+ private let isFirst: Bool
+ private let isLast: Bool
+
+ /// - Initializer
+ public init(
+ store: StoreOf,
+ isFirst: Bool = false,
+ isLast: Bool = false
+ ) {
+ self.store = store
+ self.isFirst = isFirst
+ self.isLast = isLast
+ }
+}
+//MARK: - View
+public extension ContentCardView {
+ var body: some View {
+ WithPerceptionTracking {
+ PokitLinkCard(
+ link: store.content,
+ action: { send(.컨텐츠_항목_눌렀을때) },
+ kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때) },
+ fetchMetaData: { send(.메타데이터_조회) }
+ )
+ .divider(isFirst: isFirst, isLast: isLast)
+ }
+ }
+}
+//MARK: - Configure View
+private extension ContentCardView {
+
+}
+//MARK: - Preview
+#Preview {
+ ContentCardView(
+ store: Store(
+ initialState: .init(content: .init(
+ id: 1,
+ categoryName: "미분류",
+ categoryId: 992 ,
+ title: "youtube",
+ 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
+ )),
+ reducer: { ContentCardFeature() }
+ )
+ )
+}
+
+
diff --git a/Projects/Feature/FeatureContentCardDemo/Resources/LaunchScreen.storyboard b/Projects/Feature/FeatureContentCardDemo/Resources/LaunchScreen.storyboard
new file mode 100644
index 00000000..f1721f80
--- /dev/null
+++ b/Projects/Feature/FeatureContentCardDemo/Resources/LaunchScreen.storyboard
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Projects/Feature/FeatureContentCardDemo/Sources/FeatureContentCardDemoApp.swift b/Projects/Feature/FeatureContentCardDemo/Sources/FeatureContentCardDemoApp.swift
new file mode 100644
index 00000000..17fadce2
--- /dev/null
+++ b/Projects/Feature/FeatureContentCardDemo/Sources/FeatureContentCardDemoApp.swift
@@ -0,0 +1,17 @@
+//
+// App.stencil.swift
+// ProjectDescriptionHelpers
+//
+// Created by 김도형 on 6/16/24.
+//
+
+import SwiftUI
+
+@main
+struct FeatureContentCardDemoApp: App {
+ var body: some Scene {
+ WindowGroup {
+ // TODO: 루트 뷰 추가
+ }
+ }
+}
diff --git a/Projects/Feature/FeatureContentCardTests/Resources/info.plist b/Projects/Feature/FeatureContentCardTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureContentCardTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureContentCardTests/Sources/FeatureContentCardTests.swift b/Projects/Feature/FeatureContentCardTests/Sources/FeatureContentCardTests.swift
new file mode 100644
index 00000000..6e30407e
--- /dev/null
+++ b/Projects/Feature/FeatureContentCardTests/Sources/FeatureContentCardTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureContentCard
+
+final class FeatureContentCardTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
index 26763554..e6f39e3e 100644
--- a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
+++ b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailFeature.swift
@@ -15,11 +15,11 @@ import DSKit
@Reducer
public struct ContentDetailFeature {
/// - Dependency
- @Dependency(\.swiftSoup)
- private var swiftSoup
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.contentClient)
+ @Dependency(SwiftSoupClient.self)
+ private var swiftSoup
+ @Dependency(ContentClient.self)
private var contentClient
/// - State
@ObservableState
@@ -61,36 +61,35 @@ public struct ContentDetailFeature {
/// - Binding
case binding(BindingAction)
/// - View OnAppeared
- case contentDetailViewOnAppeared
+ case 뷰가_나타났을때
/// - Button Tapped
- case sharedButtonTapped
- case editButtonTapped
- case deleteButtonTapped
- case deleteAlertConfirmTapped
- case favoriteButtonTapped
- case alertCancelButtonTapped
+ case 공유_버튼_눌렀을때
+ case 수정_버튼_눌렀을때
+ case 삭제_버튼_눌렀을때
+ case 삭제확인_버튼_눌렀을때
+ case 즐겨찾기_버튼_눌렀을때
+ case 경고시트_해제
- case 링크_공유_완료
+ case 링크_공유_완료되었을때
}
public enum InnerAction: Equatable {
- case fetchMetadata(url: URL)
- case parsingInfo(title: String?, imageURL: String?)
- case parsingURL
- case dismissAlert
- case 컨텐츠_상세_조회(content: BaseContentDetail)
- case 즐겨찾기_갱신(Bool)
- case 링크미리보기_presented
+ case linkPreview
+ case 메타데이터_조회_수행(url: URL)
+ case 메타데이터_조회_반영(title: String?, imageURL: String?)
+ case URL_유효성_확인
+ case 컨텐츠_상세_조회_API_반영(content: BaseContentDetail)
+ case 즐겨찾기_API_반영(Bool)
}
public enum AsyncAction: Equatable {
- case 컨텐츠_상세_조회(id: Int)
- case 즐겨찾기(id: Int)
- case 즐겨찾기_취소(id: Int)
- case 컨텐츠_삭제(id: Int)
+ case 컨텐츠_상세_조회_API(id: Int)
+ case 즐겨찾기_API(id: Int)
+ case 즐겨찾기_취소_API(id: Int)
+ case 컨텐츠_삭제_API(id: Int)
}
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case editButtonTapped(contentId: Int)
@@ -138,49 +137,43 @@ private extension ContentDetailFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .contentDetailViewOnAppeared:
- guard let id = state.domain.contentId else {
+ case .뷰가_나타났을때:
+ if let content = state.content {
+ state.domain.content = content
+ return .send(.inner(.URL_유효성_확인))
+ } else if let id = state.domain.contentId {
+ return .send(.async(.컨텐츠_상세_조회_API(id: id)))
+ } else {
return .none
}
- return .run { send in
- await send(.async(.컨텐츠_상세_조회(id: id)))
- }
- case .sharedButtonTapped:
+ case .공유_버튼_눌렀을때:
state.showShareSheet = true
return .none
- case .editButtonTapped:
+ case .수정_버튼_눌렀을때:
guard let content = state.domain.content else { return .none }
- return .run { [content] send in
- await send(.delegate(.editButtonTapped(contentId: content.id)))
- }
- case .deleteButtonTapped:
+ return .send(.delegate(.editButtonTapped(contentId: content.id)))
+ case .삭제_버튼_눌렀을때:
state.showAlert = true
return .none
- case .deleteAlertConfirmTapped:
+ case .삭제확인_버튼_눌렀을때:
guard let id = state.domain.contentId else {
return .none
}
- return .run { send in
- await send(.async(.컨텐츠_삭제(id: id)))
- }
+ return .send(.async(.컨텐츠_삭제_API(id: id)))
case .binding:
return .none
- case .favoriteButtonTapped:
+ case .즐겨찾기_버튼_눌렀을때:
guard let content = state.domain.content,
let favorites = state.domain.content?.favorites else {
return .none
}
- return .run { send in
- if favorites {
- await send(.async(.즐겨찾기_취소(id: content.id)))
- } else {
- await send(.async(.즐겨찾기(id: content.id)))
- }
- }
- case .링크_공유_완료:
+ return favorites
+ ? .send(.async(.즐겨찾기_취소_API(id: content.id)))
+ : .send(.async(.즐겨찾기_API(id: content.id)))
+ case .링크_공유_완료되었을때:
state.showShareSheet = false
return .none
- case .alertCancelButtonTapped:
+ case .경고시트_해제:
state.showAlert = false
return .none
}
@@ -189,22 +182,21 @@ private extension ContentDetailFeature {
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .fetchMetadata(url: let url):
+ case .메타데이터_조회_수행(url: let url):
return .run { send in
/// - 링크에 대한 메타데이터의 제목 및 썸네일 항목 파싱
- let (title, imageURL) = await swiftSoup.parseOGTitleAndImage(url) {
- await send(.inner(.링크미리보기_presented), animation: .pokitDissolve)
- }
- await send(
- .inner(.parsingInfo(title: title, imageURL: imageURL)),
+ async let title = swiftSoup.parseOGTitle(url)
+ async let imageURL = swiftSoup.parseOGImageURL(url)
+ try await send(
+ .inner(.메타데이터_조회_반영(title: title, imageURL: imageURL)),
animation: .pokitDissolve
)
}
- case let .parsingInfo(title: title, imageURL: imageURL):
+ case let .메타데이터_조회_반영(title: title, imageURL: imageURL):
state.linkTitle = title
state.linkImageURL = imageURL
- return .none
- case .parsingURL:
+ return .send(.inner(.linkPreview), animation: .pokitDissolve)
+ case .URL_유효성_확인:
guard let urlString = state.domain.content?.data,
let url = URL(string: urlString) else {
/// 🚨 Error Case [1]: 올바른 링크가 아닐 때
@@ -213,20 +205,17 @@ private extension ContentDetailFeature {
state.linkImageURL = nil
return .none
}
- return .send(.inner(.fetchMetadata(url: url)), animation: .pokitDissolve)
- case .dismissAlert:
- state.showAlert = false
- return .none
- case .컨텐츠_상세_조회(content: let content):
+ return .send(.inner(.메타데이터_조회_수행(url: url)), animation: .pokitDissolve)
+ case .컨텐츠_상세_조회_API_반영(content: let content):
state.domain.content = content
- return .run { send in
- await send(.delegate(.컨텐츠_조회_완료))
- await send(.inner(.parsingURL))
- }
- case .즐겨찾기_갱신(let favorite):
+ return .merge(
+ .send(.delegate(.컨텐츠_조회_완료)),
+ .send(.inner(.URL_유효성_확인))
+ )
+ case .즐겨찾기_API_반영(let favorite):
state.domain.content?.favorites = favorite
return .send(.delegate(.즐겨찾기_갱신_완료))
- case .링크미리보기_presented:
+ case .linkPreview:
state.showLinkPreview = true
return .none
}
@@ -235,22 +224,22 @@ private extension ContentDetailFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .컨텐츠_상세_조회(id: let id):
+ case .컨텐츠_상세_조회_API(id: let id):
return .run { send in
let contentResponse = try await contentClient.컨텐츠_상세_조회("\(id)").toDomain()
- await send(.inner(.컨텐츠_상세_조회(content: contentResponse)))
+ await send(.inner(.컨텐츠_상세_조회_API_반영(content: contentResponse)))
}
- case .즐겨찾기(id: let id):
+ case .즐겨찾기_API(id: let id):
return .run { send in
let _ = try await contentClient.즐겨찾기("\(id)")
- await send(.inner(.즐겨찾기_갱신(true)))
+ await send(.inner(.즐겨찾기_API_반영(true)))
}
- case .즐겨찾기_취소(id: let id):
+ case .즐겨찾기_취소_API(id: let id):
return .run { send in
try await contentClient.즐겨찾기_취소("\(id)")
- await send(.inner(.즐겨찾기_갱신(false)))
+ await send(.inner(.즐겨찾기_API_반영(false)))
}
- case .컨텐츠_삭제(id: let id):
+ case .컨텐츠_삭제_API(id: let id):
return .run { send in
try await contentClient.컨텐츠_삭제("\(id)")
await send(.delegate(.컨텐츠_삭제_완료))
diff --git a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
index dabd2a2e..c049946f 100644
--- a/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
+++ b/Projects/Feature/FeatureContentDetail/Sources/ContentDetail/ContentDetailView.swift
@@ -53,8 +53,8 @@ public extension ContentDetailView {
"링크를 정말 삭제하시겠습니까?",
message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
confirmText: "삭제",
- action: { send(.deleteAlertConfirmTapped) },
- cancelAction: { send(.alertCancelButtonTapped) }
+ action: { send(.삭제확인_버튼_눌렀을때) },
+ cancelAction: { send(.경고시트_해제) }
)
}
.sheet(isPresented: $store.showShareSheet) {
@@ -62,13 +62,13 @@ public extension ContentDetailView {
let shareURL = URL(string: content.data) {
PokitShareSheet(
items: [shareURL],
- completion: { send(.링크_공유_완료) }
+ completion: { send(.링크_공유_완료되었을때) }
)
.presentationDetents([.medium, .large])
}
}
.task {
- await send(.contentDetailViewOnAppeared, animation: .pokitDissolve).finish()
+ await send(.뷰가_나타났을때, animation: .pokitDissolve).finish()
}
}
}
@@ -174,7 +174,7 @@ private extension ContentDetailView {
@ViewBuilder
func favorite(favorites: Bool) -> some View {
- Button(action: { send(.favoriteButtonTapped, animation: .pokitDissolve) }) {
+ Button(action: { send(.즐겨찾기_버튼_눌렀을때, animation: .pokitDissolve) }) {
Image(favorites ? .icon(.starFill) : .icon(.starFill))
.resizable()
.scaledToFit()
@@ -195,17 +195,17 @@ private extension ContentDetailView {
Group {
toolbarButton(
.icon(.share),
- action: { send(.sharedButtonTapped) }
+ action: { send(.공유_버튼_눌렀을때) }
)
toolbarButton(
.icon(.edit),
- action: { send(.editButtonTapped) }
+ action: { send(.수정_버튼_눌렀을때) }
)
toolbarButton(
.icon(.trash),
- action: { send(.deleteButtonTapped) }
+ action: { send(.삭제_버튼_눌렀을때) }
)
}
.disabled(store.contentId == nil)
diff --git a/Projects/Feature/FeatureContentDetailTests/Resources/info.plist b/Projects/Feature/FeatureContentDetailTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureContentDetailTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureContentDetailTests/Sources/FeatureContentDetailTests.swift b/Projects/Feature/FeatureContentDetailTests/Sources/FeatureContentDetailTests.swift
new file mode 100644
index 00000000..96dfa6dd
--- /dev/null
+++ b/Projects/Feature/FeatureContentDetailTests/Sources/FeatureContentDetailTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureContentDetail
+
+final class FeatureContentDetailTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
index 4d73e346..dfbf0c2d 100644
--- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
+++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListFeature.swift
@@ -7,6 +7,7 @@
import Foundation
import ComposableArchitecture
+import FeatureContentCard
import Domain
import CoreKit
import DSKit
@@ -17,11 +18,11 @@ public struct ContentListFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.pasteboard)
+ @Dependency(PasteboardClient.self)
private var pasteBoard
- @Dependency(\.remindClient)
+ @Dependency(RemindClient.self)
private var remindClient
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
/// - State
@ObservableState
@@ -32,14 +33,7 @@ public struct ContentListFeature {
let contentType: ContentType
fileprivate var domain = ContentList()
- var contents: IdentifiedArrayOf? {
- guard let contentList = domain.contentList.data else {
- return nil
- }
- var identifiedArray = IdentifiedArrayOf()
- contentList.forEach { identifiedArray.append($0) }
- return identifiedArray
- }
+ var contents: IdentifiedArrayOf = []
var contentCount: Int {
get { domain.contentCount }
}
@@ -52,6 +46,7 @@ public struct ContentListFeature {
var hasNext: Bool {
domain.contentList.hasNext
}
+ var isLoading: Bool = true
}
/// - Action
@@ -61,52 +56,54 @@ public struct ContentListFeature {
case async(AsyncAction)
case scope(ScopeAction)
case delegate(DelegateAction)
+ case contents(IdentifiedActionOf)
@CasePathable
public enum View: Equatable, BindableAction {
/// - Binding
case binding(BindingAction)
+
+ case pagenation
/// - Button Tapped
- case linkCardTapped(content: BaseContentItem)
- case kebabButtonTapped(content: BaseContentItem)
- case bottomSheetButtonTapped(
+ case bottomSheet(
delegate: PokitBottomSheet.Delegate,
content: BaseContentItem
)
- case deleteAlertConfirmTapped(content: BaseContentItem)
- case sortTextLinkTapped
- case backButtonTapped
+ case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
+ case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
+ case 컨텐츠_삭제_눌렀을때(content: BaseContentItem)
+ case 정렬_버튼_눌렀을때
+ case dismiss
/// - On Appeared
- case contentListViewOnAppeared
- case pagenation
+ case 뷰가_나타났을때
- case 링크_공유_완료
+ case 링크_공유시트_해제
+ case 경고시트_해제
}
public enum InnerAction: Equatable {
- case dismissBottomSheet
- case 컨텐츠_목록_조회(BaseContentListInquiry)
- case 컨텐츠_삭제_반영(id: Int)
- case pagenation_네트워크_결과(BaseContentListInquiry)
- case pagenation_초기화
- case 컨텐츠_목록_갱신(BaseContentListInquiry)
- case 컨텐츠_개수_갱신(Int)
+ case 바텀시트_해제
+ case 컨텐츠_목록_조회_API_반영(BaseContentListInquiry)
+ case 컨텐츠_삭제_API_반영(id: Int)
+ case 컨텐츠_목록_조회_페이징_API_반영(BaseContentListInquiry)
+ case 페이징_초기화
+ case 컨텐츠_개수_업데이트(Int)
}
public enum AsyncAction: Equatable {
- case 읽지않음_컨텐츠_조회
- case 즐겨찾기_링크모음_조회
- case 컨텐츠_삭제(id: Int)
- case pagenation_네트워크
- case 페이징_재조회
- case 컨텐츠_개수_조회
+ case 컨텐츠_삭제_API(id: Int)
+ case 컨텐츠_목록_조회_페이징_API
+ case 컨텐츠_목록_조회_API
+ case 컨텐츠_개수_조회_API
+ case 클립보드_감지
}
- public enum ScopeAction: Equatable {
+ public enum ScopeAction {
case bottomSheet(
delegate: PokitBottomSheet.Delegate,
content: BaseContentItem
)
+ case contents(IdentifiedActionOf)
}
public enum DelegateAction: Equatable {
@@ -142,12 +139,18 @@ public struct ContentListFeature {
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
+
+ case let .contents(contentAction):
+ return .send(.scope(.contents(contentAction)))
}
}
/// - Reducer body
public var body: some ReducerOf {
Reduce(self.core)
+ .forEach(\.contents, action: \.contents) {
+ ContentCardFeature()
+ }
}
}
//MARK: - FeatureAction Effect
@@ -155,87 +158,82 @@ private extension ContentListFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .kebabButtonTapped(let content):
+ case .컨텐츠_항목_케밥_버튼_눌렀을때(let content):
state.bottomSheetItem = content
return .none
- case .linkCardTapped(let content):
+ case .컨텐츠_항목_눌렀을때(let content):
return .send(.delegate(.링크상세(content: content)))
- case .bottomSheetButtonTapped(let delegate, let content):
- return .run { send in
- await send(.inner(.dismissBottomSheet))
- await send(.scope(.bottomSheet(delegate: delegate, content: content)))
- }
- case .deleteAlertConfirmTapped:
+ 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 .run { [id] send in
- await send(.async(.컨텐츠_삭제(id: id)))
- }
+ return .send(.async(.컨텐츠_삭제_API(id: id)))
case .binding:
return .none
- case .sortTextLinkTapped:
+ case .정렬_버튼_눌렀을때:
state.isListDescending.toggle()
state.domain.pageable.sort = [
state.isListDescending ? "createdAt,desc" : "createdAt,asc"
]
- return .send(.inner(.pagenation_초기화), animation: .pokitDissolve)
- case .backButtonTapped:
+ return .send(.inner(.페이징_초기화), animation: .pokitDissolve)
+ case .dismiss:
return .run { _ in await dismiss() }
- case .contentListViewOnAppeared:
- return .run { [type = state.contentType] send in
- switch type {
- case .unread:
- await send(.async(.읽지않음_컨텐츠_조회), animation: .pokitDissolve)
- case .favorite:
- await send(.async(.즐겨찾기_링크모음_조회), animation: .pokitDissolve)
- }
-
- for await _ in self.pasteBoard.changes() {
- let url = try await pasteBoard.probableWebURL()
- await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
- }
- }
-
+ case .뷰가_나타났을때:
+ return .merge(
+ .send(.async(.컨텐츠_개수_조회_API)),
+ .send(.async(.컨텐츠_목록_조회_API)),
+ .send(.async(.클립보드_감지))
+ )
case .pagenation:
- return .send(.async(.pagenation_네트워크))
- case .링크_공유_완료:
+ 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 .dismissBottomSheet:
+ case .바텀시트_해제:
state.bottomSheetItem = nil
return .none
- case .컨텐츠_목록_조회(let contentList):
+ case .컨텐츠_목록_조회_페이징_API_반영(let contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
state.domain.contentList = contentList
state.domain.contentList.data = list + newList
+
+ newList.forEach { state.contents.append(.init(content: $0)) }
return .none
- case .컨텐츠_삭제_반영(id: let id):
+ 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 .pagenation_네트워크_결과(let contentList):
+ case .컨텐츠_목록_조회_API_반영(let contentList):
state.domain.contentList = contentList
+
+ var identifiedArray = IdentifiedArrayOf()
+ contentList.data?.forEach { identifiedArray.append(.init(content: $0)) }
+ state.contents = identifiedArray
+
+ state.isLoading = false
return .none
- case .pagenation_초기화:
+ case .페이징_초기화:
state.domain.pageable.page = 0
state.domain.contentList.data = nil
- switch state.contentType {
- case .unread:
- return .send(.async(.읽지않음_컨텐츠_조회), animation: .pokitDissolve)
- case .favorite:
- return .send(.async(.즐겨찾기_링크모음_조회), animation: .pokitDissolve)
- }
- case let .컨텐츠_목록_갱신(contentList):
- state.domain.contentList = contentList
- return .none
- case let .컨텐츠_개수_갱신(count):
+ state.isLoading = true
+ state.contents.removeAll()
+ return .send(.async(.컨텐츠_목록_조회_API), animation: .pokitDissolve)
+ case let .컨텐츠_개수_업데이트(count):
state.domain.contentCount = count
return .none
}
@@ -244,105 +242,58 @@ private extension ContentListFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .읽지않음_컨텐츠_조회:
- return .run { [pageable = state.domain.pageable] send in
- await send(.async(.컨텐츠_개수_조회))
- let contentList = try await remindClient.읽지않음_컨텐츠_조회(
- BasePageableRequest(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- )
- ).toDomain()
- await send(
- .inner(.컨텐츠_목록_조회(contentList)),
- animation: pageable.page == 0 ? .pokitDissolve : nil
- )
- }
- case .즐겨찾기_링크모음_조회:
- return .run { [pageable = state.domain.pageable] send in
- await send(.async(.컨텐츠_개수_조회))
- let contentList = try await remindClient.즐겨찾기_링크모음_조회(
- BasePageableRequest(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- )
- ).toDomain()
- await send(
- .inner(.컨텐츠_목록_조회(contentList)),
- animation: pageable.page == 0 ? .pokitDissolve : nil
- )
- }
- case .컨텐츠_삭제(id: let id):
- return .run { [count = state.domain.contentCount] send in
- let newCount = count - 1
- await send(.inner(.컨텐츠_개수_갱신(newCount)), animation: .pokitSpring)
- let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring)
- }
+ case .컨텐츠_삭제_API(id: let id):
+ let count = state.domain.contentCount
+ let newCount = count - 1
+
+ return .merge(
+ .send(.inner(.컨텐츠_개수_업데이트(newCount))),
+ contentDelete(contentId: id)
+ )
- case .pagenation_네트워크:
+ case .컨텐츠_목록_조회_페이징_API:
state.domain.pageable.page += 1
- return .run { [type = state.contentType] send in
+ return .run { [
+ type = state.contentType,
+ pageableRequest = BasePageableRequest(
+ page: state.domain.pageable.page,
+ size: state.domain.pageable.size,
+ sort: state.domain.pageable.sort
+ )
+ ] send in
+ let contentList: BaseContentListInquiry
switch type {
case .unread:
- await send(.async(.읽지않음_컨텐츠_조회))
- break
+ contentList = try await remindClient.읽지않음_컨텐츠_조회(
+ pageableRequest
+ ).toDomain()
case .favorite:
- await send(.async(.즐겨찾기_링크모음_조회))
- break
- }
- }
- case .페이징_재조회:
- return .run { [
- pageable = state.domain.pageable,
- contentType = state.contentType
- ] send in
- await send(.async(.컨텐츠_개수_조회))
- let stream = AsyncThrowingStream { continuation in
- Task {
- for page in 0...pageable.page {
- let paeagableRequest = BasePageableRequest(
- page: page,
- size: pageable.size,
- sort: pageable.sort
- )
- switch contentType {
- case .favorite:
- let contentList = try await remindClient.즐겨찾기_링크모음_조회(
- paeagableRequest
- ).toDomain()
- continuation.yield(contentList)
- case .unread:
- let contentList = try await remindClient.읽지않음_컨텐츠_조회(
- paeagableRequest
- ).toDomain()
- continuation.yield(contentList)
- }
- }
- continuation.finish()
- }
+ contentList = try await remindClient.즐겨찾기_링크모음_조회(
+ pageableRequest
+ ).toDomain()
}
- var contentItems: BaseContentListInquiry? = nil
- for try await contentList in stream {
- let items = contentItems?.data ?? []
- let newItems = contentList.data ?? []
- contentItems = contentList
- contentItems?.data = items + newItems
- }
- guard let contentItems else { return }
- await send(.inner(.컨텐츠_목록_갱신(contentItems)), animation: .pokitSpring)
+
+ await send(.inner(.컨텐츠_목록_조회_페이징_API_반영(contentList)))
}
- case .컨텐츠_개수_조회:
+ case .컨텐츠_목록_조회_API:
+ return contentListFetch(state: &state)
+ case .컨텐츠_개수_조회_API:
return .run { [ contentType = state.contentType ] send in
+ let count: Int
switch contentType {
case .favorite:
- let count = try await remindClient.즐겨찾기_컨텐츠_개수_조회().count
- await send(.inner(.컨텐츠_개수_갱신(count)), animation: .pokitSpring)
+ count = try await remindClient.즐겨찾기_컨텐츠_개수_조회().count
case .unread:
- let count = try await remindClient.읽지않음_컨텐츠_개수_조회().count
- await send(.inner(.컨텐츠_개수_갱신(count)), animation: .pokitSpring)
+ count = try await remindClient.읽지않음_컨텐츠_개수_조회().count
+ }
+
+ await send(.inner(.컨텐츠_개수_업데이트(count)), animation: .pokitSpring)
+ }
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteBoard.changes() {
+ let url = try await pasteBoard.probableWebURL()
+ await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
}
}
}
@@ -365,6 +316,14 @@ private extension ContentListFeature {
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
+ case .contents:
+ return .none
}
}
@@ -372,11 +331,59 @@ private extension ContentListFeature {
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
switch action {
case .컨텐츠_목록_조회:
- return .send(.async(.페이징_재조회))
+ return .send(.async(.컨텐츠_목록_조회_API))
default:
return .none
}
}
+
+ func contentListFetch(state: inout State) -> Effect {
+ return .run { [
+ pageable = state.domain.pageable,
+ contentType = state.contentType
+ ] send in
+ let stream = AsyncThrowingStream { continuation in
+ Task {
+ for page in 0...pageable.page {
+ let paeagableRequest = BasePageableRequest(
+ page: page,
+ size: pageable.size,
+ sort: pageable.sort
+ )
+ switch contentType {
+ case .favorite:
+ let contentList = try await remindClient.즐겨찾기_링크모음_조회(
+ paeagableRequest
+ ).toDomain()
+ continuation.yield(contentList)
+ case .unread:
+ let contentList = try await remindClient.읽지않음_컨텐츠_조회(
+ paeagableRequest
+ ).toDomain()
+ continuation.yield(contentList)
+ }
+ }
+ continuation.finish()
+ }
+ }
+ var contentItems: BaseContentListInquiry? = nil
+ for try await contentList in stream {
+ let items = contentItems?.data ?? []
+ let newItems = contentList.data ?? []
+ contentItems = contentList
+ contentItems?.data = items + newItems
+ }
+ guard let contentItems else { return }
+ await send(.inner(.컨텐츠_목록_조회_API_반영(contentItems)), animation: .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 1fb27e51..e38a4713 100644
--- a/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift
+++ b/Projects/Feature/FeatureContentList/Sources/ContentList/ContentListView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureContentCard
import DSKit
@ViewAction(for: ContentListFeature.self)
@@ -38,7 +39,7 @@ public extension ContentListView {
items: [.share, .edit, .delete],
height: 224,
delegateSend: {
- send(.bottomSheetButtonTapped(delegate: $0, content: content))
+ send(.bottomSheet(delegate: $0, content: content))
}
)
}
@@ -46,7 +47,7 @@ public extension ContentListView {
if let shareURL = URL(string: content.data) {
PokitShareSheet(
items: [shareURL],
- completion: { send(.링크_공유_완료) }
+ completion: { send(.링크_공유시트_해제) }
)
.presentationDetents([.medium, .large])
}
@@ -55,10 +56,12 @@ public extension ContentListView {
PokitAlert(
"링크를 정말 삭제하시겠습니까?",
message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
- confirmText: "삭제"
- ) { send(.deleteAlertConfirmTapped(content: content)) }
+ confirmText: "삭제",
+ action: { send(.컨텐츠_삭제_눌렀을때(content: content)) },
+ cancelAction: { send(.경고시트_해제) }
+ )
}
- .task { await send(.contentListViewOnAppeared, animation: .pokitDissolve).finish() }
+ .task { await send(.뷰가_나타났을때, animation: .pokitDissolve).finish() }
}
}
}
@@ -76,7 +79,7 @@ private extension ContentListView {
PokitIconLTextLink(
store.isListDescending ? "최신순" : "오래된순",
icon: .icon(.align),
- action: { send(.sortTextLinkTapped) }
+ action: { send(.정렬_버튼_눌렀을때) }
)
.contentTransition(.numericText())
}
@@ -84,8 +87,8 @@ private extension ContentListView {
var list: some View {
Group {
- if let contents = store.contents {
- if contents.isEmpty {
+ if !store.isLoading {
+ if store.contents.isEmpty {
PokitCaution(
image: .empty,
titleKey: "즐겨찾기 링크가 없어요!",
@@ -97,16 +100,17 @@ private extension ContentListView {
} else {
ScrollView {
LazyVStack(spacing: 0) {
- ForEach(contents) { content in
- let isFirst = content == contents.first
- let isLast = content == contents.last
+ ForEach(
+ 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
- PokitLinkCard(
- link: content,
- action: { send(.linkCardTapped(content: content)) },
- kebabAction: { send(.kebabButtonTapped(content: content)) }
+ ContentCardView(
+ store: store,
+ isFirst: isFirst,
+ isLast: isLast
)
- .divider(isFirst: isFirst, isLast: isLast)
}
if store.hasNext {
@@ -128,7 +132,7 @@ private extension ContentListView {
PokitHeader(title: store.contentType.title) {
PokitHeaderItems(placement: .leading) {
PokitToolbarButton(.icon(.arrowLeft)) {
- send(.backButtonTapped)
+ send(.dismiss)
}
}
}
diff --git a/Projects/Feature/FeatureContentListTests/Resources/info.plist b/Projects/Feature/FeatureContentListTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureContentListTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureContentListTests/Sources/FeatureContentListTests.swift b/Projects/Feature/FeatureContentListTests/Sources/FeatureContentListTests.swift
new file mode 100644
index 00000000..4051d641
--- /dev/null
+++ b/Projects/Feature/FeatureContentListTests/Sources/FeatureContentListTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureContentList
+
+final class FeatureContentListTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
index 4be31b47..089b9fa3 100644
--- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
+++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingFeature.swift
@@ -18,22 +18,24 @@ public struct ContentSettingFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.swiftSoup)
+ @Dependency(SwiftSoupClient.self)
private var swiftSoup
- @Dependency(\.pasteboard)
+ @Dependency(PasteboardClient.self)
private var pasteboard
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
/// - State
@ObservableState
public struct State: Equatable {
public init(
contentId: Int? = nil,
- urlText: String? = nil
+ urlText: String? = nil,
+ isShareExtension: Bool = false
) {
self.domain = .init(contentId: contentId, data: urlText)
+ self.isShareExtension = isShareExtension
}
fileprivate var domain: ContentSetting
var urlText: String {
@@ -72,6 +74,7 @@ public struct ContentSettingFeature {
var saveIsLoading: Bool = false
var link: String?
var showLinkPreview = false
+ var isShareExtension: Bool
}
/// - Action
@@ -87,43 +90,44 @@ public struct ContentSettingFeature {
/// - Binding
case binding(BindingAction)
/// - Button Tapped
- case pokitSelectButtonTapped
- case pokitSelectItemButtonTapped(pokit: BaseCategoryItem)
- case contentSettingViewOnAppeared
- case saveBottomButtonTapped
- case addPokitButtonTapped
- case linkCopyButtonTapped
+ case 포킷선택_버튼_눌렀을때
+ case 포킷선택_항목_눌렀을때(pokit: BaseCategoryItem)
+ case 뷰가_나타났을때
+ case 저장_버튼_눌렀을때
+ case 포킷추가_버튼_눌렀을때
+ case 링크복사_버튼_눌렀을때
- case dismiss
+ case 뒤로가기_버튼_눌렀을때
}
public enum InnerAction: Equatable {
- case fetchMetadata(url: URL)
- case parsingInfo(title: String?, imageURL: String?)
- case parsingURL
- case showPopup
- case showLinkPopup(URL?)
- case updateURLText(String?)
- case 컨텐츠_갱신(content: BaseContentDetail)
- case 카테고리_갱신(category: BaseCategory)
- case 카테고리_목록_갱신(categoryList: BaseCategoryListInquiry)
- case 링크미리보기_presented
+ case linkPopup(URL?)
+ case linkPreview
+ case 메타데이터_조회_수행(url: URL)
+ case 메타데이텨_조회_반영(title: String?, imageURL: String?)
+ case URL_유효성_확인
+ case 링크복사_반영(String?)
+ case 컨텐츠_상세_조회_API_반영(content: BaseContentDetail)
+ case 카테고리_상세_조회_API_반영(category: BaseCategory)
+ case 카테고리_목록_조회_API_반영(categoryList: BaseCategoryListInquiry)
case 선택한_포킷_인메모리_삭제
}
public enum AsyncAction: Equatable {
- case 컨텐츠_상세_조회(id: Int)
- case 카테고리_상세_조회(id: Int?, sharedId: Int?)
- case 카테고리_목록_조회
- case 컨텐츠_수정
- case 컨텐츠_추가
+ case 컨텐츠_상세_조회_API(id: Int)
+ case 카테고리_상세_조회_API(id: Int?, sharedId: Int?)
+ case 카테고리_목록_조회_API
+ case 컨텐츠_수정_API
+ case 컨텐츠_추가_API
+ case 클립보드_감지
}
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case 저장하기_완료
case 포킷추가하기
+ case dismiss
}
}
@@ -168,79 +172,81 @@ private extension ContentSettingFeature {
switch action {
case .binding(\.urlText):
enum CancelID { case urlTextChanged }
- return .run { send in
- await send(.inner(.parsingURL))
- }
- /// - 1초마다 `urlText`변화의 마지막을 감지하여 이벤트 방출
- .throttle(
+ return .send(.inner(.URL_유효성_확인)).debounce(
+ /// - 1초마다 `urlText`변화의 마지막을 감지하여 이벤트 방출
id: CancelID.urlTextChanged,
for: 1,
- scheduler: DispatchQueue.main,
- latest: true
+ scheduler: DispatchQueue.main
)
case .binding:
return .none
- case .pokitSelectButtonTapped:
- return .send(.async(.카테고리_목록_조회))
- case .pokitSelectItemButtonTapped(pokit: let pokit):
+ case .포킷선택_버튼_눌렀을때:
+ return .send(.async(.카테고리_목록_조회_API))
+ case .포킷선택_항목_눌렀을때(pokit: let pokit):
state.selectedPokit = pokit
return .none
- case .contentSettingViewOnAppeared:
- return .run { [id = state.domain.contentId] send in
- if let id {
- await send(.async(.컨텐츠_상세_조회(id: id)))
- }
- await send(.async(.카테고리_목록_조회))
- await send(.inner(.parsingURL))
- for await _ in self.pasteboard.changes() {
- let url = try await pasteboard.probableWebURL()
- await send(.inner(.showLinkPopup(url)), animation: .pokitSpring)
- }
+ case .뷰가_나타났을때:
+ var mergeEffect: [Effect] = [
+ .send(.async(.카테고리_목록_조회_API)),
+ .send(.inner(.URL_유효성_확인)),
+ .send(.async(.클립보드_감지))
+ ]
+ if let id = state.domain.contentId {
+ mergeEffect.append(.send(.async(.컨텐츠_상세_조회_API(id: id))))
}
- case .saveBottomButtonTapped:
- return .run { [isEdit = state.domain.categoryId != nil] send in
- if isEdit {
- await send(.async(.컨텐츠_수정))
- } else {
- await send(.async(.컨텐츠_추가))
- }
- }
- case .addPokitButtonTapped:
+ return .merge(mergeEffect)
+ case .저장_버튼_눌렀을때:
+ let isEdit = state.domain.categoryId != nil
+
+ return isEdit
+ ? .send(.async(.컨텐츠_수정_API))
+ : .send(.async(.컨텐츠_추가_API))
+ case .포킷추가_버튼_눌렀을때:
guard state.domain.categoryTotalCount < 30 else {
/// 🚨 Error Case [1]: 포킷 갯수가 30개 이상일 경우
- return .send(.inner(.showPopup), animation: .pokitSpring)
+ state.showMaxCategoryPopup = true
+ return .none
}
+
return .send(.delegate(.포킷추가하기))
-
- case .dismiss:
- return .run { _ in await dismiss() }
- case .linkCopyButtonTapped:
- return .send(.inner(.updateURLText(state.link)))
+ case .뒤로가기_버튼_눌렀을때:
+ return state.isShareExtension
+ ? .send(.delegate(.dismiss))
+ : .run { _ in await dismiss() }
+ case .링크복사_버튼_눌렀을때:
+ return .send(.inner(.링크복사_반영(state.link)))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .fetchMetadata(url: let url):
+ case let .linkPopup(url):
+ guard let url else { return .none }
+ state.link = url.absoluteString
+ state.showDetectedURLPopup = true
+ return .none
+ case .linkPreview:
+ state.showLinkPreview = true
+ return .none
+ case .메타데이터_조회_수행(url: let url):
return .run { send in
- let (title, imageURL) = await swiftSoup.parseOGTitleAndImage(url) {
- await send(.inner(.링크미리보기_presented), animation: .pokitDissolve)
- }
- await send(
- .inner(.parsingInfo(title: title, imageURL: imageURL)),
+ async let title = swiftSoup.parseOGTitle(url)
+ async let imageURL = swiftSoup.parseOGImageURL(url)
+ try await send(
+ .inner(.메타데이텨_조회_반영(title: title, imageURL: imageURL)),
animation: .pokitDissolve
)
}
- case let .parsingInfo(title: title, imageURL: imageURL):
+ case let .메타데이텨_조회_반영(title: title, imageURL: imageURL):
state.linkTitle = title
state.linkImageURL = imageURL
if let title, state.domain.title.isEmpty {
state.domain.title = title
}
state.domain.thumbNail = imageURL
- return .none
- case .parsingURL:
+ return .send(.inner(.linkPreview), animation: .pokitDissolve)
+ case .URL_유효성_확인:
guard let url = URL(string: state.domain.data),
!state.domain.data.isEmpty else {
/// 🚨 Error Case [1]: 올바른 링크가 아닐 때
@@ -251,17 +257,14 @@ private extension ContentSettingFeature {
state.domain.thumbNail = nil
return .none
}
- return .send(.inner(.fetchMetadata(url: url)), animation: .pokitDissolve)
- case .showPopup:
- state.showMaxCategoryPopup = true
- return .none
- case .updateURLText(let urlText):
+ return .send(.inner(.메타데이터_조회_수행(url: url)), animation: .pokitDissolve)
+ case .링크복사_반영(let urlText):
state.showDetectedURLPopup = false
state.link = nil
guard let urlText else { return .none }
state.domain.data = urlText
- return .send(.inner(.parsingURL))
- case .컨텐츠_갱신(content: let content):
+ return .send(.inner(.URL_유효성_확인))
+ case .컨텐츠_상세_조회_API_반영(content: let content):
state.domain.content = content
state.domain.data = content.data
state.domain.contentId = content.id
@@ -270,14 +273,13 @@ private extension ContentSettingFeature {
state.domain.memo = content.memo
state.domain.alertYn = content.alertYn
state.contentLoading = false
- return .run { [
- id = content.category.categoryId,
- sharedCategoryId = state.categoryId
- ] send in
- await send(.inner(.parsingURL))
- await send(.async(.카테고리_상세_조회(id: id, sharedId: sharedCategoryId)))
- }
- case .카테고리_갱신(category: let category):
+ let id = content.category.categoryId
+
+ return .merge(
+ .send(.inner(.URL_유효성_확인)),
+ .send(.async(.카테고리_상세_조회_API(id: id, sharedId: state.categoryId)))
+ )
+ case .카테고리_상세_조회_API_반영(category: let category):
state.selectedPokit = BaseCategoryItem(
id: category.categoryId,
userId: 0,
@@ -287,7 +289,7 @@ private extension ContentSettingFeature {
createdAt: ""
)
return .none
- case .카테고리_목록_갱신(categoryList: let categoryList):
+ case .카테고리_목록_조회_API_반영(categoryList: let categoryList):
/// - `카테고리_목록_조회`의 filter 옵션을 `false`로 해두었기 때문에 `미분류` 카테고리 또한 항목에서 조회가 가능함
/// [1]. `미분류`에 해당하는 인덱스 번호와 항목을 체크, 없다면 목록갱신이 불가함
@@ -307,15 +309,6 @@ private extension ContentSettingFeature {
state.selectedPokit = unclassifiedItem
}
return .none
- case let .showLinkPopup(url):
- guard let url else { return .none }
- state.link = url.absoluteString
- state.showDetectedURLPopup = true
- return .none
- case .링크미리보기_presented:
- state.showLinkPreview = true
- return .none
-
case .선택한_포킷_인메모리_삭제:
state.selectedPokit = nil
return .none
@@ -325,96 +318,75 @@ private extension ContentSettingFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .컨텐츠_상세_조회(id: let id):
+ case .컨텐츠_상세_조회_API(id: let id):
state.contentLoading = true
- return .run { [id] send in
+ return .run { send in
let content = try await contentClient.컨텐츠_상세_조회("\(id)").toDomain()
- await send(.inner(.컨텐츠_갱신(content: content)))
+ await send(.inner(.컨텐츠_상세_조회_API_반영(content: content)))
}
- case let .카테고리_상세_조회(id, sharedId):
+ case let .카테고리_상세_조회_API(id, sharedId):
return .run { send in
if let sharedId {
let category = try await categoryClient.카테고리_상세_조회("\(sharedId)").toDomain()
- await send(.inner(.카테고리_갱신(category: category)))
+ await send(.inner(.카테고리_상세_조회_API_반영(category: category)))
} else if let id {
let category = try await categoryClient.카테고리_상세_조회("\(id)").toDomain()
- await send(.inner(.카테고리_갱신(category: category)))
+ await send(.inner(.카테고리_상세_조회_API_반영(category: category)))
}
-
}
- case .카테고리_목록_조회:
- return .run { [
- pageable = state.domain.pageable,
- id = state.domain.categoryId,
- sharedId = state.categoryId
- ] send in
- let categoryList = try await categoryClient.카테고리_목록_조회(
- BasePageableRequest(
- page: pageable.page,
- size: 100,
- sort: pageable.sort
- ),
- false
- ).toDomain()
-
- await send(.async(.카테고리_상세_조회(id: id, sharedId: sharedId)))
- await send(.inner(.카테고리_목록_갱신(categoryList: categoryList)), animation: .pokitDissolve)
- }
- case .컨텐츠_수정:
- guard let contentId = state.domain.contentId else {
- return .none
- }
- guard let categoryId = state.selectedPokit?.id else {
+ case .카테고리_목록_조회_API:
+ let request = BasePageableRequest(
+ page: state.domain.pageable.page,
+ size: 30,
+ sort: state.domain.pageable.sort
+ )
+ let id = state.domain.categoryId
+ let sharedId = state.categoryId
+ return .merge(
+ .send(.async(.카테고리_상세_조회_API(id: id, sharedId: sharedId))),
+ categoryListFetch(request: request)
+ )
+ case .컨텐츠_수정_API:
+ guard let contentId = state.domain.contentId,
+ let categoryId = state.selectedPokit?.id else {
return .none
}
- return .run { [
- id = contentId,
- data = state.domain.data,
- title = state.domain.title,
- categoryId = categoryId,
- memo = state.domain.memo,
- alertYn = state.domain.alertYn,
- thumbNail = state.domain.thumbNail
- ] send in
- let _ = try await contentClient.컨텐츠_수정(
- "\(id)",
- ContentBaseRequest(
- data: data,
- title: title,
- categoryId: categoryId,
- memo: memo,
- alertYn: alertYn.rawValue,
- thumbNail: thumbNail
- )
- )
- await send(.inner(.선택한_포킷_인메모리_삭제))
- await send(.delegate(.저장하기_완료))
- }
- case .컨텐츠_추가:
+ let request = ContentBaseRequest(
+ data: state.domain.data,
+ title: state.domain.title,
+ categoryId: categoryId,
+ memo: state.domain.memo,
+ 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 { [
- data = state.domain.data,
- title = state.domain.title,
- categoryId = categoryId,
- memo = state.domain.memo,
- alertYn = state.domain.alertYn,
- thumbNail = state.domain.thumbNail
- ] send in
- let _ = try await contentClient.컨텐츠_추가(
- ContentBaseRequest(
- data: data,
- title: title,
- categoryId: categoryId,
- memo: memo,
- alertYn: alertYn.rawValue,
- thumbNail: thumbNail
- )
- )
- await send(.inner(.선택한_포킷_인메모리_삭제))
- await send(.delegate(.저장하기_완료))
+ let request = ContentBaseRequest(
+ data: state.domain.data,
+ title: state.domain.title,
+ categoryId: categoryId,
+ memo: state.domain.memo,
+ alertYn: state.domain.alertYn.rawValue,
+ thumbNail: state.domain.thumbNail
+ )
+ return .concatenate(
+ .run { _ in let _ = try await contentClient.컨텐츠_추가(request) },
+ .send(.inner(.선택한_포킷_인메모리_삭제)),
+ .send(.delegate(.저장하기_완료))
+ )
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteboard.changes() {
+ let url = try await pasteboard.probableWebURL()
+ await send(.inner(.linkPopup(url)), animation: .pokitSpring)
+ }
}
}
}
@@ -428,4 +400,20 @@ private extension ContentSettingFeature {
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
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()
+ await send(.inner(.카테고리_목록_조회_API_반영(categoryList: categoryList)), animation: .pokitDissolve)
+ }
+ }
}
diff --git a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
index 705d33b0..7e91b2b5 100644
--- a/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
+++ b/Projects/Feature/FeatureContentSetting/Sources/ContentSetting/ContentSettingView.swift
@@ -56,12 +56,13 @@ public extension ContentSettingView {
isPresented: $store.showMaxCategoryPopup,
type: .text
)
+ .animation(.pokitSpring, value: store.showMaxCategoryPopup)
} else if store.state.showDetectedURLPopup {
PokitLinkPopup(
"복사한 링크 저장하기",
isPresented: $store.showDetectedURLPopup,
type: .link(url: store.link ?? ""),
- action: { send(.linkCopyButtonTapped, animation: .pokitSpring) }
+ action: { send(.링크복사_버튼_눌렀을때, animation: .pokitSpring) }
)
}
}
@@ -74,14 +75,14 @@ public extension ContentSettingView {
"저장하기",
state: isDisable ? .disable : .filled(.primary),
isLoading: $store.saveIsLoading,
- action: { send(.saveBottomButtonTapped) }
+ action: { send(.저장_버튼_눌렀을때) }
)
.padding(.horizontal, 20)
.pokitMaxWidth()
}
.pokitNavigationBar { navigationBar }
.ignoresSafeArea(edges: focusedType == nil ? .bottom : [])
- .onAppear { send(.contentSettingViewOnAppeared) }
+ .onAppear { send(.뷰가_나타났을때) }
}
}
}
@@ -90,8 +91,12 @@ private extension ContentSettingView {
var navigationBar: some View {
PokitHeader(title: store.content == nil ? "링크 추가" : "링크 수정") {
PokitHeaderItems(placement: .leading) {
- PokitToolbarButton(.icon(.arrowLeft)) {
- send(.dismiss)
+ PokitToolbarButton(.icon(
+ store.isShareExtension
+ ? .x
+ : .arrowLeft
+ )) {
+ send(.뒤로가기_버튼_눌렀을때)
}
}
}
@@ -134,7 +139,7 @@ private extension ContentSettingView {
selectedItem: $store.selectedPokit,
label: "포킷",
list: store.pokitList,
- action: { send(.pokitSelectItemButtonTapped(pokit: $0), animation: .pokitDissolve) }
+ action: { send(.포킷선택_항목_눌렀을때(pokit: $0), animation: .pokitDissolve) }
)
}
@@ -144,7 +149,7 @@ private extension ContentSettingView {
state: .filled(.primary),
size: .large,
shape: .rectangle
- ) { send(.addPokitButtonTapped, animation: .pokitSpring) }
+ ) { send(.포킷추가_버튼_눌렀을때, animation: .pokitSpring) }
}
var memoTextArea: some View {
diff --git a/Projects/Feature/FeatureContentSettingTests/Resources/info.plist b/Projects/Feature/FeatureContentSettingTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureContentSettingTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureContentSettingTests/Sources/FeatureContentSettingTests.swift b/Projects/Feature/FeatureContentSettingTests/Sources/FeatureContentSettingTests.swift
new file mode 100644
index 00000000..b6810784
--- /dev/null
+++ b/Projects/Feature/FeatureContentSettingTests/Sources/FeatureContentSettingTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureContentSetting
+
+final class FeatureContentSettingTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsFeature.swift b/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsFeature.swift
index 21dca3b0..bec21026 100644
--- a/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsFeature.swift
@@ -39,21 +39,21 @@ public struct AgreeToTermsFeature {
/// - Binding
case binding(BindingAction)
/// - Button Tapped
- case nextButtonTapped
- case backButtonTapped
- case 개인정보_동의_버튼_클릭
- case 서비스_이용약관_버튼_클릭
- case 마케팅_정보_수신_버튼_클릭
+ case 다음_버튼_눌렀을때
+ case 뒤로가기_버튼_눌렀을때
+ case 개인정보_동의_버튼_눌렀을때
+ case 서비스_이용약관_버튼_눌렀을때
+ case 마케팅_정보_수신_버튼_눌렀을때
}
public enum InnerAction: Equatable {
- case checkAgreements
- case personalAndUsageAgreeSelected
- case serviceAgreeSelected
- case marketingAgreeSelected
- case allAgreementSelected
+ case 동의_체크_확인
+ case 개인정보_동의_선택했을때
+ case 서비스_이용약관_동의_선택했을때
+ case 마케팅_정보_수신_동의_선택했을때
+ case 전체_동의_선택했을때
}
- public enum AsyncAction: Equatable { case doNothing }
- public enum ScopeAction: Equatable { case doNothing }
+ public enum AsyncAction: Equatable { case 없음 }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case pushRegisterNicknameView
}
@@ -91,29 +91,29 @@ private extension AgreeToTermsFeature {
/// - View Effect
func handleViewAction(_ action: Action.ViewAction, state: inout State) -> Effect {
switch action {
- case .nextButtonTapped:
+ case .다음_버튼_눌렀을때:
return .send(.delegate(.pushRegisterNicknameView))
- case .backButtonTapped:
+ case .뒤로가기_버튼_눌렀을때:
return .run { _ in await self.dismiss() }
case .binding(\.isAgreeAllTerms):
- return .send(.inner(.allAgreementSelected))
+ return .send(.inner(.전체_동의_선택했을때))
case .binding(\.isPersonalAndUsageArgee):
- return .send(.inner(.personalAndUsageAgreeSelected))
+ return .send(.inner(.개인정보_동의_선택했을때))
case .binding(\.isServiceAgree):
- return .send(.inner(.serviceAgreeSelected))
+ return .send(.inner(.서비스_이용약관_동의_선택했을때))
case .binding(\.isMarketingAgree):
- return .send(.inner(.marketingAgreeSelected))
+ return .send(.inner(.마케팅_정보_수신_동의_선택했을때))
case .binding:
return .none
- case .개인정보_동의_버튼_클릭:
+ case .개인정보_동의_버튼_눌렀을때:
state.webViewURL = Constants.개인정보_처리방침_주소
state.isWebViewPresented = true
return .none
- case .서비스_이용약관_버튼_클릭:
+ case .서비스_이용약관_버튼_눌렀을때:
state.webViewURL = Constants.서비스_이용약관_주소
state.isWebViewPresented = true
return .none
- case .마케팅_정보_수신_버튼_클릭:
+ case .마케팅_정보_수신_버튼_눌렀을때:
state.webViewURL = Constants.마케팅_정보_수신_주소
state.isWebViewPresented = true
return .none
@@ -123,19 +123,19 @@ private extension AgreeToTermsFeature {
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
/// - 개별 동의 체크 박스를 확인 하여 전체 동의 체크 여부 결정
- case .checkAgreements:
+ case .동의_체크_확인:
let isAgreeAllterm = state.isPersonalAndUsageArgee
&& state.isServiceAgree
&& state.isMarketingAgree
state.isAgreeAllTerms = isAgreeAllterm
return .none
/// - 각각의 개별 동의 체크박스가 선택 되었을 때
- case .personalAndUsageAgreeSelected,
- .serviceAgreeSelected,
- .marketingAgreeSelected:
- return .send(.inner(.checkAgreements))
+ case .개인정보_동의_선택했을때,
+ .서비스_이용약관_동의_선택했을때,
+ .마케팅_정보_수신_동의_선택했을때:
+ return .send(.inner(.동의_체크_확인))
/// - 전체 동의 체크박으가 선택 되었을 때
- case .allAgreementSelected:
+ case .전체_동의_선택했을때:
state.isPersonalAndUsageArgee = state.isAgreeAllTerms
state.isServiceAgree = state.isAgreeAllTerms
state.isMarketingAgree = state.isAgreeAllTerms
diff --git a/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsView.swift b/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsView.swift
index 39e20bbb..3361dae5 100644
--- a/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/AgreeToTerms/AgreeToTermsView.swift
@@ -40,7 +40,7 @@ public extension AgreeToTermsView {
PokitBottomButton(
"다음",
state: store.isPersonalAndUsageArgee && store.isServiceAgree ? .filled(.primary) : .disable,
- action: { send(.nextButtonTapped) }
+ action: { send(.다음_버튼_눌렀을때) }
)
}
.pokitMaxWidth()
@@ -49,7 +49,7 @@ public extension AgreeToTermsView {
PokitHeader {
PokitHeaderItems(placement: .leading) {
PokitToolbarButton(.icon(.arrowLeft)) {
- send(.backButtonTapped)
+ send(.뒤로가기_버튼_눌렀을때)
}
}
}
@@ -98,19 +98,19 @@ extension AgreeToTermsView {
termsButton(
"(필수)개인정보 수집 및 이용 동의",
isSelected: $store.isPersonalAndUsageArgee,
- action: { send(.개인정보_동의_버튼_클릭) }
+ action: { send(.개인정보_동의_버튼_눌렀을때) }
)
termsButton(
"(필수)서비스 이용약관",
isSelected: $store.isServiceAgree,
- action: { send(.서비스_이용약관_버튼_클릭) }
+ action: { send(.서비스_이용약관_버튼_눌렀을때) }
)
termsButton(
"(선택)마케팅 정보 수신",
isSelected: $store.isMarketingAgree,
- action: { send(.마케팅_정보_수신_버튼_클릭) }
+ action: { send(.마케팅_정보_수신_버튼_눌렀을때) }
)
}
.padding(.leading, 20)
diff --git a/Projects/App/Sources/Intro/IntroFeature.swift b/Projects/Feature/FeatureLogin/Sources/Intro/IntroFeature.swift
similarity index 96%
rename from Projects/App/Sources/Intro/IntroFeature.swift
rename to Projects/Feature/FeatureLogin/Sources/Intro/IntroFeature.swift
index e0f39160..584b66be 100644
--- a/Projects/App/Sources/Intro/IntroFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/Intro/IntroFeature.swift
@@ -6,12 +6,11 @@
import ComposableArchitecture
import CoreKit
-import FeatureLogin
@Reducer
public struct IntroFeature {
/// - Dependency
- @Dependency(\.userDefaults) var userDefaults
+ @Dependency(UserDefaultsClient.self) var userDefaults
/// - State
@ObservableState
public enum State {
diff --git a/Projects/App/Sources/Intro/IntroView.swift b/Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift
similarity index 100%
rename from Projects/App/Sources/Intro/IntroView.swift
rename to Projects/Feature/FeatureLogin/Sources/Intro/IntroView.swift
diff --git a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift
index 990b656a..edc5e139 100644
--- a/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/Login/LoginFeature.swift
@@ -11,21 +11,27 @@ import Util
@Reducer
public struct LoginFeature {
/// - Dependency
- @Dependency(\.dismiss) var dismiss
- @Dependency(\.socialLogin) var socialLogin
- @Dependency(\.authClient) var authClient
- @Dependency(\.userClient) var userClient
- @Dependency(\.userDefaults) var userDefaults
- @Dependency(\.keychain) var keychain
+ @Dependency(\.dismiss)
+ var dismiss
+ @Dependency(SocialLoginClient.self)
+ var socialLogin
+ @Dependency(AuthClient.self)
+ var authClient
+ @Dependency(UserClient.self)
+ var userClient
+ @Dependency(UserDefaultsClient.self)
+ var userDefaults
+ @Dependency(KeychainClient.self)
+ var keychain
/// - State
@ObservableState
public struct State {
var path = StackState()
- var nickName: String?
- var interests: [String]?
+ var nickName: String? = nil
+ var interests: [String]? = nil
- public init() {}
+ public init() { }
}
/// - Action
public enum Action: FeatureAction, ViewAction {
@@ -39,21 +45,22 @@ public struct LoginFeature {
@CasePathable
public enum View: Equatable {
/// - Button Tapped
- case appleLoginButtonTapped
- case googleLoginButtonTapped
+ case 애플로그인_버튼_눌렀을때
+ case 구글로그인_버튼_눌렀을때
}
public enum InnerAction: Equatable {
- case pushAgreeToTermsView
- case pushRegisterNicknameView
- case pushSelectFieldView(nickname: String)
- case pushSignUpDoneView
- case 애플로그인(SocialLoginInfo)
- case 구글로그인(SocialLoginInfo)
+ case 약관동의_화면이동
+ case 닉네임_등록_화면이동
+ case 관심분야_선택_화면이동(nickname: String)
+ case 회원가입_완료_화면이동
case 로그인_이후_화면이동(isRegistered: Bool)
}
public enum AsyncAction: Equatable {
- case 회원가입
- case 로그인(SocialLoginInfo)
+ case 회원가입_API
+ case 애플로그인_API(SocialLoginInfo)
+ case 구글로그인_API(SocialLoginInfo)
+ case 애플로그인_소셜_API
+ case 구글로그인_소셜_API
}
public enum ScopeAction {
case agreeToTerms(AgreeToTermsFeature.Action.DelegateAction)
@@ -100,34 +107,48 @@ private extension LoginFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .appleLoginButtonTapped:
- return .run { send in
- let response = try await socialLogin.appleLogin()
- await send(.async(.로그인(response)))
- }
-
- case .googleLoginButtonTapped:
- return .run { send in
- let response = try await socialLogin.googleLogin()
- await send(.async(.로그인(response)))
- }
+ case .애플로그인_버튼_눌렀을때:
+ return .send(.async(.애플로그인_소셜_API))
+ case .구글로그인_버튼_눌렀을때:
+ return .send(.async(.구글로그인_소셜_API))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .pushAgreeToTermsView:
+ case .약관동의_화면이동:
state.path.append(.agreeToTerms(AgreeToTermsFeature.State()))
return .none
- case .pushRegisterNicknameView:
+ case .닉네임_등록_화면이동:
state.path.append(.registerNickname(RegisterNicknameFeature.State()))
return .none
- case .pushSelectFieldView(let nickname):
+ case .관심분야_선택_화면이동(let nickname):
state.path.append(.selecteField(SelectFieldFeature.State(nickname: nickname)))
return .none
- case .pushSignUpDoneView:
+ case .회원가입_완료_화면이동:
return .send(.delegate(.회원가입_완료_화면_이동))
- case let .애플로그인(response):
+ case let .로그인_이후_화면이동(isRegistered):
+ /// [3]. 이미 회원가입했던 유저라면 `메인`이동
+ if isRegistered {
+ return .run { send in await send(.delegate(.dismissLoginRootView)) }
+ } else {
+ return .run { send in await send(.inner(.약관동의_화면이동)) }
+ }
+ }
+ }
+ /// - Async Effect
+ func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
+ switch action {
+ case .회원가입_API:
+ return .run { [nickName = state.nickName, interests = state.interests] send in
+ guard let nickName else { return }
+ guard let interests else { return }
+ let signUpRequest = SignupRequest(nickName: nickName, interests: interests)
+ let _ = try await userClient.회원등록(signUpRequest)
+
+ await send(.inner(.회원가입_완료_화면이동))
+ }
+ case let .애플로그인_API(response):
return .run { send in
guard let idToken = response.idToken else { return }
guard let authCode = response.authCode else { return }
@@ -151,7 +172,7 @@ private extension LoginFeature {
await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered)))
}
- case let .구글로그인(response):
+ case let .구글로그인_API(response):
return .run { send in
guard let idToken = response.idToken else { return }
let platform = response.provider.description
@@ -167,34 +188,15 @@ private extension LoginFeature {
await send(.inner(.로그인_이후_화면이동(isRegistered: tokenResponse.isRegistered)))
}
- case let .로그인_이후_화면이동(isRegistered):
- /// [3]. 이미 회원가입했던 유저라면 `메인`이동
- if isRegistered {
- return .run { send in await send(.delegate(.dismissLoginRootView)) }
- } else {
- return .run { send in await send(.inner(.pushAgreeToTermsView)) }
- }
- }
- }
- /// - Async Effect
- func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
- switch action {
- case .회원가입:
- return .run { [nickName = state.nickName, interests = state.interests] send in
- guard let nickName else { return }
- guard let interests else { return }
- let signUpRequest = SignupRequest(nickName: nickName, interests: interests)
- let _ = try await userClient.회원등록(signUpRequest)
-
- await send(.inner(.pushSignUpDoneView))
+ case .애플로그인_소셜_API:
+ return .run { send in
+ let response = try await socialLogin.appleLogin()
+ await send(.async(.애플로그인_API(response)))
}
-
- case .로그인(let response):
- switch response.provider {
- case .apple:
- return .run { send in await send(.inner(.애플로그인(response))) }
- case .google:
- return .run { send in await send(.inner(.구글로그인(response))) }
+ case .구글로그인_소셜_API:
+ return .run { send in
+ let response = try await socialLogin.googleLogin()
+ await send(.async(.구글로그인_API(response)))
}
}
}
@@ -204,19 +206,19 @@ private extension LoginFeature {
case .agreeToTerms(let delegate):
switch delegate {
case .pushRegisterNicknameView:
- return .send(.inner(.pushRegisterNicknameView))
+ return .send(.inner(.닉네임_등록_화면이동))
}
case .registerNickname(let delegate):
switch delegate {
case .pushSelectFieldView(let nickname):
state.nickName = nickname
- return .send(.inner(.pushSelectFieldView(nickname: nickname)))
+ return .send(.inner(.관심분야_선택_화면이동(nickname: nickname)))
}
case .selectField(let delegate):
switch delegate {
case let .pushSignUpDoneView(interests):
state.interests = interests
- return .send(.async(.회원가입))
+ return .send(.async(.회원가입_API))
}
}
}
diff --git a/Projects/Feature/FeatureLogin/Sources/Login/LoginView.swift b/Projects/Feature/FeatureLogin/Sources/Login/LoginView.swift
index 17519224..7452a7e1 100644
--- a/Projects/Feature/FeatureLogin/Sources/Login/LoginView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/Login/LoginView.swift
@@ -82,7 +82,7 @@ extension LoginView {
private var appleLoginButton: some View {
Button {
- send(.appleLoginButtonTapped)
+ send(.애플로그인_버튼_눌렀을때)
} label: {
appleLoginButtonLabel
}
@@ -123,7 +123,7 @@ extension LoginView {
private var googleLoginButton: some View {
Button {
- send(.googleLoginButtonTapped)
+ send(.구글로그인_버튼_눌렀을때)
} label: {
googleLoginButtonLabel
}
diff --git a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameFeature.swift b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameFeature.swift
index c1f67009..ee5bf4df 100644
--- a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameFeature.swift
@@ -14,8 +14,8 @@ import Util
public struct RegisterNicknameFeature {
/// - Dependency
@Dependency(\.dismiss) var dismiss
- @Dependency(\.userClient) var userClient
@Dependency(\.mainQueue) var mainQueue
+ @Dependency(UserClient.self) var userClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -45,17 +45,19 @@ public struct RegisterNicknameFeature {
/// - Binding
case binding(BindingAction)
/// - Button Tapped
- case nextButtonTapped
- case backButtonTapped
+ case 다음_버튼_눌렀을때
+ case dismiss
}
+
public enum InnerAction: Equatable {
- case textChanged
- case 닉네임_중복_체크_네트워크_결과(Bool)
+ case 닉네임_텍스트_변경되었을때
+ case 닉네임_중복_체크_API_반영(Bool)
}
+
public enum AsyncAction: Equatable {
- case 닉네임_중복_체크_네트워크
+ case 닉네임_중복_체크_API
}
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case pushSelectFieldView(nickname: String)
}
@@ -94,18 +96,14 @@ private extension RegisterNicknameFeature {
/// - View Effect
func handleViewAction(_ action: Action.ViewAction, state: inout State) -> Effect {
switch action {
- case .nextButtonTapped:
- return .run { [nickName = state.nicknameText] send in
- await send(.delegate(.pushSelectFieldView(nickname: nickName)))
- }
- case .backButtonTapped:
+ case .다음_버튼_눌렀을때:
+ let nickname = state.nicknameText
+ return .send(.delegate(.pushSelectFieldView(nickname: nickname)))
+ case .dismiss:
return .run { _ in await self.dismiss() }
case .binding(\.nicknameText):
state.buttonActive = false
- return .run { send in
- await send(.inner(.textChanged))
- }
- .debounce(
+ return .send(.inner(.닉네임_텍스트_변경되었을때)).debounce(
id: CancelID.response,
for: 0.5,
scheduler: mainQueue
@@ -117,7 +115,7 @@ private extension RegisterNicknameFeature {
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .textChanged:
+ case .닉네임_텍스트_변경되었을때:
/// [1]. 닉네임 텍스트필드가 비어있을 때
if state.nicknameText.isEmpty {
state.buttonActive = false
@@ -136,10 +134,10 @@ private extension RegisterNicknameFeature {
return .none
} else {
/// [4]. 정상 케이스일 때
- return .run { send in await send(.async(.닉네임_중복_체크_네트워크)) }
+ return .run { send in await send(.async(.닉네임_중복_체크_API)) }
}
- case let .닉네임_중복_체크_네트워크_결과(isDuplicate):
+ case let .닉네임_중복_체크_API_반영(isDuplicate):
if isDuplicate {
state.textfieldState = .error(message: "중복된 닉네임입니다.")
state.buttonActive = false
@@ -153,10 +151,10 @@ private extension RegisterNicknameFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .닉네임_중복_체크_네트워크:
+ case .닉네임_중복_체크_API:
return .run { [nickName = state.nicknameText] send in
let result = try await userClient.닉네임_중복_체크(nickName)
- await send(.inner(.닉네임_중복_체크_네트워크_결과(result.isDuplicate)))
+ await send(.inner(.닉네임_중복_체크_API_반영(result.isDuplicate)))
}
}
}
diff --git a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
index e321c920..83cf8c24 100644
--- a/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/RegisterNickname/RegisterNicknameView.swift
@@ -42,7 +42,7 @@ public extension RegisterNicknameView {
state: store.buttonActive
? .filled(.primary)
: .disable,
- action: { send(.nextButtonTapped) }
+ action: { send(.다음_버튼_눌렀을때) }
)
.setKeyboardHeight()
}
@@ -52,7 +52,7 @@ public extension RegisterNicknameView {
PokitHeader {
PokitHeaderItems(placement: .leading) {
PokitToolbarButton(.icon(.arrowLeft)) {
- send(.backButtonTapped)
+ send(.dismiss)
}
}
}
diff --git a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift
index 30bb71eb..38771fd6 100644
--- a/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/SelectField/SelectFieldFeature.swift
@@ -13,7 +13,7 @@ import Util
public struct SelectFieldFeature {
/// - Dependency
@Dependency(\.dismiss) var dismiss
- @Dependency(\.userClient) var userClient
+ @Dependency(UserClient.self) var userClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -48,8 +48,8 @@ public struct SelectFieldFeature {
public enum InnerAction: Equatable {
case 관심사_목록_조회_결과(interests: [InterestResponse])
}
- public enum AsyncAction: Equatable { case doNothing }
- public enum ScopeAction: Equatable { case doNothing }
+ public enum AsyncAction: Equatable { case 없음 }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case pushSignUpDoneView(interests: [String])
}
diff --git a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift
index df3fa547..4a9a50ff 100644
--- a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneFeature.swift
@@ -35,16 +35,16 @@ public struct SignUpDoneFeature {
@CasePathable
public enum View: Equatable {
/// - Button Tapped
- case startButtonTapped
+ case 시작_버튼_눌렀을때
- case firecrackerOnAppeared
- case titleOnAppeared
- case confettiOnAppeared
- case pookiOnAppeared
+ case 폭죽불꽃_이미지_나타났을때
+ case 제목_나타났을때
+ case 폭죽_이미지_나타났을때
+ case 푸키_이미지_나타났을때
}
- public enum InnerAction: Equatable { case doNothing }
- public enum AsyncAction: Equatable { case doNothing }
- public enum ScopeAction: Equatable { case doNothing }
+ public enum InnerAction: Equatable { case 없음 }
+ public enum AsyncAction: Equatable { case 없음 }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case dismissLoginRootView
}
@@ -81,18 +81,18 @@ private extension SignUpDoneFeature {
/// - View Effect
func handleViewAction(_ action: Action.ViewAction, state: inout State) -> Effect {
switch action {
- case .startButtonTapped:
+ case .시작_버튼_눌렀을때:
return .send(.delegate(.dismissLoginRootView), animation: .pokitDissolve)
- case .firecrackerOnAppeared:
+ case .폭죽불꽃_이미지_나타났을때:
state.firecrackIsAppear = true
return .none
- case .titleOnAppeared:
+ case .제목_나타났을때:
state.titleIsAppear = true
return .none
- case .confettiOnAppeared:
+ case .폭죽_이미지_나타났을때:
state.confettiIsAppear = true
return .none
- case .pookiOnAppeared:
+ case .푸키_이미지_나타났을때:
state.pookiIsAppear = true
return .none
}
diff --git a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift
index d5f5c0de..018e4360 100644
--- a/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift
+++ b/Projects/Feature/FeatureLogin/Sources/SignUpDone/SignUpDoneView.swift
@@ -41,7 +41,7 @@ public extension SignUpDoneView {
PokitBottomButton(
"시작하기",
state: .filled(.primary),
- action: { send(.startButtonTapped) }
+ action: { send(.시작_버튼_눌렀을때) }
)
.pokitMaxWidth()
.padding(.horizontal, 20)
@@ -65,7 +65,7 @@ extension SignUpDoneView {
store.firecrackIsAppear ? 1 : 0,
anchor: .bottomTrailing
)
- .onAppear { send(.firecrackerOnAppeared, animation: .pokitSpring) }
+ .onAppear { send(.폭죽불꽃_이미지_나타났을때, animation: .pokitSpring) }
Spacer()
}
@@ -83,7 +83,7 @@ extension SignUpDoneView {
.multilineTextAlignment(.center)
}
.opacity(store.titleIsAppear ? 1 : 0)
- .onAppear { send(.titleOnAppeared, animation: .pokitDissolve) }
+ .onAppear { send(.제목_나타났을때, animation: .pokitDissolve) }
}
private var images: some View {
@@ -97,7 +97,7 @@ extension SignUpDoneView {
store.confettiIsAppear ? 1 : 0,
anchor: .bottom
)
- .onAppear { send(.confettiOnAppeared, animation: .pokitSpring) }
+ .onAppear { send(.폭죽_이미지_나타났을때, animation: .pokitSpring) }
Image(.character(.pooki))
.resizable()
@@ -107,7 +107,7 @@ extension SignUpDoneView {
store.pookiIsAppear ? 1 : 0,
anchor: .bottom
)
- .onAppear{ send(.pookiOnAppeared, animation: .pokitSpring) }
+ .onAppear{ send(.푸키_이미지_나타났을때, animation: .pokitSpring) }
}
}
}
diff --git a/Projects/App/Sources/Splash/SplashFeature.swift b/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift
similarity index 93%
rename from Projects/App/Sources/Splash/SplashFeature.swift
rename to Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift
index da062e25..4d8b4acd 100644
--- a/Projects/App/Sources/Splash/SplashFeature.swift
+++ b/Projects/Feature/FeatureLogin/Sources/Splash/SplashFeature.swift
@@ -13,10 +13,14 @@ import Util
@Reducer
public struct SplashFeature {
/// - Dependency
- @Dependency(\.continuousClock) var clock
- @Dependency(\.userDefaults) var userDefaults
- @Dependency(\.authClient) var authClient
- @Dependency(\.keychain) var keychain
+ @Dependency(\.continuousClock)
+ var clock
+ @Dependency(UserDefaultsClient.self)
+ var userDefaults
+ @Dependency(AuthClient.self)
+ var authClient
+ @Dependency(KeychainClient.self)
+ var keychain
/// - State
@ObservableState
public struct State: Equatable {
@@ -122,14 +126,14 @@ private extension SplashFeature {
}
/// 🚨 Error Case [1]: 로그인 했던 플랫폼 정보가 없을 때
guard let _ = userDefaults.stringKey(.authPlatform) else {
- await send(.delegate(.loginNeeded))
+ await send(.delegate(.loginNeeded), animation: .smooth)
return
}
/// 🚨 Error Case [2]: refresh Token이 없을 때
guard let refreshToken = keychain.read(.refreshToken) else {
keychain.delete(.accessToken)
keychain.delete(.refreshToken)
- await send(.delegate(.loginNeeded))
+ await send(.delegate(.loginNeeded), animation: .smooth)
return
}
@@ -139,9 +143,8 @@ private extension SplashFeature {
keychain.save(.accessToken, tokenResponse.accessToken)
await send(.delegate(.autoLoginSuccess))
} catch {
- await send(.delegate(.loginNeeded))
+ await send(.delegate(.loginNeeded), animation: .smooth)
}
- await send(.delegate(.loginNeeded))
}
}
}
diff --git a/Projects/App/Sources/Splash/SplashView.swift b/Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift
similarity index 100%
rename from Projects/App/Sources/Splash/SplashView.swift
rename to Projects/Feature/FeatureLogin/Sources/Splash/SplashView.swift
diff --git a/Projects/Feature/FeatureLoginTests/Resources/info.plist b/Projects/Feature/FeatureLoginTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureLoginTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureLoginTests/Sources/FeatureLoginTests.swift b/Projects/Feature/FeatureLoginTests/Sources/FeatureLoginTests.swift
new file mode 100644
index 00000000..0ead1e32
--- /dev/null
+++ b/Projects/Feature/FeatureLoginTests/Sources/FeatureLoginTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureLogin
+
+final class FeatureLoginTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureMyFolderTests/Resources/info.plist b/Projects/Feature/FeatureMyFolderTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureMyFolderTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureMyFolderTests/Sources/FeatureMyFolderTests.swift b/Projects/Feature/FeatureMyFolderTests/Sources/FeatureMyFolderTests.swift
new file mode 100644
index 00000000..96dd47e4
--- /dev/null
+++ b/Projects/Feature/FeatureMyFolderTests/Sources/FeatureMyFolderTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureMyFolder
+
+final class FeatureMyFolderTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureMyPageTests/Resources/info.plist b/Projects/Feature/FeatureMyPageTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureMyPageTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureMyPageTests/Sources/FeatureMyPageTests.swift b/Projects/Feature/FeatureMyPageTests/Sources/FeatureMyPageTests.swift
new file mode 100644
index 00000000..ff92c2db
--- /dev/null
+++ b/Projects/Feature/FeatureMyPageTests/Sources/FeatureMyPageTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureMyPage
+
+final class FeatureMyPageTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
index 429a9b83..87ec21cf 100644
--- a/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
+++ b/Projects/Feature/FeaturePokit/Sources/PokitRootFeature.swift
@@ -5,21 +5,20 @@
// Created by 김민호 on 7/16/24.
import ComposableArchitecture
+import FeatureContentCard
import Domain
import CoreKit
import DSKit
import Util
-/// `unclassified`: 미분류 키워드
-
@Reducer
public struct PokitRootFeature {
/// - Dependency
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
- @Dependency(\.kakaoShareClient)
+ @Dependency(KakaoShareClient.self)
private var kakaoShareClient
/// - State
@ObservableState
@@ -38,16 +37,7 @@ public struct PokitRootFeature {
}
return identifiedArray
}
- var unclassifiedContents: IdentifiedArrayOf? {
- guard let unclassifiedContentList = domain.unclassifiedContentList.data else {
- return nil
- }
- var identifiedArray = IdentifiedArrayOf()
- unclassifiedContentList.forEach { content in
- identifiedArray.append(content)
- }
- return identifiedArray
- }
+ var contents: IdentifiedArrayOf = []
var selectedKebobItem: BaseCategoryItem?
var selectedUnclassifiedItem: BaseContentItem?
@@ -55,13 +45,10 @@ public struct PokitRootFeature {
var isKebobSheetPresented: Bool = false
var isPokitDeleteSheetPresented: Bool = false
- var hasNext: Bool {
- domain.categoryList.hasNext
- }
-
- var unclassifiedHasNext: Bool {
- domain.unclassifiedContentList.hasNext
- }
+
+ var hasNext: Bool { domain.categoryList.hasNext }
+ var unclassifiedHasNext: Bool { domain.unclassifiedContentList.hasNext }
+ var isLoading: Bool = true
public init() { }
}
@@ -73,59 +60,56 @@ public struct PokitRootFeature {
case async(AsyncAction)
case scope(ScopeAction)
case delegate(DelegateAction)
+ case contents(IdentifiedActionOf)
@CasePathable
public enum View: BindableAction, Equatable {
- /// - Binding
case binding(BindingAction)
- /// - Navigaiton Bar
- case searchButtonTapped
- case alertButtonTapped
- case settingButtonTapped
- /// - Filter
- case filterButtonTapped(PokitRootFilterType.Folder)
- case sortButtonTapped
- /// - Kebob
- case kebobButtonTapped(BaseCategoryItem)
- case unclassifiedKebobButtonTapped(BaseContentItem)
-
- case categoryTapped(BaseCategoryItem)
- case contentItemTapped(BaseContentItem)
-
- case 링크_공유_완료
-
- case pokitRootViewOnAppeared
-
- case 다음페이지_로딩_presented
+ case 검색_버튼_눌렀을때
+ case 알람_버튼_눌렀을때
+ case 설정_버튼_눌렀을때
+ case 필터_버튼_눌렀을때(PokitRootFilterType.Folder)
+ case 분류_버튼_눌렀을때
+ case 케밥_버튼_눌렀을때(BaseCategoryItem)
+ case 미분류_케밥_버튼_눌렀을때(BaseContentItem)
+ case 카테고리_눌렀을때(BaseCategoryItem)
+ case 컨텐츠_항목_눌렀을때(BaseContentItem)
+ case 링크_공유_완료되었을때
+ case 뷰가_나타났을때
+ case 페이지_로딩중일때
}
public enum InnerAction: Equatable {
- case pokitCategorySheetPresented(Bool)
- case pokitDeleteSheetPresented(Bool)
case sort
- case onAppearResult(classified: BaseCategoryListInquiry)
- case 미분류_카테고리_컨텐츠_갱신(contentList: BaseContentListInquiry)
- case 미분류_페이지네이션_결과(contentList: BaseContentListInquiry)
- case 카테고리_갱신(categoryList: BaseCategoryListInquiry)
- case 카테고리_페이지네이션_결과(contentList: BaseCategoryListInquiry)
- case 컨텐츠_삭제(contentId: Int)
+ case 카테고리_시트_활성화(Bool)
+ case 카테고리_삭제_시트_활성화(Bool)
+
+ case 미분류_카테고리_조회_API_반영(contentList: BaseContentListInquiry)
+ case 미분류_카테고리_페이징_조회_API_반영(contentList: BaseContentListInquiry)
+ case 미분류_카테고리_컨텐츠_삭제_API_반영(contentId: Int)
+
+ case 카테고리_조회_API_반영(categoryList: BaseCategoryListInquiry)
+ case 카테고리_페이징_조회_API_반영(contentList: BaseCategoryListInquiry)
+
case 페이지네이션_초기화
}
public enum AsyncAction: Equatable {
- case 포킷삭제(categoryId: Int)
- case 미분류_카테고리_컨텐츠_페이징_조회
- case 카테고리_페이징_조회
- case 미분류_카테고리_컨텐츠_조회
- case 카테고리_조회
- case 미분류_카테고리_컨텐츠_페이징_재조회
- case 카테고리_페이징_재조회
- case 미분류_카테고리_컨텐츠_삭제(contentId: Int)
+ case 카테고리_조회_API
+ case 카테고리_페이징_조회_API
+ case 카테고리_페이징_재조회_API
+ case 카테고리_삭제_API(categoryId: Int)
+
+ case 미분류_카테고리_조회_API
+ case 미분류_카테고리_페이징_조회_API
+ case 미분류_카테고리_페이징_재조회_API
+ case 미분류_카테고리_컨텐츠_삭제_API(contentId: Int)
}
- public enum ScopeAction: Equatable {
+ public enum ScopeAction {
case bottomSheet(PokitBottomSheet.Delegate)
case deleteBottomSheet(PokitDeleteBottomSheet.Delegate)
+ case contents(IdentifiedActionOf)
}
public enum DelegateAction: Equatable {
@@ -167,6 +151,9 @@ public struct PokitRootFeature {
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
+
+ case .contents(let contentsAciton):
+ return .send(.scope(.contents(contentsAciton)))
}
}
@@ -174,6 +161,10 @@ public struct PokitRootFeature {
public var body: some ReducerOf {
BindingReducer(action: \.view)
Reduce(self.core)
+ .forEach(\.contents, action: \.contents) {
+ ContentCardFeature()
+ }
+
}
}
//MARK: - FeatureAction Effect
@@ -181,73 +172,80 @@ private extension PokitRootFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- /// - Binding Action
case .binding:
return .none
- /// - Navigation Bar Tapped Action
- case .searchButtonTapped:
+ /// - Navigation Bar
+ case .검색_버튼_눌렀을때:
return .run { send in await send(.delegate(.searchButtonTapped)) }
- case .alertButtonTapped:
+
+ case .알람_버튼_눌렀을때:
return .run { send in await send(.delegate(.alertButtonTapped)) }
- case .settingButtonTapped:
+
+ case .설정_버튼_눌렀을때:
return .run { send in await send(.delegate(.settingButtonTapped)) }
- /// - Filter Action
- /// 포킷 / 미분류 버튼 눌렀을 때
- case .filterButtonTapped(let selectedFolderType):
+
+ case .필터_버튼_눌렀을때(let selectedFolderType):
state.folderType = .folder(selectedFolderType)
state.sortType = .sort(.최신순)
return .send(.inner(.sort))
- /// 최신순 / 이름순 버튼 눌렀을 때
- case .sortButtonTapped:
+
+ case .분류_버튼_눌렀을때:
switch state.folderType {
case .folder(.포킷):
state.sortType = .sort(state.sortType == .sort(.이름순) ? .최신순 : .이름순)
return .send(.inner(.sort), animation: .pokitDissolve)
+
case .folder(.미분류):
state.sortType = .sort(state.sortType == .sort(.오래된순) ? .최신순 : .오래된순)
return .send(.inner(.sort), animation: .pokitDissolve)
+
default: return .none
}
- /// - 케밥버튼 눌렀을 때
- /// 분류된 아이템의 케밥버튼
- case .kebobButtonTapped(let selectedItem):
+
+ case .케밥_버튼_눌렀을때(let selectedItem):
state.selectedKebobItem = selectedItem
- return .run { send in await send(.inner(.pokitCategorySheetPresented(true))) }
- /// 미분류 아이템의 케밥버튼
- case .unclassifiedKebobButtonTapped(let selectedItem):
+ return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
+
+ case .미분류_케밥_버튼_눌렀을때(let selectedItem):
state.selectedUnclassifiedItem = selectedItem
- return .run { send in await send(.inner(.pokitCategorySheetPresented(true))) }
+ return .run { send in await send(.inner(.카테고리_시트_활성화(true))) }
- /// - 카테고리 항목을 눌렀을 때
- case .categoryTapped(let category):
+ case .카테고리_눌렀을때(let category):
return .run { send in await send(.delegate(.categoryTapped(category))) }
/// - 링크 아이템을 눌렀을 때
- case .contentItemTapped(let selectedItem):
+ case .컨텐츠_항목_눌렀을때(let selectedItem):
return .run { send in await send(.delegate(.contentDetailTapped(selectedItem))) }
- case .pokitRootViewOnAppeared:
+
+ case .뷰가_나타났을때:
switch state.folderType {
case .folder(.포킷):
guard let _ = state.domain.categoryList.data?.count else {
return .send(.inner(.페이지네이션_초기화))
}
- return .send(.async(.카테고리_페이징_재조회), animation: .pokitSpring)
+ return .send(.async(.카테고리_페이징_재조회_API), animation: .pokitSpring)
+
case .folder(.미분류):
guard let _ = state.domain.unclassifiedContentList.data?.count else {
return .send(.inner(.페이지네이션_초기화))
}
- return .send(.async(.미분류_카테고리_컨텐츠_페이징_재조회), animation: .pokitSpring)
+ return .send(.async(.미분류_카테고리_페이징_재조회_API), animation: .pokitSpring)
+
default: return .none
}
- case .다음페이지_로딩_presented:
+
+ case .페이지_로딩중일때:
switch state.folderType {
case .folder(.포킷):
- return .send(.async(.카테고리_페이징_조회))
+ return .send(.async(.카테고리_페이징_조회_API))
+
case .folder(.미분류):
- return .send(.async(.미분류_카테고리_컨텐츠_페이징_조회))
+ return .send(.async(.미분류_카테고리_페이징_조회_API))
+
default: return .none
}
- case .링크_공유_완료:
+
+ case .링크_공유_완료되었을때:
state.shareSheetItem = nil
return .none
}
@@ -256,40 +254,46 @@ private extension PokitRootFeature {
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case let .pokitCategorySheetPresented(presented):
+ case let .카테고리_시트_활성화(presented):
state.isKebobSheetPresented = presented
return .none
- case let .pokitDeleteSheetPresented(presented):
+ case let .카테고리_삭제_시트_활성화(presented):
state.isPokitDeleteSheetPresented = presented
return .none
- case let .onAppearResult(classified):
- state.domain.categoryList = classified
- return .none
-
case .sort:
switch state.sortType {
case .sort(.이름순):
state.domain.pageable.sort = ["name,asc"]
return .send(.inner(.페이지네이션_초기화), animation: .pokitDissolve)
+
case .sort(.오래된순):
state.domain.pageable.sort = ["createdAt,asc"]
return .send(.inner(.페이지네이션_초기화), animation: .pokitDissolve)
+
case .sort(.최신순):
state.domain.pageable.sort = ["createdAt,desc"]
return .send(.inner(.페이지네이션_초기화), animation: .pokitDissolve)
+
default: return .none
}
- case .미분류_카테고리_컨텐츠_갱신(contentList: let contentList):
+ case .미분류_카테고리_조회_API_반영(contentList: let contentList):
state.domain.unclassifiedContentList = contentList
+
+ var contents = IdentifiedArrayOf()
+ contentList.data?.forEach { contents.append(.init(content: $0)) }
+ state.contents = contents
+
+ state.isLoading = false
return .none
- case let .카테고리_갱신(categoryList):
+
+ case let .카테고리_조회_API_반영(categoryList):
state.domain.categoryList = categoryList
return .none
- case let .카테고리_페이지네이션_결과(contentList):
+ case let .카테고리_페이징_조회_API_반영(contentList):
let list = state.domain.categoryList.data ?? []
guard let newList = contentList.data else { return .none }
@@ -298,30 +302,41 @@ private extension PokitRootFeature {
state.domain.pageable.size = 10
return .none
- case let .미분류_페이지네이션_결과(contentList):
+ case let .미분류_카테고리_페이징_조회_API_반영(contentList):
let list = state.domain.unclassifiedContentList.data ?? []
guard let newList = contentList.data else { return .none }
state.domain.unclassifiedContentList = contentList
state.domain.unclassifiedContentList.data = list + newList
state.domain.pageable.size = 10
+ newList.forEach { content in
+ state.contents.append(.init(content: content))
+ }
return .none
- case let .컨텐츠_삭제(contentId: contentId):
+
+ case let .미분류_카테고리_컨텐츠_삭제_API_반영(contentId: contentId):
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
return .none
+
case .페이지네이션_초기화:
state.domain.pageable.page = 0
state.domain.categoryList.data = nil
state.domain.unclassifiedContentList.data = nil
+ state.isLoading = true
+ state.contents.removeAll()
+
switch state.folderType {
case .folder(.포킷):
- return .send(.async(.카테고리_조회), animation: .pokitDissolve)
+ return .send(.async(.카테고리_조회_API), animation: .pokitDissolve)
+
case .folder(.미분류):
- return .send(.async(.미분류_카테고리_컨텐츠_조회), animation: .pokitDissolve)
+ return .send(.async(.미분류_카테고리_조회_API), animation: .pokitDissolve)
+
default: return .none
}
}
@@ -330,72 +345,43 @@ private extension PokitRootFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case let .포킷삭제(categoryId):
- return .run { send in
- try await categoryClient.카테고리_삭제(categoryId)
- }
- case .미분류_카테고리_컨텐츠_페이징_조회:
+ case let .카테고리_삭제_API(categoryId):
+ return .run { _ in try await categoryClient.카테고리_삭제(categoryId) }
+
+ case .미분류_카테고리_페이징_조회_API:
state.domain.pageable.page += 1
- return .run { [
- pageable = state.domain.pageable
- ] send in
- let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회(
- .init(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- )
- ).toDomain()
- await send(.inner(.미분류_페이지네이션_결과(contentList: contentList)))
+ return .run { [pageable = state.domain.pageable] send in
+ let request = BasePageableRequest(page: pageable.page, size: pageable.size, sort: pageable.sort)
+ let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회(request).toDomain()
+ await send(.inner(.미분류_카테고리_페이징_조회_API_반영(contentList: contentList)))
}
- case .카테고리_페이징_조회:
+
+ case .카테고리_페이징_조회_API:
state.domain.pageable.page += 1
- return .run { [
- pageable = state.domain.pageable
- ] send in
- let classified = try await categoryClient.카테고리_목록_조회(
- .init(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- ),
- true
- ).toDomain()
- await send(.inner(.카테고리_페이지네이션_결과(contentList: classified)))
+ return .run { [pageable = state.domain.pageable] send in
+ let request = BasePageableRequest(page: pageable.page, size: pageable.size, sort: pageable.sort)
+ let classified = try await categoryClient.카테고리_목록_조회(request, true).toDomain()
+ await send(.inner(.카테고리_페이징_조회_API_반영(contentList: classified)))
}
- case .미분류_카테고리_컨텐츠_조회:
+
+ case .미분류_카테고리_조회_API:
state.domain.pageable.page = 0
- return .run { [
- pageable = state.domain.pageable
- ] send in
- let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회(
- .init(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- )
- ).toDomain()
- await send(.inner(.미분류_카테고리_컨텐츠_갱신(contentList: contentList)), animation: .pokitSpring)
+ return .run { [pageable = state.domain.pageable] send in
+ let request = BasePageableRequest(page: pageable.page, size: pageable.size, sort: pageable.sort)
+ let contentList = try await contentClient.미분류_카테고리_컨텐츠_조회(request).toDomain()
+ await send(.inner(.미분류_카테고리_조회_API_반영(contentList: contentList)), animation: .pokitSpring)
}
- case .카테고리_조회:
+
+ case .카테고리_조회_API:
state.domain.pageable.page = 0
- return .run { [
- pageable = state.domain.pageable
- ] send in
- let classified = try await categoryClient.카테고리_목록_조회(
- .init(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- ),
- true
- ).toDomain()
- await send(.inner(.카테고리_갱신(categoryList: classified)), animation: .pokitSpring)
+ return .run { [pageable = state.domain.pageable] send in
+ let request = BasePageableRequest(page: pageable.page, size: pageable.size, sort: pageable.sort)
+ let classified = try await categoryClient.카테고리_목록_조회(request, true).toDomain()
+ await send(.inner(.카테고리_조회_API_반영(categoryList: classified)), animation: .pokitSpring)
}
- case .미분류_카테고리_컨텐츠_페이징_재조회:
- return .run { [
- pageable = state.domain.pageable
- ] send in
+
+ case .미분류_카테고리_페이징_재조회_API:
+ return .run { [pageable = state.domain.pageable] send in
let stream = AsyncThrowingStream { continuation in
Task {
for page in 0...pageable.page {
@@ -420,14 +406,12 @@ private extension PokitRootFeature {
}
guard let contentItems else { return }
await send(
- .inner(.미분류_카테고리_컨텐츠_갱신(contentList: contentItems)),
+ .inner(.미분류_카테고리_조회_API_반영(contentList: contentItems)),
animation: .pokitSpring
)
}
- case .카테고리_페이징_재조회:
- return .run { [
- pageable = state.domain.pageable
- ] send in
+ case .카테고리_페이징_재조회_API:
+ return .run { [pageable = state.domain.pageable] send in
let stream = AsyncThrowingStream { continuation in
Task {
for page in 0...pageable.page {
@@ -452,15 +436,13 @@ private extension PokitRootFeature {
categoryItems?.data = items + newItems
}
guard let categoryItems else { return }
- await send(
- .inner(.카테고리_갱신(categoryList: categoryItems)),
- animation: .pokitSpring
- )
+ await send(.inner(.카테고리_조회_API_반영(categoryList: categoryItems)), animation: .pokitSpring)
}
- case let .미분류_카테고리_컨텐츠_삭제(contentId):
+
+ case let .미분류_카테고리_컨텐츠_삭제_API(contentId):
return .run { send in
let _ = try await contentClient.컨텐츠_삭제("\(contentId)")
- await send(.inner(.컨텐츠_삭제(contentId: contentId)), animation: .pokitSpring)
+ await send(.inner(.미분류_카테고리_컨텐츠_삭제_API_반영(contentId: contentId)), animation: .pokitSpring)
}
}
@@ -521,8 +503,8 @@ private extension PokitRootFeature {
case .bottomSheet(.deleteCellButtonTapped):
return .run { send in
- await send(.inner(.pokitCategorySheetPresented(false)))
- await send(.inner(.pokitDeleteSheetPresented(true)))
+ await send(.inner(.카테고리_시트_활성화(false)))
+ await send(.inner(.카테고리_삭제_시트_활성화(true)))
}
/// - Pokit Delete BottomSheet Delegate
@@ -537,11 +519,7 @@ private extension PokitRootFeature {
/// 🚨 Error Case [1]: 항목을 삭제하려는데 항목이 없을 때
return .none
}
-
- return .send(
- .async(.미분류_카테고리_컨텐츠_삭제(contentId: selectedItem.id)),
- animation: .pokitSpring
- )
+ return .send(.async(.미분류_카테고리_컨텐츠_삭제_API(contentId: selectedItem.id)), animation: .pokitSpring)
case .folder(.포킷):
guard let selectedItem = state.selectedKebobItem else {
@@ -554,9 +532,19 @@ private extension PokitRootFeature {
state.domain.categoryList.data?.remove(at: index)
state.isPokitDeleteSheetPresented = false
- return .run { send in await send(.async(.포킷삭제(categoryId: selectedItem.id))) }
+ return .run { send in await send(.async(.카테고리_삭제_API(categoryId: selectedItem.id))) }
+
default: return .none
}
+
+ 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)))
+ case .contents:
+ return .none
+
default: return .none
}
}
@@ -567,9 +555,11 @@ private extension PokitRootFeature {
case .미분류_카테고리_컨텐츠_조회:
switch state.folderType {
case .folder(.포킷):
- return .send(.async(.카테고리_페이징_재조회), animation: .pokitSpring)
+ return .send(.async(.카테고리_페이징_재조회_API), animation: .pokitSpring)
+
case .folder(.미분류):
- return .send(.async(.미분류_카테고리_컨텐츠_페이징_재조회), animation: .pokitSpring)
+ return .send(.async(.미분류_카테고리_페이징_재조회_API), animation: .pokitSpring)
+
default: return .none
}
default:
diff --git a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
index b8525b4d..912b10fb 100644
--- a/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
+++ b/Projects/Feature/FeaturePokit/Sources/PokitRootView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureContentCard
import Domain
import DSKit
@@ -49,7 +50,7 @@ public extension PokitRootView {
if let shareURL = URL(string: content.data) {
PokitShareSheet(
items: [shareURL],
- completion: { send(.링크_공유_완료) }
+ completion: { send(.링크_공유_완료되었을때) }
)
.presentationDetents([.medium, .large])
}
@@ -62,7 +63,7 @@ public extension PokitRootView {
delegateSend: { store.send(.scope(.deleteBottomSheet($0)), animation: .pokitSpring) }
)
}
- .task { await send(.pokitRootViewOnAppeared).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
@@ -78,7 +79,7 @@ private extension PokitRootView {
: .default(.secondary),
size: .small,
shape: .round,
- action: { send(.filterButtonTapped(.포킷)) }
+ action: { send(.필터_버튼_눌렀을때(.포킷)) }
)
PokitIconLButton(
@@ -89,7 +90,7 @@ private extension PokitRootView {
: .default(.secondary),
size: .small,
shape: .round,
- action: { send(.filterButtonTapped(.미분류)) }
+ action: { send(.필터_버튼_눌렀을때(.미분류)) }
)
Spacer()
@@ -98,7 +99,7 @@ private extension PokitRootView {
store.sortType == .sort(.최신순) ?
"최신순" : store.folderType == .folder(.포킷) ? "이름순" : "오래된순",
icon: .icon(.align),
- action: { send(.sortButtonTapped) }
+ action: { send(.분류_버튼_눌렀을때) }
)
.contentTransition(.numericText())
}
@@ -149,15 +150,15 @@ private extension PokitRootView {
ForEach(categories, id: \.id) { item in
PokitCard(
category: item,
- action: { send(.categoryTapped(item)) },
- kebabAction: { send(.kebobButtonTapped(item)) }
+ action: { send(.카테고리_눌렀을때(item)) },
+ kebabAction: { send(.케밥_버튼_눌렀을때(item)) }
)
}
}
if store.hasNext {
PokitLoading()
- .onAppear { send(.다음페이지_로딩_presented) }
+ .onAppear { send(.페이지_로딩중일때) }
}
}
.padding(.bottom, 150)
@@ -166,8 +167,8 @@ private extension PokitRootView {
var unclassifiedView: some View {
Group {
- if let unclassifiedContents = store.unclassifiedContents {
- if unclassifiedContents.isEmpty {
+ if !store.isLoading {
+ if store.contents.isEmpty {
VStack {
PokitCaution(
image: .empty,
@@ -179,7 +180,7 @@ private extension PokitRootView {
Spacer()
}
} else {
- unclassifiedList(unclassifiedContents)
+ unclassifiedList
}
} else {
PokitLoading()
@@ -187,25 +188,25 @@ private extension PokitRootView {
}
}
- @ViewBuilder
- func unclassifiedList(_ unclassifiedContents: IdentifiedArrayOf) -> some View {
+ var unclassifiedList: some View {
ScrollView {
LazyVStack(spacing: 0) {
- ForEach(unclassifiedContents) { content in
- let isFirst = content == unclassifiedContents.first
- let isLast = content == unclassifiedContents.last
-
- PokitLinkCard(
- link: content,
- action: { send(.contentItemTapped(content)) },
- kebabAction: { send(.unclassifiedKebobButtonTapped(content)) }
+ ForEach(
+ 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,
+ isFirst: isFirst,
+ isLast: isLast
)
- .divider(isFirst: isFirst, isLast: isLast)
}
if store.unclassifiedHasNext {
PokitLoading()
- .onAppear(perform: { send(.다음페이지_로딩_presented) })
+ .onAppear(perform: { send(.페이지_로딩중일때) })
}
}
.padding(.bottom, 150)
diff --git a/Projects/Feature/FeaturePokitTests/Resources/info.plist b/Projects/Feature/FeaturePokitTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeaturePokitTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift b/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift
new file mode 100644
index 00000000..6ddbeaaa
--- /dev/null
+++ b/Projects/Feature/FeaturePokitTests/Sources/FeaturePokitTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeaturePokit
+
+final class FeaturePokitTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift b/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
index 72f34f6f..24766a92 100644
--- a/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
+++ b/Projects/Feature/FeatureRemind/Sources/Remind/RemindFeature.swift
@@ -4,6 +4,8 @@
//
// Created by 김도형 on 7/12/24.
+import SwiftUI
+
import ComposableArchitecture
import Domain
import CoreKit
@@ -15,11 +17,12 @@ public struct RemindFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.remindClient)
+ @Dependency(RemindClient.self)
private var remindClient
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
-
+ @Dependency(SwiftSoupClient.self)
+ private var swiftSoupClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -65,35 +68,46 @@ public struct RemindFeature {
public enum View: Equatable, BindableAction {
case binding(BindingAction)
- /// - Button Tapped
- case bellButtonTapped
- case searchButtonTapped
- case linkCardTapped(content: BaseContentItem)
- case kebabButtonTapped(content: BaseContentItem)
- case unreadNavigationLinkTapped
- case favoriteNavigationLinkTapped
- case bottomSheetButtonTapped(
+ case bottomSheet(
delegate: PokitBottomSheet.Delegate,
content: BaseContentItem
)
- case deleteAlertConfirmTapped(content: BaseContentItem)
+
+ /// - Button Tapped
+ case 알림_버튼_눌렀을때
+ case 검색_버튼_눌렀을때
+ case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
+ case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
+ case 안읽음_목록_버튼_눌렀을때
+ case 즐겨찾기_목록_버튼_눌렀을때
+ case 링크_삭제_눌렀을때(content: BaseContentItem)
case 링크_공유_완료
- case remindViewOnAppeared
+ case 뷰가_나타났을때
+ case 즐겨찾기_항목_이미지_조회(contentId: Int)
+ case 읽지않음_항목_이미지_조회(contentId: Int)
+ case 리마인드_항목_이미지오류_나타났을때(contentId: Int)
}
public enum InnerAction: Equatable {
- case dismissBottomSheet
- case 오늘의_리마인드_조회(contents: [BaseContentItem])
- case 읽지않음_컨텐츠_조회(contentList: BaseContentListInquiry)
- case 즐겨찾기_링크모음_조회(contentList: BaseContentListInquiry)
- case 컨텐츠_삭제_반영(id: Int)
+ 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 오늘의_리마인드_조회
- case 읽지않음_컨텐츠_조회
- case 즐겨찾기_링크모음_조회
- case 컨텐츠_삭제(id: Int)
+ case 오늘의_리마인드_조회_API
+ case 읽지않음_컨텐츠_조회_API
+ case 즐겨찾기_링크모음_조회_API
+ case 컨텐츠_삭제_API(id: Int)
+ case 즐겨찾기_이미지_조회_수행(contentId: Int)
+ case 읽지않음_이미지_조회_수행(contentId: Int)
+ case 리마인드_이미지_조회_수행(contentId: Int)
}
public enum ScopeAction: Equatable {
case bottomSheet(
@@ -108,7 +122,7 @@ public struct RemindFeature {
case 링크수정(id: Int)
case 링크목록_안읽음
case 링크목록_즐겨찾기
- case 컨텐츠목록_조회
+ case 컨텐츠_상세보기_delegate_위임
}
}
/// initiallizer
@@ -144,74 +158,92 @@ private extension RemindFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- case .bellButtonTapped:
- return .run { send in await send(.delegate(.alertButtonTapped)) }
- case .searchButtonTapped:
- return .run { send in await send(.delegate(.searchButtonTapped)) }
- case .favoriteNavigationLinkTapped:
+ 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 .검색_버튼_눌렀을때:
+ return .send(.delegate(.searchButtonTapped))
+ case .즐겨찾기_목록_버튼_눌렀을때:
return .send(.delegate(.링크목록_즐겨찾기))
- case .unreadNavigationLinkTapped:
+ case .안읽음_목록_버튼_눌렀을때:
return .send(.delegate(.링크목록_안읽음))
- case .kebabButtonTapped(let content):
+ case .컨텐츠_항목_케밥_버튼_눌렀을때(let content):
state.bottomSheetItem = content
return .none
- case .linkCardTapped(let content):
+ case .컨텐츠_항목_눌렀을때(let content):
return .send(.delegate(.링크상세(content: content)))
- case .bottomSheetButtonTapped(let delegate, let content):
- return .run { send in
- await send(.inner(.dismissBottomSheet))
- await send(.scope(.bottomSheet(delegate: delegate, content: content)))
- }
- case .deleteAlertConfirmTapped:
+ case .링크_삭제_눌렀을때:
guard let id = state.alertItem?.id else { return .none }
- return .run { [id] send in
- await send(.async(.컨텐츠_삭제(id: id)))
- }
- case .binding:
- return .none
- case .remindViewOnAppeared:
- return .run { send in
- await send(.async(.오늘의_리마인드_조회), animation: .pokitDissolve)
- await send(.async(.읽지않음_컨텐츠_조회), animation: .pokitDissolve)
- await send(.async(.즐겨찾기_링크모음_조회), animation: .pokitDissolve)
- }
+ return .send(.async(.컨텐츠_삭제_API(id: id)))
+ case .뷰가_나타났을때:
+ return allContentFetch(animation: .pokitDissolve)
case .링크_공유_완료:
state.shareSheetItem = nil
return .none
+ case let .즐겨찾기_항목_이미지_조회(contentId):
+ return .send(.async(.즐겨찾기_이미지_조회_수행(contentId: contentId)))
+ case let .읽지않음_항목_이미지_조회(contentId):
+ return .send(.async(.읽지않음_이미지_조회_수행(contentId: contentId)))
+ case let .리마인드_항목_이미지오류_나타났을때(contentId):
+ return .send(.async(.리마인드_이미지_조회_수행(contentId: contentId)))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .dismissBottomSheet:
+ case .바텀시트_해제:
state.bottomSheetItem = nil
return .none
- case .오늘의_리마인드_조회(contents: let contents):
+ case .오늘의_리마인드_조회_API_반영(contents: let contents):
state.domain.recommendedList = contents
return .none
- case .읽지않음_컨텐츠_조회(contentList: let contentList):
+ case .읽지않음_컨텐츠_조회_API_반영(contentList: let contentList):
state.domain.unreadList = contentList
return .none
- case .즐겨찾기_링크모음_조회(contentList: let contentList):
+ case .즐겨찾기_링크모음_조회_API_반영(contentList: let contentList):
state.domain.favoriteList = contentList
return .none
- case .컨텐츠_삭제_반영(id: let contentId):
+ 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
+ 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
+ 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
}
}
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .오늘의_리마인드_조회:
+ case .오늘의_리마인드_조회_API:
return .run { send in
let contents = try await remindClient.오늘의_리마인드_조회().map { $0.toDomain() }
- await send(.inner(.오늘의_리마인드_조회(contents: contents)), animation: .pokitDissolve)
+ await send(.inner(.오늘의_리마인드_조회_API_반영(contents: contents)), animation: .pokitDissolve)
}
- case .읽지않음_컨텐츠_조회:
+ case .읽지않음_컨텐츠_조회_API:
return .run { [pageable = state.domain.unreadListPageable] send in
let contentList = try await remindClient.읽지않음_컨텐츠_조회(
BasePageableRequest(
@@ -220,9 +252,9 @@ private extension RemindFeature {
sort: pageable.sort
)
).toDomain()
- await send(.inner(.읽지않음_컨텐츠_조회(contentList: contentList)), animation: .pokitDissolve)
+ await send(.inner(.읽지않음_컨텐츠_조회_API_반영(contentList: contentList)), animation: .pokitDissolve)
}
- case .즐겨찾기_링크모음_조회:
+ case .즐겨찾기_링크모음_조회_API:
return .run { [pageable = state.domain.favoriteListPageable] send in
let contentList = try await remindClient.즐겨찾기_링크모음_조회(
BasePageableRequest(
@@ -231,12 +263,58 @@ private extension RemindFeature {
sort: pageable.sort
)
).toDomain()
- await send(.inner(.즐겨찾기_링크모음_조회(contentList: contentList)), animation: .pokitDissolve)
+ await send(.inner(.즐겨찾기_링크모음_조회_API_반영(contentList: contentList)), animation: .pokitDissolve)
}
- case .컨텐츠_삭제(id: let id):
- return .run { [id] send in
+ case .컨텐츠_삭제_API(id: let id):
+ return .run { send in
let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring)
+ 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
+ }
+
+ let imageURL = try await swiftSoupClient.parseOGImageURL(url)
+ guard let imageURL else { return }
+
+ await send(.inner(.즐겨찾기_이미지_조회_수행_반영(
+ imageURL: imageURL,
+ index: index
+ )))
+ }
+ 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
+ }
+ let imageURL = try await swiftSoupClient.parseOGImageURL(url)
+ guard let imageURL else { return }
+
+ await send(.inner(.읽지않음_이미지_조회_수행_반영(
+ imageURL: imageURL,
+ index: index
+ )))
+ }
+ 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
+ }
+ let imageURL = try await swiftSoupClient.parseOGImageURL(url)
+ guard let imageURL else { return }
+
+ await send(.inner(.리마인드_이미지_조회_수행_반영(
+ imageURL: imageURL,
+ index: index
+ )))
}
}
}
@@ -262,13 +340,17 @@ private extension RemindFeature {
/// - Delegate Effect
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
switch action {
- case .컨텐츠목록_조회:
- return .run { send in
- await send(.async(.오늘의_리마인드_조회))
- await send(.async(.읽지않음_컨텐츠_조회))
- await send(.async(.즐겨찾기_링크모음_조회))
- }
+ case .컨텐츠_상세보기_delegate_위임:
+ return allContentFetch()
default: return .none
}
}
+
+ func allContentFetch(animation: Animation? = nil) -> Effect {
+ return .run { send in
+ await send(.async(.오늘의_리마인드_조회_API), animation: animation)
+ await send(.async(.읽지않음_컨텐츠_조회_API), animation: animation)
+ await send(.async(.즐겨찾기_링크모음_조회_API), animation: animation)
+ }
+ }
}
diff --git a/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift b/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
index 6f78327d..f4630aa5 100644
--- a/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
+++ b/Projects/Feature/FeatureRemind/Sources/Remind/RemindView.swift
@@ -35,7 +35,7 @@ public extension RemindView {
PokitBottomSheet(
items: [.share, .edit, .delete],
height: 224
- ) { send(.bottomSheetButtonTapped(delegate: $0, content: content)) }
+ ) { send(.bottomSheet(delegate: $0, content: content)) }
}
.sheet(item: $store.shareSheetItem) { content in
if let shareURL = URL(string: content.data) {
@@ -51,9 +51,9 @@ public extension RemindView {
"링크를 정말 삭제하시겠습니까?",
message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
confirmText: "삭제"
- ) { send(.deleteAlertConfirmTapped(content: content)) }
+ ) { send(.링크_삭제_눌렀을때(content: content)) }
}
- .task { await send(.remindViewOnAppeared, animation: .pokitDissolve).finish() }
+ .task { await send(.뷰가_나타났을때, animation: .pokitDissolve).finish() }
}
}
}
@@ -134,28 +134,18 @@ extension RemindView {
@ViewBuilder
private func recommendedContentCell(content: BaseContentItem) -> some View {
- Button(action: { send(.linkCardTapped(content: content)) }) {
+ Button(action: { send(.컨텐츠_항목_눌렀을때(content: content)) }) {
recommendedContentCellLabel(content: content)
}
-
}
@ViewBuilder
private func recommendedContentCellLabel(content: BaseContentItem) -> some View {
ZStack(alignment: .bottom) {
- LazyImage(url: .init(string: content.thumbNail)) { phase in
- if let image = phase.image {
- image
- .resizable()
- } else {
- ZStack {
- Color.pokit(.bg(.disable))
-
- PokitSpinner()
- .foregroundStyle(.pokit(.icon(.brand)))
- .frame(width: 48, height: 48)
- }
- }
+ if let url = URL(string: content.thumbNail) {
+ recommendedContentCellImage(url: url, contentId: content.id)
+ } else {
+ imagePlaceholder
}
LinearGradient(
@@ -186,7 +176,7 @@ extension RemindView {
Spacer()
kebabButton {
- send(.kebabButtonTapped(content: content))
+ send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content))
}
.foregroundStyle(.pokit(.icon(.inverseWh)))
.zIndex(1)
@@ -203,6 +193,33 @@ extension RemindView {
}
.frame(width: 216, height: 194)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
+ .clipped()
+ }
+
+ @MainActor
+ private func recommendedContentCellImage(url: URL, contentId: Int) -> some View {
+ LazyImage(url: url) { phase in
+ if let image = phase.image {
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ } else if phase.error != nil {
+ imagePlaceholder
+ .task { await send(.리마인드_항목_이미지오류_나타났을때(contentId: contentId)).finish() }
+ } else {
+ imagePlaceholder
+ }
+ }
+ }
+
+ private var imagePlaceholder: some View {
+ ZStack {
+ Color.pokit(.bg(.disable))
+
+ PokitSpinner()
+ .foregroundStyle(.pink)
+ .frame(width: 48, height: 48)
+ }
}
@ViewBuilder
@@ -243,7 +260,7 @@ extension RemindView {
VStack(spacing: 0) {
VStack(spacing: 0) {
listNavigationLink("한번도 읽지 않았어요") {
- send(.unreadNavigationLinkTapped)
+ send(.안읽음_목록_버튼_눌렀을때)
}
.padding(.bottom, 16)
}
@@ -254,8 +271,9 @@ extension RemindView {
PokitLinkCard(
link: content,
- action: { send(.linkCardTapped(content: content)) },
- kebabAction: { send(.kebabButtonTapped(content: content)) }
+ action: { send(.컨텐츠_항목_눌렀을때(content: content)) },
+ kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content)) },
+ fetchMetaData: { send(.읽지않음_항목_이미지_조회(contentId: content.id)) }
)
.divider(isFirst: isFirst, isLast: isLast)
}
@@ -270,7 +288,7 @@ extension RemindView {
) -> some View {
VStack(spacing: 0) {
listNavigationLink("즐겨찾기 링크만 모았어요") {
- send(.favoriteNavigationLinkTapped)
+ send(.즐겨찾기_목록_버튼_눌렀을때)
}
.padding(.bottom, 16)
@@ -288,8 +306,9 @@ extension RemindView {
PokitLinkCard(
link: content,
- action: { send(.linkCardTapped(content: content)) },
- kebabAction: { send(.kebabButtonTapped(content: content)) }
+ action: { send(.컨텐츠_항목_눌렀을때(content: content)) },
+ kebabAction: { send(.컨텐츠_항목_케밥_버튼_눌렀을때(content: content)) },
+ fetchMetaData: { send(.즐겨찾기_항목_이미지_조회(contentId: content.id)) }
)
.divider(isFirst: isFirst, isLast: isLast)
}
diff --git a/Projects/Feature/FeatureRemindTests/Resources/info.plist b/Projects/Feature/FeatureRemindTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureRemindTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureRemindTests/Sources/FeatureRemindTests.swift b/Projects/Feature/FeatureRemindTests/Sources/FeatureRemindTests.swift
new file mode 100644
index 00000000..3690595d
--- /dev/null
+++ b/Projects/Feature/FeatureRemindTests/Sources/FeatureRemindTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureRemind
+
+final class FeatureRemindTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
index 232aa4a3..ff647e64 100644
--- a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxFeature.swift
@@ -14,20 +14,21 @@ import CoreKit
@Reducer
public struct PokitAlertBoxFeature {
/// - Dependency
- @Dependency(\.dismiss) var dismiss
- @Dependency(\.pasteboard) var pasteboard
- @Dependency(\.alertClient) var alertClient
+ @Dependency(\.dismiss)
+ var dismiss
+ @Dependency(PasteboardClient.self)
+ var pasteboard
+ @Dependency(AlertClient.self)
+ var alertClient
/// - State
@ObservableState
public struct State: Equatable {
public init() {}
-
+
fileprivate var domain = Alert()
var alertContents: IdentifiedArrayOf? {
- guard let list = domain.alertList.data else {
- return nil
- }
+ guard let list = domain.alertList.data else { return nil }
var identifiedArray = IdentifiedArrayOf()
list.forEach { identifiedArray.append($0) }
return identifiedArray
@@ -44,22 +45,27 @@ public struct PokitAlertBoxFeature {
@CasePathable
public enum View: Equatable {
- case deleteSwiped(item: AlertItem)
- case itemSelected(item: AlertItem)
case dismiss
- case onAppear
case pagenation
+ case 밀어서_삭제했을때(item: AlertItem)
+ case 알람_항목_선택했을때(item: AlertItem)
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
- case onAppearResult(AlertListInquiry)
- case pagenation_result(AlertListInquiry)
- case 삭제결과(item: AlertItem)
+ case pagenation_알람_목록_조회_API_반영(AlertListInquiry)
+ case 뷰가_나타났을때_알람_목록_조회_API_반영(AlertListInquiry)
+ case 알람_삭제_API_반영(item: AlertItem)
}
- public enum AsyncAction: Equatable { case doNothing }
+ public enum AsyncAction: Equatable {
+ case pagenation_알람_목록_조회_API
+ case 뷰가_나타났을때_알람_목록_조회_API
+ case 알람_삭제_API(item: AlertItem)
+ case 클립보드_감지
+ }
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case moveToContentEdit(id: Int)
@@ -70,8 +76,6 @@ public struct PokitAlertBoxFeature {
/// - Initiallizer
public init() {}
-
- public enum CancelID { case disAppear }
/// - Reducer Core
private func core(into state: inout State, action: Action) -> Effect {
@@ -79,19 +83,15 @@ public struct PokitAlertBoxFeature {
/// - View
case .view(let viewAction):
return handleViewAction(viewAction, state: &state)
-
/// - Inner
case .inner(let innerAction):
return handleInnerAction(innerAction, state: &state)
-
/// - Async
case .async(let asyncAction):
return handleAsyncAction(asyncAction, state: &state)
-
/// - Scope
case .scope(let scopeAction):
return handleScopeAction(scopeAction, state: &state)
-
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
@@ -108,57 +108,35 @@ private extension PokitAlertBoxFeature {
/// - View Effect
func handleViewAction(_ action: Action.View, state: inout State) -> Effect {
switch action {
- /// - 스와이프를 통해 아이템 삭제를 눌렀을 때
- case .deleteSwiped(let item):
- return .run { send in
- try await alertClient.알람_삭제("\(item.id)")
- await send(.inner(.삭제결과(item: item)))
- }
- /// - 선택한 항목을 `링크수정`화면으로 이동해 수정
- case .itemSelected(let item):
- return .run { send in await send(.delegate(.moveToContentEdit(id: item.contentId))) }
case .dismiss:
- return .run { send in
-// await dismiss()
- await send(.delegate(.alertBoxDismiss))
- }
- case .onAppear:
- return .run { [domain = state.domain.alertList] send in
- let sort: [String] = ["createdAt", "desc"]
- let request = BasePageableRequest(page: 0, size: domain.size, sort: sort)
- let result = try await alertClient.알람_목록_조회(request).toDomain()
- await send(.inner(.onAppearResult(result)))
-
- for await _ in self.pasteboard.changes() {
- let url = try await pasteboard.probableWebURL()
- await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
- }
- }
+ return .send(.delegate(.alertBoxDismiss))
+
+ case let .밀어서_삭제했을때(item):
+ return .send(.async(.알람_삭제_API(item: item)))
+
+ case let .알람_항목_선택했을때(item):
+ return .send(.delegate(.moveToContentEdit(id: item.contentId)))
+
+ case .뷰가_나타났을때:
+ return .merge(
+ .send(.async(.뷰가_나타났을때_알람_목록_조회_API)),
+ .send(.async(.클립보드_감지))
+ )
+
case .pagenation:
- if state.domain.alertList.hasNext {
- return .run { [domain = state.domain.alertList] send in
- let sort: [String] = ["createdAt", "desc"]
- let request = BasePageableRequest(
- page: domain.page + 1,
- size: 10,
- sort: sort
- )
- let result = try await alertClient.알람_목록_조회(request).toDomain()
- await send(.inner(.pagenation_result(result)))
- }
- }
- return .none
+ return state.domain.alertList.hasNext
+ ? .send(.async(.pagenation_알람_목록_조회_API))
+ : .none
}
}
-
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case let .onAppearResult(list):
+ case let .뷰가_나타났을때_알람_목록_조회_API_반영(list):
state.domain.alertList = list
return .none
- case let .pagenation_result(alertList):
+ case let .pagenation_알람_목록_조회_API_반영(alertList):
guard var list = state.domain.alertList.data else { return .none }
guard let newList = alertList.data else { return .none }
@@ -167,24 +145,54 @@ private extension PokitAlertBoxFeature {
state.domain.alertList.data = list
return .none
- case let .삭제결과(item):
- //TODO: 삭제연결
+ case let .알람_삭제_API_반영(item):
guard let idx = state.domain.alertList.data?.firstIndex(where: { $0 == item }) else { return .none }
state.domain.alertList.data?.remove(at: idx)
return .none
}
}
-
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
- return .none
+ switch action {
+ case .pagenation_알람_목록_조회_API:
+ return .run { [domain = state.domain.alertList] send in
+ let sort: [String] = ["createdAt", "desc"]
+ let request = BasePageableRequest(
+ page: domain.page + 1,
+ size: 10,
+ sort: sort
+ )
+ let result = try await alertClient.알람_목록_조회(request).toDomain()
+ await send(.inner(.pagenation_알람_목록_조회_API_반영(result)))
+ }
+
+ case .뷰가_나타났을때_알람_목록_조회_API:
+ return .run { [domain = state.domain.alertList] send in
+ let sort: [String] = ["createdAt", "desc"]
+ let request = BasePageableRequest(page: 0, size: domain.size, sort: sort)
+ let result = try await alertClient.알람_목록_조회(request).toDomain()
+ await send(.inner(.뷰가_나타났을때_알람_목록_조회_API_반영(result)))
+ }
+
+ case let .알람_삭제_API(item):
+ return .run { send in
+ try await alertClient.알람_삭제("\(item.id)")
+ await send(.inner(.알람_삭제_API_반영(item: item)))
+ }
+
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteboard.changes() {
+ let url = try await pasteboard.probableWebURL()
+ await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
+ }
+ }
+ }
}
-
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
return .none
}
-
/// - Delegate Effect
func handleDelegateAction(_ action: Action.DelegateAction, state: inout State) -> Effect {
return .none
diff --git a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift
index 505d2a96..c7fff957 100644
--- a/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Alert/PokitAlertBoxView.swift
@@ -31,7 +31,7 @@ public extension PokitAlertBoxView {
VStack {
PokitCaution(
image: .pooki,
- titleKey: "알람이 없어요",
+ titleKey: "알림이 없어요",
message: "리마인드 알림을 설정하세요"
)
.padding(.top, 84)
@@ -40,7 +40,7 @@ public extension PokitAlertBoxView {
} else {
List {
ForEach(alertContents, id: \.id) { item in
- Button(action: { send(.itemSelected(item: item)) }) {
+ Button(action: { send(.알람_항목_선택했을때(item: item)) }) {
AlertContent(item: item)
}
.listRowSeparator(.hidden)
@@ -58,7 +58,7 @@ public extension PokitAlertBoxView {
}
.pokitNavigationBar { navigationBar }
.ignoresSafeArea(edges: .bottom)
- .task { await send(.onAppear).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
@@ -76,7 +76,7 @@ private extension PokitAlertBoxView {
}
func delete(_ item: AlertItem) {
- send(.deleteSwiped(item: item),animation: .pokitSpring)
+ send(.밀어서_삭제했을때(item: item),animation: .pokitSpring)
}
struct AlertContent: View {
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
index c66315be..9664bf83 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchFeature.swift
@@ -7,6 +7,7 @@
import Foundation
import ComposableArchitecture
+import FeatureContentCard
import Domain
import CoreKit
import DSKit
@@ -19,11 +20,11 @@ public struct PokitSearchFeature {
private var dismiss
@Dependency(\.mainQueue)
private var mainQueue
- @Dependency(\.pasteboard)
+ @Dependency(PasteboardClient.self)
private var pasteboard
- @Dependency(\.userDefaults)
+ @Dependency(UserDefaultsClient.self)
private var userDefaults
- @Dependency(\.contentClient)
+ @Dependency(ContentClient.self)
private var contentClient
/// - State
@ObservableState
@@ -45,14 +46,7 @@ public struct PokitSearchFeature {
get { domain.condition.searchWord }
set { domain.condition.searchWord = newValue }
}
- var resultList: IdentifiedArrayOf? {
- guard let contentList = domain.contentList.data else {
- return nil
- }
- var identifiedArray = IdentifiedArrayOf()
- contentList.forEach { identifiedArray.append($0) }
- return identifiedArray
- }
+ var contents: IdentifiedArrayOf = []
var favoriteFilter: Bool {
get { domain.condition.favorites }
set { domain.condition.favorites = newValue }
@@ -70,16 +64,14 @@ public struct PokitSearchFeature {
set { domain.condition.endDate = newValue }
}
var startDateString: String? {
- guard let startDate = domain.condition.startDate else {
- return nil
- }
+ guard let startDate = domain.condition.startDate else { return nil}
let formatter = DateFormat.searchCondition.formatter
-
return formatter.string(from: startDate)
}
var hasNext: Bool {
get { domain.contentList.hasNext }
}
+ var isLoading: Bool = false
/// sheet item
var bottomSheetItem: BaseContentItem? = nil
@@ -95,75 +87,72 @@ public struct PokitSearchFeature {
case scope(ScopeAction)
case delegate(DelegateAction)
case fiterBottomSheet(PresentationAction)
+ case contents(IdentifiedActionOf)
@CasePathable
public enum View: Equatable, BindableAction {
- /// - Binding
case binding(BindingAction)
- /// - Button Tapped
- case autoSaveButtonTapped
- case searchTextInputIconTapped
- case searchTextChipButtonTapped(text: String)
- case filterButtonTapped
- case contentTypeFilterButtonTapped
- case favoriteChipTapped
- case unreadChipTapped
- case dateFilterButtonTapped
- case categoryFilterButtonTapped
- case categoryFilterChipTapped(category: BaseCategoryItem)
- case recentSearchAllRemoveButtonTapped
- case recentSearchChipIconTapped(searchText: String)
- case linkCardTapped(content: BaseContentItem)
- case kebabButtonTapped(content: BaseContentItem)
- case bottomSheetButtonTapped(
+ case dismiss
+ case bottomSheet(
delegate: PokitBottomSheet.Delegate,
content: BaseContentItem
)
- case deleteAlertConfirmTapped
- case sortTextLinkTapped
- case backButtonTapped
- /// - TextInput OnSubmitted
- case searchTextInputOnSubmitted
-
- case 링크_공유_완료
-
- case onAppear
- case 로딩_isPresented
+ case 자동저장_버튼_눌렀을때
+ case 검색_버튼_눌렀을때
+ case 최근검색_태그_눌렀을때(text: String)
+ case 최근검색_태그_삭제_눌렀을때(searchText: String)
+ case 필터링_버튼_눌렀을때
+ case 카테고리_버튼_눌렀을때
+ case 모아보기_버튼_눌렀을때
+ case 기간_버튼_눌렀을때
+ case 카테고리_태그_눌렀을때(category: BaseCategoryItem)
+ case 즐겨찾기_태그_눌렀을때
+ case 안읽음_태그_눌렀을때
+ case 전체_삭제_버튼_눌렀을때
+ case 컨텐츠_항목_눌렀을때(content: BaseContentItem)
+ case 컨텐츠_항목_케밥_버튼_눌렀을때(content: BaseContentItem)
+ case 정렬_버튼_눌렀을때
+ case 검색_키보드_엔터_눌렀을때
+ case 링크_삭제_눌렀을때
+ case 링크_공유_완료되었을때
+ case 뷰가_나타났을때
+ case 로딩중일때
}
public enum InnerAction: Equatable {
- case enableIsSearching
- case disableIsSearching
- case updateDateFilter(startDate: Date?, endDate: Date?)
- case showFilterBottomSheet(filterType: FilterBottomFeature.FilterType)
- case updateContentTypeFilter(favoriteFilter: Bool, unreadFilter: Bool)
- case dismissBottomSheet
- case updateIsFiltered
- case updateCategoryIds
- case 컨텐츠_목록_갱신(BaseContentListInquiry)
+ case filterBottomSheet(filterType: FilterBottomFeature.FilterType)
+ case 검색창_활성화(Bool)
+ case 기간_업데이트(startDate: Date?, endDate: Date?)
+ case 모아보기_업데이트(favoriteFilter: Bool, unreadFilter: Bool)
+ case 필터_업데이트
+ case 카테고리_ID_목록_업데이트
+ case 바텀시트_해제
+ case 컨텐츠_검색_API_반영(BaseContentListInquiry)
+ case 컨텐츠_검색_페이징_API_반영(BaseContentListInquiry)
+ case 컨텐츠_삭제_API_반영(id: Int)
case 최근검색어_불러오기
- case 자동저장_켜기_불러오기
+ case 자동저장_불러오기
case 최근검색어_추가
- case 컨텐츠_삭제_반영(id: Int)
- case 컨텐츠_검색_결과_페이징_갱신(BaseContentListInquiry)
case 페이징_초기화
}
public enum AsyncAction: Equatable {
- case 컨텐츠_검색
- case 최근검색어_갱신
- case 자동저장_켜기_갱신
- case 컨텐츠_삭제(id: Int)
- case 컨텐츠_검색_결과_페이징_조회
+ case 컨텐츠_검색_API
+ case 최근검색어_갱신_수행
+ case 자동저장_수행
+ case 컨텐츠_삭제_API(id: Int)
+ case 컨텐츠_검색_페이징_API
+ case 클립보드_감지
}
- public enum ScopeAction: Equatable {
+ public enum ScopeAction {
case filterBottomSheet(FilterBottomFeature.Action.DelegateAction)
case bottomSheet(
delegate: PokitBottomSheet.Delegate,
content: BaseContentItem
)
+ case contents(IdentifiedActionOf)
}
public enum DelegateAction: Equatable {
@@ -183,26 +172,27 @@ public struct PokitSearchFeature {
/// - View
case .view(let viewAction):
return handleViewAction(viewAction, state: &state)
-
/// - Inner
case .inner(let innerAction):
return handleInnerAction(innerAction, state: &state)
-
/// - Async
case .async(let asyncAction):
return handleAsyncAction(asyncAction, state: &state)
-
/// - Scope
case .scope(let scopeAction):
return handleScopeAction(scopeAction, state: &state)
-
/// - Delegate
case .delegate(let delegateAction):
return handleDelegateAction(delegateAction, state: &state)
+
case .fiterBottomSheet(.presented(.delegate(let delegate))):
return .send(.scope(.filterBottomSheet(delegate)))
+
case .fiterBottomSheet:
return .none
+
+ case .contents(let contentsAction):
+ return .send(.scope(.contents(contentsAction)))
}
}
public enum CancelID { case response }
@@ -210,6 +200,9 @@ public struct PokitSearchFeature {
public var body: some ReducerOf {
BindingReducer(action: \.view)
Reduce(self.core)
+ .forEach(\.contents, action: \.contents) {
+ ContentCardFeature()
+ }
.ifLet(\.$filterBottomSheet, action: \.fiterBottomSheet) {
FilterBottomFeature()
}
@@ -223,152 +216,167 @@ private extension PokitSearchFeature {
switch action {
case .binding(\.searchText):
guard !state.searchText.isEmpty else {
- /// 🚨 Error Case [1]: 빈 문자열 일 때
- return .send(.inner(.disableIsSearching))
+ /// 🚨 Error Case: 빈 문자열 일 때
+ return .send(.inner(.검색창_활성화(false)))
}
return .none
+
case .binding:
return .none
- case .autoSaveButtonTapped:
+
+ case .자동저장_버튼_눌렀을때:
state.isAutoSaveSearch.toggle()
- return .send(.async(.자동저장_켜기_갱신))
- case .searchTextInputOnSubmitted:
+ return .send(.async(.자동저장_수행))
+
+ case .검색_키보드_엔터_눌렀을때:
return .run { send in
await send(.inner(.최근검색어_추가))
await send(.inner(.페이징_초기화), animation: .pokitDissolve)
}
- case .searchTextInputIconTapped:
+
+ case .검색_버튼_눌렀을때:
/// - 검색 중일 경우 `문자열 지우기 버튼 동작`
if state.isSearching {
state.domain.condition.searchWord = ""
- return .send(.inner(.disableIsSearching))
+ return .send(.inner(.검색창_활성화(false)))
} else {
return .run { send in
await send(.inner(.최근검색어_추가))
await send(.inner(.페이징_초기화), animation: .pokitDissolve)
}
}
- case .searchTextChipButtonTapped(text: let text):
+
+ case let .최근검색_태그_눌렀을때(text):
state.searchText = text
return .send(.inner(.페이징_초기화), animation: .pokitDissolve)
- case .filterButtonTapped:
- return .send(.inner(.showFilterBottomSheet(filterType: .pokit)))
- case .contentTypeFilterButtonTapped:
- return .send(.inner(.showFilterBottomSheet(filterType: .contentType)))
- case .dateFilterButtonTapped:
+
+ case .필터링_버튼_눌렀을때:
+ return .send(.inner(.filterBottomSheet(filterType: .pokit)))
+
+ case .모아보기_버튼_눌렀을때:
+ return .send(.inner(.filterBottomSheet(filterType: .contentType)))
+
+ case .기간_버튼_눌렀을때:
guard state.domain.condition.startDate != nil && state.domain.condition.endDate != nil else {
/// - 선택된 기간이 없을 경우
- return .send(.inner(.showFilterBottomSheet(filterType: .date)))
+ return .send(.inner(.filterBottomSheet(filterType: .date)))
}
state.domain.condition.startDate = nil
state.domain.condition.endDate = nil
return .run { send in
- await send(.inner(.updateDateFilter(startDate: nil, endDate: nil)))
+ await send(.inner(.기간_업데이트(startDate: nil, endDate: nil)))
await send(.inner(.페이징_초기화), animation: .pokitDissolve)
}
- case .categoryFilterButtonTapped:
- return .send(.inner(.showFilterBottomSheet(filterType: .pokit)))
- case .recentSearchAllRemoveButtonTapped:
+
+ case .카테고리_버튼_눌렀을때:
+ return .send(.inner(.filterBottomSheet(filterType: .pokit)))
+
+ case .전체_삭제_버튼_눌렀을때:
state.recentSearchTexts.removeAll()
- return .send(.async(.최근검색어_갱신))
- case .recentSearchChipIconTapped(searchText: let searchText):
+ return .send(.async(.최근검색어_갱신_수행))
+
+ case let .최근검색_태그_삭제_눌렀을때(searchText):
guard let predicate = state.recentSearchTexts.firstIndex(of: searchText) else {
return .none
}
state.recentSearchTexts.remove(at: predicate)
- return .send(.async(.최근검색어_갱신))
- case .linkCardTapped(content: let content):
+ return .send(.async(.최근검색어_갱신_수행))
+
+ case let .컨텐츠_항목_눌렀을때(content):
return .send(.delegate(.linkCardTapped(content: content)))
- case .kebabButtonTapped(content: let content):
+
+ case let .컨텐츠_항목_케밥_버튼_눌렀을때(content):
state.bottomSheetItem = content
return .none
- case .bottomSheetButtonTapped(delegate: let delegate, content: let content):
+
+ case .bottomSheet(delegate: let delegate, content: let content):
return .run { send in
- await send(.inner(.dismissBottomSheet))
+ await send(.inner(.바텀시트_해제))
await send(.scope(.bottomSheet(delegate: delegate, content: content)))
}
- case .deleteAlertConfirmTapped:
+
+ case .링크_삭제_눌렀을때:
guard let id = state.alertItem?.id else { return .none }
state.alertItem = nil
- return .run { [id] send in
- await send(.async(.컨텐츠_삭제(id: id)))
- }
- case .sortTextLinkTapped:
+ return .send(.async(.컨텐츠_삭제_API(id: id)))
+
+ case .정렬_버튼_눌렀을때:
state.isResultAscending.toggle()
state.domain.pageable.sort = [
state.isResultAscending ? "createdAt,asc" : "createdAt,desc"
]
return .send(.inner(.페이징_초기화))
- case .backButtonTapped:
- return .run { _ in
- await dismiss()
- }
- case .onAppear:
- return .run { [
- contentList = state.domain.contentList.data
- ] send in
- async let _ = send(.inner(.자동저장_켜기_불러오기))
- async let _ = send(.inner(.최근검색어_불러오기))
- if let contentList, !contentList.isEmpty {
- async let _ = send(.async(.컨텐츠_검색))
- }
- for await _ in self.pasteboard.changes() {
- let url = try await pasteboard.probableWebURL()
- await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
- }
+ case .dismiss:
+ return .run { _ in await dismiss() }
+
+ case .뷰가_나타났을때:
+ let contentList = state.domain.contentList.data
+
+ var effectBox: [Effect] = [
+ .send(.inner(.자동저장_불러오기)),
+ .send(.inner(.최근검색어_불러오기)),
+ .send(.async(.클립보드_감지))
+ ]
+
+ if let contentList, !contentList.isEmpty {
+ effectBox.append(.send(.async(.컨텐츠_검색_API)))
}
- case .categoryFilterChipTapped(category: let category):
+
+ return .merge(effectBox)
+
+ case let .카테고리_태그_눌렀을때(category):
state.categoryFilter.remove(category)
return .run { send in
- await send(.inner(.updateCategoryIds))
+ await send(.inner(.카테고리_ID_목록_업데이트))
await send(.inner(.페이징_초기화))
}
- case .favoriteChipTapped:
+
+ case .즐겨찾기_태그_눌렀을때:
state.domain.condition.favorites = false
return .send(.inner(.페이징_초기화))
- case .unreadChipTapped:
+
+ case .안읽음_태그_눌렀을때:
state.domain.condition.isRead = false
return .send(.inner(.페이징_초기화))
- case .링크_공유_완료:
+
+ case .링크_공유_완료되었을때:
state.shareSheetItem = nil
return .none
- case .로딩_isPresented:
- return .send(.async(.컨텐츠_검색_결과_페이징_조회))
+ case .로딩중일때:
+ return .send(.async(.컨텐츠_검색_페이징_API))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .enableIsSearching:
- state.isSearching = true
- return .none
- case .disableIsSearching:
- state.isSearching = false
- state.domain.contentList.data = []
+ case let .검색창_활성화(isActive):
+ if isActive {
+ state.isSearching = true
+ } else {
+ state.isSearching = false
+ state.domain.contentList.data = []
+ }
return .none
- case .updateDateFilter(startDate: let startDate, endDate: let endDate):
+ case let .기간_업데이트(startDate, endDate):
let formatter = DateFormat.dateFilter.formatter
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
}
-
- if startDate == endDate {
- /// - 날짜 필터를 하루만 선택했을 경우
- state.dateFilterText = "\(formatter.string(from: startDate))"
- } else {
- state.dateFilterText = "\(formatter.string(from: startDate))~\(formatter.string(from: endDate))"
- }
-
+ state.dateFilterText = startDate == endDate
+ ? "\(formatter.string(from: startDate))" /// - 날짜 필터를 하루만 선택했을 경우
+ : "\(formatter.string(from: startDate))~\(formatter.string(from: endDate))"
return .none
- case .showFilterBottomSheet(filterType: let filterType):
+
+ case let .filterBottomSheet(filterType):
state.filterBottomSheet = .init(
filterType: filterType,
pokitFilter: state.categoryFilter,
@@ -378,113 +386,91 @@ private extension PokitSearchFeature {
endDateFilter: state.endDateFilter
)
return .none
- case .updateContentTypeFilter(favoriteFilter: let favoriteFilter, unreadFilter: let unreadFilter):
+
+ case let .모아보기_업데이트(favoriteFilter, unreadFilter):
state.domain.condition.favorites = favoriteFilter
state.domain.condition.isRead = unreadFilter
return .none
- case .dismissBottomSheet:
+
+ case .바텀시트_해제:
state.bottomSheetItem = nil
return .none
- case .updateIsFiltered:
- state.isFiltered = !state.categoryFilter.isEmpty ||
- state.favoriteFilter ||
- state.unreadFilter ||
- state.startDateFilter != nil ||
- state.endDateFilter != nil
+
+ case .필터_업데이트:
+ state.isFiltered = !state.categoryFilter.isEmpty
+ || state.favoriteFilter
+ || state.unreadFilter
+ || state.startDateFilter != nil
+ || state.endDateFilter != nil
return .none
- case .updateCategoryIds:
+
+ case .카테고리_ID_목록_업데이트:
state.domain.condition.categoryIds = state.categoryFilter.map { $0.id }
return .none
- case .컨텐츠_목록_갱신(let contentList):
+
+ case .컨텐츠_검색_API_반영(let contentList):
state.domain.contentList = contentList
- return .send(.inner(.enableIsSearching))
+
+ var contents = IdentifiedArrayOf()
+ contentList.data?.forEach { contents.append(.init(content: $0)) }
+ state.contents = contents
+ state.isLoading = false
+ return .send(.inner(.검색창_활성화(true)))
+
case .최근검색어_불러오기:
- guard state.isAutoSaveSearch else {
- return .none
- }
+ guard state.isAutoSaveSearch else { return .none }
state.recentSearchTexts = userDefaults.stringArrayKey(.searchWords) ?? []
return .none
- case .자동저장_켜기_불러오기:
+
+ case .자동저장_불러오기:
state.isAutoSaveSearch = userDefaults.boolKey(.autoSaveSearch)
return .none
case .최근검색어_추가:
- guard state.isAutoSaveSearch else { return .none }
- guard !state.domain.condition.searchWord.isEmpty else { return .none }
- if !state.recentSearchTexts.contains(state.domain.condition.searchWord) {
- state.recentSearchTexts.append(state.domain.condition.searchWord)
+ let searchWord = state.domain.condition.searchWord
+ guard state.isAutoSaveSearch && !searchWord.isEmpty else {
+ /// 🚨 Error Case: 검색어 자동저장이 `off`거나, 검색 키워드가 없다면 종료
+ return .none
}
- return .send(.async(.최근검색어_갱신))
- case .컨텐츠_삭제_반영(id: let id):
+
+ if !state.recentSearchTexts.contains(searchWord) {
+ state.recentSearchTexts.append(searchWord)
+ }
+ 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 .컨텐츠_검색_결과_페이징_갱신(contentList):
+
+ case let .컨텐츠_검색_페이징_API_반영(contentList):
let list = state.domain.contentList.data ?? []
guard let newList = contentList.data else { return .none }
state.domain.contentList = contentList
state.domain.contentList.data = list + newList
- return .send(.inner(.enableIsSearching))
+
+ newList.forEach { state.contents.append(.init(content: $0)) }
+
+ return .send(.inner(.검색창_활성화(true)))
+
case .페이징_초기화:
state.domain.pageable.page = 0
state.domain.contentList.data = nil
- return .send(.async(.컨텐츠_검색), animation: .pokitDissolve)
+ state.isLoading = true
+ state.contents.removeAll()
+ return .send(.async(.컨텐츠_검색_API), animation: .pokitDissolve)
}
}
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .컨텐츠_검색:
- let formatter = DateFormat.yearMonthDate.formatter
+ case .컨텐츠_검색_API:
+ return contentSearch(state: &state)
- var startDateString: String? = nil
- var endDateString: String? = nil
- if let startDate = state.domain.condition.startDate {
- startDateString = formatter.string(from: startDate)
- }
- if let endDate = state.domain.condition.endDate {
- endDateString = formatter.string(from: endDate)
- }
- return .run { [
- pageable = state.domain.pageable,
- condition = BaseConditionRequest(
- searchWord: state.domain.condition.searchWord,
- categoryIds: state.domain.condition.categoryIds,
- isRead: state.domain.condition.isRead,
- favorites: state.domain.condition.favorites,
- startDate: startDateString,
- endDate: endDateString
- )
- ] send in
- let stream = AsyncThrowingStream { continuation in
- Task {
- for page in 0...pageable.page {
- let contentList = try await contentClient.컨텐츠_검색(
- BasePageableRequest(
- page: page,
- size: pageable.size,
- sort: pageable.sort
- ),
- condition
- ).toDomain()
- continuation.yield(contentList)
- }
- continuation.finish()
- }
- }
- var contentItems: BaseContentListInquiry? = nil
- for try await contentList in stream {
- let items = contentItems?.data ?? []
- let newItems = contentList.data ?? []
- contentItems = contentList
- contentItems?.data = items + newItems
- }
- guard let contentItems else { return }
- await send(.inner(.컨텐츠_목록_갱신(contentItems)), animation: .pokitSpring)
- }
- case .최근검색어_갱신:
+ case .최근검색어_갱신_수행:
guard state.isAutoSaveSearch else { return .none }
return .run { [ searchWords = state.recentSearchTexts ] _ in
await userDefaults.setStringArray(
@@ -492,18 +478,19 @@ private extension PokitSearchFeature {
.searchWords
)
}
- case .자동저장_켜기_갱신:
- return .run { [
- isAutoSaveSearch = state.isAutoSaveSearch
- ] send in
+
+ case .자동저장_수행:
+ return .run { [isAutoSaveSearch = state.isAutoSaveSearch] _ in
await userDefaults.setBool(isAutoSaveSearch, .autoSaveSearch)
}
- case .컨텐츠_삭제(id: let id):
- return .run { [id] send in
+
+ case let .컨텐츠_삭제_API(id):
+ return .run { send in
let _ = try await contentClient.컨텐츠_삭제("\(id)")
- await send(.inner(.컨텐츠_삭제_반영(id: id)), animation: .pokitSpring)
+ await send(.inner(.컨텐츠_삭제_API_반영(id: id)), animation: .pokitSpring)
}
- case .컨텐츠_검색_결과_페이징_조회:
+
+ case .컨텐츠_검색_페이징_API:
state.domain.pageable.page += 1
let formatter = DateFormat.yearMonthDate.formatter
@@ -521,30 +508,44 @@ private extension PokitSearchFeature {
startDateString,
endDateString
] send in
+ let pageableRequest = BasePageableRequest(
+ page: pageable.page,
+ size: pageable.size,
+ sort: pageable.sort
+ )
+
+ let conditionRequest = BaseConditionRequest(
+ searchWord: condition.searchWord,
+ categoryIds: condition.categoryIds,
+ isRead: condition.isRead,
+ favorites: condition.favorites,
+ startDate: startDateString,
+ endDate: endDateString
+ )
+
let contentList = try await contentClient.컨텐츠_검색(
- BasePageableRequest(
- page: pageable.page,
- size: pageable.size,
- sort: pageable.sort
- ),
- BaseConditionRequest(
- searchWord: condition.searchWord,
- categoryIds: condition.categoryIds,
- isRead: condition.isRead,
- favorites: condition.favorites,
- startDate: startDateString,
- endDate: endDateString
- )
+ pageableRequest,
+ conditionRequest
).toDomain()
- await send(.inner(.컨텐츠_검색_결과_페이징_갱신(contentList)))
+
+ await send(.inner(.컨텐츠_검색_페이징_API_반영(contentList)))
+ }
+
+ case .클립보드_감지:
+ return .run { send in
+ for await _ in self.pasteboard.changes() {
+ let url = try await pasteboard.probableWebURL()
+ await send(.delegate(.linkCopyDetected(url)), animation: .pokitSpring)
+ }
}
}
}
+
/// - Scope Effect
func handleScopeAction(_ action: Action.ScopeAction, state: inout State) -> Effect {
switch action {
- case .filterBottomSheet(.searchButtonTapped(
+ case .filterBottomSheet(.검색_버튼_눌렀을때(
categories: let categories,
isFavorite: let isFavorite,
isUnread: let isUnread,
@@ -552,12 +553,13 @@ private extension PokitSearchFeature {
endDate: let endDate)):
state.categoryFilter = categories
return .run { send in
- await send(.inner(.updateCategoryIds))
- await send(.inner(.updateContentTypeFilter(favoriteFilter: isFavorite, unreadFilter: isUnread)))
- await send(.inner(.updateDateFilter(startDate: startDate, endDate: endDate)))
- await send(.inner(.updateIsFiltered))
+ await send(.inner(.카테고리_ID_목록_업데이트))
+ await send(.inner(.모아보기_업데이트(favoriteFilter: isFavorite, unreadFilter: isUnread)))
+ await send(.inner(.기간_업데이트(startDate: startDate, endDate: endDate)))
+ await send(.inner(.필터_업데이트))
await send(.inner(.페이징_초기화))
}
+
case .bottomSheet(let delegate, let content):
switch delegate {
case .deleteCellButtonTapped:
@@ -571,6 +573,14 @@ private extension PokitSearchFeature {
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
+ case .contents:
+ return .none
}
}
@@ -581,8 +591,55 @@ private extension PokitSearchFeature {
guard let contentList = state.domain.contentList.data, !contentList.isEmpty else {
return .none
}
- return .send(.async(.컨텐츠_검색), animation: .pokitSpring)
+ return .send(.async(.컨텐츠_검색_API), animation: .pokitSpring)
default: return .none
}
}
+
+ func contentSearch(state: inout State) -> Effect {
+ let formatter = DateFormat.yearMonthDate.formatter
+
+ var startDateString: String? = nil
+ var endDateString: String? = nil
+ if let startDate = state.domain.condition.startDate {
+ startDateString = formatter.string(from: startDate)
+ }
+ if let endDate = state.domain.condition.endDate {
+ endDateString = formatter.string(from: endDate)
+ }
+ let condition = BaseConditionRequest(
+ searchWord: state.domain.condition.searchWord,
+ categoryIds: state.domain.condition.categoryIds,
+ isRead: state.domain.condition.isRead,
+ favorites: state.domain.condition.favorites,
+ startDate: startDateString,
+ endDate: endDateString
+ )
+
+ return .run { [pageable = state.domain.pageable, condition] send in
+ let stream = AsyncThrowingStream { continuation in
+ Task {
+ for page in 0...pageable.page {
+ let request = BasePageableRequest(
+ page: page,
+ size: pageable.size,
+ sort: pageable.sort
+ )
+ let contentList = try await contentClient.컨텐츠_검색(request, condition).toDomain()
+ continuation.yield(contentList)
+ }
+ continuation.finish()
+ }
+ }
+ var contentItems: BaseContentListInquiry? = nil
+ for try await contentList in stream {
+ let items = contentItems?.data ?? []
+ let newItems = contentList.data ?? []
+ contentItems = contentList
+ contentItems?.data = items + newItems
+ }
+ guard let contentItems else { return }
+ await send(.inner(.컨텐츠_검색_API_반영(contentItems)), animation: .pokitSpring)
+ }
+ }
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
index 8a1cfc22..e0938d6e 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/PokitSearchView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import ComposableArchitecture
+import FeatureContentCard
import DSKit
@ViewAction(for: PokitSearchFeature.self)
@@ -54,13 +55,13 @@ public extension PokitSearchView {
PokitBottomSheet(
items: [.share, .edit, .delete],
height: 224
- ) { send(.bottomSheetButtonTapped(delegate: $0, content: content)) }
+ ) { send(.bottomSheet(delegate: $0, content: content)) }
}
.sheet(item: $store.shareSheetItem) { content in
if let shareURL = URL(string: content.data) {
PokitShareSheet(
items: [shareURL],
- completion: { send(.링크_공유_완료) }
+ completion: { send(.링크_공유_완료되었을때) }
)
.presentationDetents([.medium, .large])
}
@@ -70,9 +71,9 @@ public extension PokitSearchView {
"링크를 정말 삭제하시겠습니까?",
message: "함께 저장한 모든 정보가 삭제되며, \n복구하실 수 없습니다.",
confirmText: "삭제"
- ) { send(.deleteAlertConfirmTapped) }
+ ) { send(.링크_삭제_눌렀을때) }
}
- .task { await send(.onAppear).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
@@ -82,7 +83,7 @@ private extension PokitSearchView {
HStack(spacing: 8) {
PokitToolbarButton(
.icon(.arrowLeft),
- action: { send(.backButtonTapped) }
+ action: { send(.dismiss) }
)
PokitIconRInput(
@@ -91,8 +92,8 @@ private extension PokitSearchView {
shape: .round,
focusState: $focused,
equals: true,
- onSubmit: { send(.searchTextInputOnSubmitted) },
- iconTappedAction: store.isSearching ? { send(.searchTextInputIconTapped) } : nil
+ onSubmit: { send(.검색_키보드_엔터_눌렀을때) },
+ iconTappedAction: store.isSearching ? { send(.검색_버튼_눌렀을때) } : nil
)
}
.padding(.vertical, 8)
@@ -110,7 +111,7 @@ private extension PokitSearchView {
PokitTextLink(
"전체 삭제",
color: .text(.tertiary),
- action: { send(.recentSearchAllRemoveButtonTapped) }
+ action: { send(.전체_삭제_버튼_눌렀을때) }
)
Text("|")
@@ -120,7 +121,7 @@ private extension PokitSearchView {
PokitTextLink(
"자동저장 \(store.isAutoSaveSearch ? "끄기" : "켜기")",
color: .text(.tertiary),
- action: { send(.autoSaveButtonTapped, animation: .pokitSpring) }
+ action: { send(.자동저장_버튼_눌렀을때, animation: .pokitSpring) }
)
.contentTransition(.numericText())
}
@@ -162,10 +163,10 @@ private extension PokitSearchView {
state: .default(.primary),
size: .small,
action: {
- send(.searchTextChipButtonTapped(text: text), animation: .pokitSpring)
+ send(.최근검색_태그_눌렀을때(text: text), animation: .pokitSpring)
},
iconTappedAction: {
- send(.recentSearchChipIconTapped(searchText: text), animation: .pokitSpring)
+ send(.최근검색_태그_삭제_눌렀을때(searchText: text), animation: .pokitSpring)
}
)
.pokitScrollTransition(.opacity)
@@ -207,7 +208,7 @@ private extension PokitSearchView {
state: store.isFiltered ? .filled(.primary) : .stroke(.secondary),
size: .small,
shape: .round,
- action: { send(.filterButtonTapped) }
+ action: { send(.필터링_버튼_눌렀을때) }
)
}
@@ -219,7 +220,7 @@ private extension PokitSearchView {
icon: .icon(.arrowDown),
state: .default(.primary),
size: .small,
- action: { send(.categoryFilterButtonTapped) }
+ action: { send(.카테고리_버튼_눌렀을때) }
)
} else {
ForEach(store.categoryFilter) { category in
@@ -227,7 +228,7 @@ private extension PokitSearchView {
category.categoryName,
state: .stroke(.primary),
size: .small,
- action: { send(.categoryFilterChipTapped(category: category), animation: .pokitSpring) }
+ action: { send(.카테고리_태그_눌렀을때(category: category), animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -243,7 +244,7 @@ private extension PokitSearchView {
icon: .icon(.arrowDown),
state: .default(.primary),
size: .small,
- action: { send(.contentTypeFilterButtonTapped) }
+ action: { send(.모아보기_버튼_눌렀을때) }
)
} else {
if store.favoriteFilter {
@@ -251,7 +252,7 @@ private extension PokitSearchView {
"즐겨찾기",
state: .stroke(.primary),
size: .small,
- action: { send(.favoriteChipTapped, animation: .pokitSpring) }
+ action: { send(.즐겨찾기_태그_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -261,7 +262,7 @@ private extension PokitSearchView {
"안읽음",
state: .stroke(.primary),
size: .small,
- action: { send(.unreadChipTapped, animation: .pokitSpring) }
+ action: { send(.안읽음_태그_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -275,7 +276,7 @@ private extension PokitSearchView {
icon: store.dateFilterText == "기간" ? .icon(.arrowDown) : .icon(.x),
state: store.dateFilterText == "기간" ? .default(.primary) : .stroke(.primary),
size: .small,
- action: { send(.dateFilterButtonTapped, animation: .pokitSpring) }
+ action: { send(.기간_버튼_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -285,29 +286,30 @@ private extension PokitSearchView {
PokitIconLTextLink(
store.isResultAscending ? "오래된순" : "최신순",
icon: .icon(.align),
- action: { send(.sortTextLinkTapped) }
+ action: { send(.정렬_버튼_눌렀을때) }
)
.contentTransition(.numericText())
.padding(.horizontal, 20)
- if let results = store.resultList {
+ if !store.isLoading {
ScrollView {
LazyVStack(spacing: 0) {
- ForEach(results, id: \.id) { content in
- let isFirst = content == results.first
- let isLast = content == results.last
+ ForEach(
+ 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
- PokitLinkCard(
- link: content,
- action: { send(.linkCardTapped(content: content)) },
- kebabAction: { send(.kebabButtonTapped(content: content)) }
+ ContentCardView(
+ store: store,
+ isFirst: isFirst,
+ isLast: isLast
)
- .divider(isFirst: isFirst, isLast: isLast)
}
if store.hasNext {
PokitLoading()
- .task { await send(.로딩_isPresented, animation: .pokitDissolve).finish() }
+ .task { await send(.로딩중일때, animation: .pokitDissolve).finish() }
}
}
.padding(.horizontal, 20)
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift
index 51da4855..95a2d5c3 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomFeature.swift
@@ -16,7 +16,7 @@ public struct FilterBottomFeature {
/// - Dependency
@Dependency(\.dismiss)
private var dismiss
- @Dependency(\.categoryClient)
+ @Dependency(CategoryClient.self)
private var categoryClient
/// - State
@ObservableState
@@ -73,31 +73,29 @@ public struct FilterBottomFeature {
public enum View: Equatable, BindableAction {
/// - Binding
case binding(BindingAction)
- /// - Button Tapped
- case pokitListCellTapped(pokit: BaseCategoryItem)
- case searchButtonTapped
- case pokitChipTapped(BaseCategoryItem)
- case favoriteChipTapped
- case unreadChipTapped
- case dateChipTapped
- case favoriteButtonTapped
- case unreadButtonTapped
-
- case pokitListOnAppeared
+ case 포킷_항목_눌렀을때(pokit: BaseCategoryItem)
+ case 검색하기_버튼_눌렀을때
+ case 포킷_태그_눌렀을때(BaseCategoryItem)
+ case 즐겨찾기_태그_눌렀을때
+ case 안읽음_태그_눌렀을때
+ case 기간_태그_눌렀을때
+ case 즐겨찾기_체크박스_눌렀을때
+ case 안읽음_체크박스_눌렀을때
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
- case 카테고리_목록_갱신(categoryList: BaseCategoryListInquiry)
+ case 카테고리_목록_조회_API_반영(categoryList: BaseCategoryListInquiry)
}
public enum AsyncAction: Equatable {
- case 카테고리_목록_조회
+ case 카테고리_목록_조회_API
}
public enum ScopeAction: Equatable { case doNothing }
public enum DelegateAction: Equatable {
- case searchButtonTapped(
+ case 검색_버튼_눌렀을때(
categories: IdentifiedArrayOf,
isFavorite: Bool,
isUnread: Bool,
@@ -149,15 +147,19 @@ private extension FilterBottomFeature {
case .binding(\.startDate):
state.dateSelected = true
return .none
+
case .binding(\.endDate):
state.dateSelected = true
return .none
+
case .binding:
return .none
- case .pokitListCellTapped(let pokit):
+
+ case .포킷_항목_눌렀을때(let pokit):
state.selectedCategories.append(pokit)
return .none
- case .searchButtonTapped:
+
+ case .검색하기_버튼_눌렀을때:
return .run { [
categories = state.selectedCategories,
isFavorite = state.isFavorite,
@@ -166,7 +168,7 @@ private extension FilterBottomFeature {
endDate = state.endDate,
dateSelected = state.dateSelected
] send in
- await send(.delegate(.searchButtonTapped(
+ await send(.delegate(.검색_버튼_눌렀을때(
categories: categories,
isFavorite: isFavorite,
isUnread: isUnread,
@@ -175,35 +177,42 @@ private extension FilterBottomFeature {
)))
await dismiss()
}
- case .pokitChipTapped(let category):
+
+ case .포킷_태그_눌렀을때(let category):
state.selectedCategories.remove(category)
return .none
- case .favoriteChipTapped:
+
+ case .즐겨찾기_태그_눌렀을때:
state.isFavorite = false
return .none
- case .unreadChipTapped:
+
+ case .안읽음_태그_눌렀을때:
state.isUnread = false
return .none
- case .dateChipTapped:
+
+ case .기간_태그_눌렀을때:
state.startDate = .now
state.endDate = .now
state.dateSelected = false
return .none
- case .favoriteButtonTapped:
+
+ case .즐겨찾기_체크박스_눌렀을때:
state.isFavorite.toggle()
return .none
- case .unreadButtonTapped:
+
+ case .안읽음_체크박스_눌렀을때:
state.isUnread.toggle()
return .none
- case .pokitListOnAppeared:
- return .send(.async(.카테고리_목록_조회))
+
+ case .뷰가_나타났을때:
+ return .send(.async(.카테고리_목록_조회_API))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .카테고리_목록_갱신(categoryList: let categoryList):
+ case let .카테고리_목록_조회_API_반영(categoryList):
state.domain.categoryList = categoryList
return .none
}
@@ -212,7 +221,7 @@ private extension FilterBottomFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .카테고리_목록_조회:
+ case .카테고리_목록_조회_API:
return .run { [pageable = state.domain.pageable] send in
let categoryList = try await categoryClient.카테고리_목록_조회(
BasePageableRequest(
@@ -222,7 +231,7 @@ private extension FilterBottomFeature {
),
true
).toDomain()
- await send(.inner(.카테고리_목록_갱신(categoryList: categoryList)), animation: .pokitDissolve)
+ await send(.inner(.카테고리_목록_조회_API_반영(categoryList: categoryList)), animation: .pokitDissolve)
}
}
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift
index cc263553..724d1d08 100644
--- a/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Search/Sheet/FilterBottomSheet.swift
@@ -35,7 +35,7 @@ public extension FilterBottomSheet {
switch store.currentType {
case .pokit:
pokitList
- .onAppear { send(.pokitListOnAppeared) }
+ .onAppear { send(.뷰가_나타났을때) }
case .contentType:
contentTypes
case .date:
@@ -53,7 +53,7 @@ public extension FilterBottomSheet {
PokitBottomButton(
"검색하기",
state: .filled(.primary),
- action: { send(.searchButtonTapped, animation: .pokitSpring) }
+ action: { send(.검색하기_버튼_눌렀을때, animation: .pokitSpring) }
)
.padding(.horizontal, 20)
}
@@ -100,7 +100,7 @@ private extension FilterBottomSheet {
PokitList(
selectedItem: nil,
list: pokitList,
- action: { send(.pokitListCellTapped(pokit: $0), animation: .pokitSpring) }
+ action: { send(.포킷_항목_눌렀을때(pokit: $0), animation: .pokitSpring) }
)
} else {
PokitLoading()
@@ -113,13 +113,13 @@ private extension FilterBottomSheet {
contentTypeButton(
"즐겨찾기",
isSelected: $store.isFavorite,
- action: { send(.favoriteButtonTapped, animation: .pokitSpring) }
+ action: { send(.즐겨찾기_체크박스_눌렀을때, animation: .pokitSpring) }
)
contentTypeButton(
"안읽음",
isSelected: $store.isUnread,
- action: { send(.unreadButtonTapped, animation: .pokitSpring) }
+ action: { send(.안읽음_체크박스_눌렀을때, animation: .pokitSpring) }
)
Spacer()
@@ -160,7 +160,7 @@ private extension FilterBottomSheet {
category.categoryName,
state: .stroke(.primary),
size: .small,
- action: { send(.pokitChipTapped(category), animation: .pokitSpring) }
+ action: { send(.포킷_태그_눌렀을때(category), animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -170,7 +170,7 @@ private extension FilterBottomSheet {
"즐겨찾기",
state: .stroke(.primary),
size: .small,
- action: { send(.favoriteChipTapped, animation: .pokitSpring) }
+ action: { send(.즐겨찾기_태그_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -180,7 +180,7 @@ private extension FilterBottomSheet {
"안읽음",
state: .stroke(.primary),
size: .small,
- action: { send(.unreadChipTapped, animation: .pokitSpring) }
+ action: { send(.안읽음_태그_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
}
@@ -191,7 +191,7 @@ private extension FilterBottomSheet {
sameDate ? "\(store.startDateText)" : "\(store.startDateText)~\(store.endDateText)",
state: .stroke(.primary),
size: .small,
- action: { send(.dateChipTapped, animation: .pokitSpring) }
+ action: { send(.기간_태그_눌렀을때, animation: .pokitSpring) }
)
.pokitBlurReplaceTransition(.pokitDissolve)
.contentTransition(.numericText())
@@ -222,7 +222,7 @@ private extension FilterBottomSheet {
startDateFilter: nil,
endDateFilter: nil
),
- reducer: { FilterBottomFeature() }
+ reducer: { FilterBottomFeature()._printChanges() }
)
)
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
index f08f9808..d0810b0b 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingFeature.swift
@@ -14,8 +14,8 @@ import Util
public struct NickNameSettingFeature {
/// - Dependency
@Dependency(\.dismiss) var dismiss
- @Dependency(\.userClient) var userClient
@Dependency(\.mainQueue) var mainQueue
+ @Dependency(UserClient.self) var userClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -46,25 +46,24 @@ public struct NickNameSettingFeature {
public enum View: BindableAction, Equatable {
case binding(BindingAction)
case dismiss
- case saveButtonTapped
-
- case onAppear
+ case 저장_버튼_눌렀을때
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
- case textChanged
- case 닉네임_중복_체크_네트워크_결과(Bool)
- case 유저정보_갱신(BaseUser)
+ case 닉네임_텍스트_변경되었을때
+ case 닉네임_중복_확인_API_반영(Bool)
+ case 닉네임_조회_API_반영(BaseUser)
}
public enum AsyncAction: Equatable {
- case 닉네임_중복_체크_네트워크
- case 닉네임_조회
+ case 닉네임_중복_확인_API
+ case 닉네임_조회_API
}
- public enum ScopeAction: Equatable { case doNothing }
+ public enum ScopeAction: Equatable { case 없음 }
- public enum DelegateAction: Equatable { case doNothing }
+ public enum DelegateAction: Equatable { case 없음 }
}
/// - Initiallizer
@@ -109,34 +108,36 @@ private extension NickNameSettingFeature {
case .binding(\.text):
state.buttonState = .disable
return .run { send in
- await send(.inner(.textChanged))
+ await send(.inner(.닉네임_텍스트_변경되었을때))
}
.debounce(
id: CancelID.response,
for: 0.5,
scheduler: mainQueue
)
+
case .binding:
return .none
case .dismiss:
return .run { _ in await dismiss() }
- case .saveButtonTapped:
+ case .저장_버튼_눌렀을때:
return .run { [nickName = state.text] send in
let request = NicknameEditRequest(nickname: nickName)
let _ = try await userClient.닉네임_수정(request)
await dismiss()
}
- case .onAppear:
- return .send(.async(.닉네임_조회))
+
+ case .뷰가_나타났을때:
+ return .send(.async(.닉네임_조회_API))
}
}
/// - Inner Effect
func handleInnerAction(_ action: Action.InnerAction, state: inout State) -> Effect {
switch action {
- case .textChanged:
+ case .닉네임_텍스트_변경되었을때:
/// [1]. 닉네임 텍스트필드가 비어있을 때
if state.text.isEmpty {
state.buttonState = .disable
@@ -155,18 +156,20 @@ private extension NickNameSettingFeature {
return .none
} else {
/// [4]. 정상 케이스일 때
- return .run { send in await send(.async(.닉네임_중복_체크_네트워크)) }
- }
- case let .닉네임_중복_체크_네트워크_결과(isDuplicate):
- if isDuplicate {
- state.textfieldState = .error(message: "중복된 닉네임입니다.")
- state.buttonState = .disable
- } else {
- state.textfieldState = .active
- state.buttonState = .filled(.primary)
+ return .run { send in await send(.async(.닉네임_중복_확인_API)) }
}
+
+ case let .닉네임_중복_확인_API_반영(isDuplicate):
+ state.textfieldState = isDuplicate
+ ? .error(message: "중복된 닉네임입니다.")
+ : .active
+
+ state.buttonState = isDuplicate
+ ? .disable
+ : .filled(.primary)
return .none
- case let .유저정보_갱신(user):
+
+ case let .닉네임_조회_API_반영(user):
state.domain.user = user
state.domain.nickname = user.nickname
return .none
@@ -176,15 +179,16 @@ private extension NickNameSettingFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .닉네임_중복_체크_네트워크:
+ case .닉네임_중복_확인_API:
return .run { [nickName = state.text] send in
let result = try await userClient.닉네임_중복_체크(nickName)
- await send(.inner(.닉네임_중복_체크_네트워크_결과(result.isDuplicate)))
+ await send(.inner(.닉네임_중복_확인_API_반영(result.isDuplicate)))
}
- case .닉네임_조회:
+
+ case .닉네임_조회_API:
return .run { send in
let user = try await userClient.닉네임_조회().toDomain()
- await send(.inner(.유저정보_갱신(user)), animation: .easeInOut)
+ await send(.inner(.닉네임_조회_API_반영(user)), animation: .easeInOut)
}
}
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
index 69e92718..f22a4af0 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/NickNameSetting/NickNameSettingView.swift
@@ -44,7 +44,7 @@ public extension NickNameSettingView {
PokitBottomButton(
"저장",
state: store.buttonState,
- action: { send(.saveButtonTapped) }
+ action: { send(.저장_버튼_눌렀을때) }
)
.setKeyboardHeight()
}
@@ -52,7 +52,7 @@ public extension NickNameSettingView {
.padding(.top, 16)
.pokitNavigationBar { navigationBar }
.ignoresSafeArea(edges: .bottom)
- .task { await send(.onAppear).finish() }
+ .task { await send(.뷰가_나타났을때).finish() }
}
}
}
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingFeature.swift b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingFeature.swift
index 698db99e..f08dadbc 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingFeature.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingFeature.swift
@@ -13,13 +13,20 @@ import Util
@Reducer
public struct PokitSettingFeature {
/// - Dependency
- @Dependency(\.dismiss) var dismiss
- @Dependency(\.openSettings) var openSetting
- @Dependency(\.pasteboard) var pasteboard
- @Dependency(\.keychain) var keychain
- @Dependency(\.userDefaults) var userDefaults
- @Dependency(\.authClient) var authClient
- @Dependency(\.openURL) var openURL
+ @Dependency(\.dismiss)
+ var dismiss
+ @Dependency(\.openURL)
+ var openURL
+ @Dependency(\.openSettings)
+ var openSetting
+ @Dependency(PasteboardClient.self)
+ var pasteboard
+ @Dependency(KeychainClient.self)
+ var keychain
+ @Dependency(UserDefaultsClient.self)
+ var userDefaults
+ @Dependency(AuthClient.self)
+ var authClient
/// - State
@ObservableState
public struct State: Equatable {
@@ -43,18 +50,18 @@ public struct PokitSettingFeature {
@CasePathable
public enum View: BindableAction, Equatable {
case binding(BindingAction)
+ case dismiss
case 닉네임설정
case 알림설정
case 공지사항
case 서비스_이용약관
case 개인정보_처리방침
case 고객문의
- case 로그아웃
- case 로그아웃수행
- case 회원탈퇴
- case 회원탈퇴수행
- case dismiss
- case onAppear
+ case 로그아웃_버튼_눌렀을때
+ case 로그아웃_팝업_확인_눌렀을때
+ case 회원탈퇴_버튼_눌렀을때
+ case 회원탈퇴_팝업_확인_눌렀을때
+ case 뷰가_나타났을때
}
public enum InnerAction: Equatable {
@@ -63,13 +70,11 @@ public struct PokitSettingFeature {
}
public enum AsyncAction: Equatable {
- case 회원탈퇴_네트워크
- case 키_제거
+ case 회원탈퇴_API
+ case 키_제거_수행
}
- public enum ScopeAction: Equatable {
- case doNothing
- }
+ public enum ScopeAction: Equatable { case 없음 }
public enum DelegateAction: Equatable {
case linkCopyDetected(URL?)
@@ -126,6 +131,9 @@ private extension PokitSettingFeature {
case .binding:
return .none
+ case .dismiss:
+ return .run { _ in await dismiss() }
+
case .닉네임설정:
state.nickNameSettingState = NickNameSettingFeature.State()
return .none
@@ -153,30 +161,27 @@ private extension PokitSettingFeature {
state.isWebViewPresented = true
return .none
- case .로그아웃:
+ case .로그아웃_버튼_눌렀을때:
return .send(.inner(.로그아웃_팝업(isPresented: true)))
- case .로그아웃수행:
+ case .로그아웃_팝업_확인_눌렀을때:
return .run { send in
- await send(.async(.키_제거))
+ await send(.async(.키_제거_수행))
await send(.inner(.로그아웃_팝업(isPresented: false)))
await send(.delegate(.로그아웃))
}
- case .회원탈퇴:
+ case .회원탈퇴_버튼_눌렀을때:
return .send(.inner(.회원탈퇴_팝업(isPresented: true)))
- case .회원탈퇴수행:
+ case .회원탈퇴_팝업_확인_눌렀을때:
return .run { send in
- await send(.async(.회원탈퇴_네트워크))
+ await send(.async(.회원탈퇴_API))
await send(.inner(.회원탈퇴_팝업(isPresented: false)))
await send(.delegate(.회원탈퇴))
}
- case .dismiss:
- return .run { _ in await dismiss() }
-
- case .onAppear:
+ case .뷰가_나타났을때:
return .run { send in
for await _ in self.pasteboard.changes() {
let url = try await pasteboard.probableWebURL()
@@ -202,7 +207,7 @@ private extension PokitSettingFeature {
/// - Async Effect
func handleAsyncAction(_ action: Action.AsyncAction, state: inout State) -> Effect {
switch action {
- case .회원탈퇴_네트워크:
+ case .회원탈퇴_API:
return .run { send in
guard keychain.read(.refreshToken) != nil else {
print("refresh가 없어서 벗어남")
@@ -235,17 +240,14 @@ private extension PokitSettingFeature {
)
}
- await send(.async(.키_제거))
+ await send(.async(.키_제거_수행))
- let request = WithdrawRequest(
- authPlatform: platform
- )
+ let request = WithdrawRequest(authPlatform: platform)
- // - TODO: 서버 디비 삭제 요청으로 바꿔야함
try await authClient.회원탈퇴(request)
}
- case .키_제거:
+ case .키_제거_수행:
keychain.delete(.accessToken)
keychain.delete(.refreshToken)
keychain.delete(.serverRefresh)
diff --git a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift
index 87dde923..b092f054 100644
--- a/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift
+++ b/Projects/Feature/FeatureSetting/Sources/Setting/PokitSettingView.swift
@@ -37,7 +37,7 @@ public extension PokitSettingView {
PokitAlert(
"로그아웃 하시겠습니까?",
confirmText: "로그아웃",
- action: { send(.로그아웃수행) }
+ action: { send(.로그아웃_팝업_확인_눌렀을때) }
)
}
.sheet(isPresented: $store.isWithdrawPresented) {
@@ -45,7 +45,7 @@ public extension PokitSettingView {
"회원 탈퇴하시겠습니까?",
message: "함께 저장한 모든 정보가 삭제되며,\n복구하실 수 없습니다.",
confirmText: "탈퇴하기",
- action: { send(.회원탈퇴수행) }
+ action: { send(.회원탈퇴_팝업_확인_눌렀을때) }
)
}
.fullScreenCover(isPresented: $store.isWebViewPresented) {
@@ -60,7 +60,7 @@ public extension PokitSettingView {
) { store in
NickNameSettingView(store: store)
}
- .onAppear { send(.onAppear) }
+ .onAppear { send(.뷰가_나타났을때) }
}
}
}
@@ -114,12 +114,12 @@ private extension PokitSettingView {
Section {
SettingItem(
title: "로그아웃",
- action: { send(.로그아웃) }
+ action: { send(.로그아웃_버튼_눌렀을때) }
)
SettingItem(
title: "회원 탈퇴",
- action: { send(.회원탈퇴) }
+ action: { send(.회원탈퇴_버튼_눌렀을때) }
)
}
}
diff --git a/Projects/Feature/FeatureSettingTests/Resources/info.plist b/Projects/Feature/FeatureSettingTests/Resources/info.plist
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Projects/Feature/FeatureSettingTests/Resources/info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Projects/Feature/FeatureSettingTests/Sources/FeatureSettingTests.swift b/Projects/Feature/FeatureSettingTests/Sources/FeatureSettingTests.swift
new file mode 100644
index 00000000..32a3e41a
--- /dev/null
+++ b/Projects/Feature/FeatureSettingTests/Sources/FeatureSettingTests.swift
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import FeatureSetting
+
+final class FeatureSettingTests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Projects/Feature/Project.swift b/Projects/Feature/Project.swift
index c2b7bfa5..d59c1746 100644
--- a/Projects/Feature/Project.swift
+++ b/Projects/Feature/Project.swift
@@ -16,7 +16,11 @@ let demoTargets = Feature.allCases.map { feature in
return feature.demoTarget
}
+let testTargets = Feature.allCases.map { feature in
+ return feature.testTarget
+}
+
let project = Project(
name: "Feature",
- targets: targets + demoTargets
+ targets: targets + demoTargets + testTargets
)
diff --git a/Projects/SharedThirdPartyLib/Project.swift b/Projects/SharedThirdPartyLib/Project.swift
index ff80eec2..a362e892 100644
--- a/Projects/SharedThirdPartyLib/Project.swift
+++ b/Projects/SharedThirdPartyLib/Project.swift
@@ -24,7 +24,7 @@ let project = Project(
// TODO: 의존성 추가
.external(name: "ComposableArchitecture")
],
- settings: .settings
+ settings: .settings()
)
]
)
diff --git a/Projects/Util/Project.swift b/Projects/Util/Project.swift
index e5c44d37..30f6daf6 100644
--- a/Projects/Util/Project.swift
+++ b/Projects/Util/Project.swift
@@ -22,7 +22,8 @@ let project = Project(
sources: ["Sources/**"],
dependencies: [
// TODO: 의존성 추가
- ]
+ ],
+ settings: .settings()
)
]
)
diff --git a/Tuist/ProjectDescriptionHelpers/Feature.swift b/Tuist/ProjectDescriptionHelpers/Feature.swift
index 631cda83..4f541ff5 100644
--- a/Tuist/ProjectDescriptionHelpers/Feature.swift
+++ b/Tuist/ProjectDescriptionHelpers/Feature.swift
@@ -19,6 +19,7 @@ public enum Feature: String, CaseIterable {
case setting = "Setting"
case contentList = "ContentList"
case categorySharing = "CategorySharing"
+ case contentCard = "ContentCard"
public var target: Target {
return .makeTarget(
@@ -26,7 +27,7 @@ public enum Feature: String, CaseIterable {
product: TuistRelease.isRelease ? .staticFramework : .framework,
bundleName: "Feature.\(self.rawValue)",
infoPlist: .file(path: .relativeToRoot("Projects/App/Resources/Pokit-info.plist")),
- dependencies: [
+ dependencies: self.depenecies + [
.project(target: "DSKit", path: .relativeToRoot("Projects/DSKit")),
.project(target: "Domain", path: .relativeToRoot("Projects/Domain"))
]
@@ -45,4 +46,48 @@ public enum Feature: String, CaseIterable {
]
)
}
+
+ public var testTarget: Target {
+ return .makeTarget(
+ name: "Feature\(self.rawValue)Tests",
+ product: .unitTests,
+ bundleName: "Feature.\(self.rawValue)Tests",
+ infoPlist: .dictionary(["ENABLE_TESTING_SEARCH_PATHS": "YES"]),
+ resources: ["Feature\(self.rawValue)Tests/Resources/**"],
+ dependencies: [
+ .target(self.target)
+ ]
+ )
+ }
+
+ public var depenecies: [TargetDependency] {
+ switch self {
+ case .contentDetail: return []
+ case .contentSetting: return []
+ case .categorySetting: return []
+ case .remind: return []
+ case .login: return []
+ case .pokit:
+ return [
+ .project(target: "FeatureContentCard", path: .relativeToRoot("Projects/Feature"))
+ ]
+ case .categoryDetail:
+ return [
+ .project(target: "FeatureContentCard", path: .relativeToRoot("Projects/Feature"))
+ ]
+ case .setting:
+ return [
+ .project(target: "FeatureContentCard", path: .relativeToRoot("Projects/Feature"))
+ ]
+ case .contentList:
+ return [
+ .project(target: "FeatureContentCard", path: .relativeToRoot("Projects/Feature"))
+ ]
+ case .categorySharing:
+ return [
+ .project(target: "FeatureContentCard", path: .relativeToRoot("Projects/Feature"))
+ ]
+ case .contentCard: return []
+ }
+ }
}
diff --git a/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift b/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift
index d869536e..47a914d2 100644
--- a/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift
+++ b/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift
@@ -8,11 +8,21 @@
import ProjectDescription
public extension Settings {
- static var settings: Settings {
- return .settings(
+ static func settings(_ release: Configuration? = nil) -> Settings {
+ var settings: Settings = .settings(
base: [
"OTHER_LDFLAGS": "$(inherited) -ObjC",
+ "CODE_SIGN_STYLE": "Manual"
+ ],
+ configurations: [
+ .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfig/Debug.xcconfig"))
]
)
+
+ if let release {
+ settings.configurations.append(release)
+ }
+
+ return settings
}
}
diff --git a/Tuist/ProjectDescriptionHelpers/Target+Extension.swift b/Tuist/ProjectDescriptionHelpers/Target+Extension.swift
index fe2bcc3c..486c516f 100644
--- a/Tuist/ProjectDescriptionHelpers/Target+Extension.swift
+++ b/Tuist/ProjectDescriptionHelpers/Target+Extension.swift
@@ -26,7 +26,7 @@ public extension Target {
sources: ["\(name)/Sources/**"],
resources: resources,
dependencies: dependencies,
- settings: .settings
+ settings: .settings()
)
}
@@ -44,7 +44,7 @@ public extension Target {
deploymentTargets: .appMinimunTarget,
sources: ["Sources/\(name)/Sources/**"],
dependencies: dependencies,
- settings: .settings
+ settings: .settings()
)
}
}
diff --git a/Tuist/Templates/test/InfoPlist.stencil b/Tuist/Templates/test/InfoPlist.stencil
new file mode 100644
index 00000000..b31ce7b0
--- /dev/null
+++ b/Tuist/Templates/test/InfoPlist.stencil
@@ -0,0 +1,8 @@
+
+
+
+
+ ENABLE_TESTING_SEARCH_PATHS
+ YES
+
+
diff --git a/Tuist/Templates/test/Test.stencil b/Tuist/Templates/test/Test.stencil
new file mode 100644
index 00000000..c0c58ef3
--- /dev/null
+++ b/Tuist/Templates/test/Test.stencil
@@ -0,0 +1,10 @@
+import ComposableArchitecture
+import XCTest
+
+@testable import {{ target }}
+
+final class {{ target }}Tests: XCTestCase {
+ func test() {
+
+ }
+}
diff --git a/Tuist/Templates/test/test.swift b/Tuist/Templates/test/test.swift
new file mode 100644
index 00000000..ec183842
--- /dev/null
+++ b/Tuist/Templates/test/test.swift
@@ -0,0 +1,27 @@
+//
+// test.swift
+// ProjectDescriptionHelpers
+//
+// Created by 김민호 on 10/10/24.
+//
+
+import ProjectDescription
+
+let appProjectAttribute: Template.Attribute = .required("project")
+let appTargetAttribute: Template.Attribute = .required("target")
+
+let appTemplate = Template(
+ description: "Target template",
+ attributes: [
+ appProjectAttribute,
+ appTargetAttribute
+ ],
+ items: [
+ .file(
+ path: "Projects/\(appProjectAttribute)/\(appTargetAttribute)Tests/Sources/\(appTargetAttribute)Tests.swift",
+ templatePath: "Test.stencil"),
+ .file(
+ path: "Projects/\(appProjectAttribute)/\(appTargetAttribute)Tests/Resources/info.plist",
+ templatePath: "InfoPlist.stencil"),
+ ]
+)
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 6dcc5811..164214f1 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -16,45 +16,37 @@
default_platform(:ios)
platform :ios do
- lane :generate do
+ lane :appstore_profile do
setup_ci
match(
type: "appstore",
- app_identifier:["com.pokitmons.pokit"],
+ app_identifier:["com.pokitmons.pokit", "com.pokitmons.pokit.ShareExtension"],
readonly: true
)
-
- Dir.chdir("../") do
- sh("make release")
- end
end
- lane :test do
+ lane :development_profile do
setup_ci
match(
- type: "appstore",
- app_identifier:["com.pokitmons.pokit"],
+ type: "development",
+ app_identifier:["com.pokitmons.pokit", "com.pokitmons.pokit.ShareExtension"],
readonly: true
)
-
- Dir.chdir("../") do
- sh("make test")
- end
end
lane :build do
build_app(
workspace: "Pokit.xcworkspace",
scheme: "App",
- export_method: "app-store",
+ configuration: "Debug",
+ export_method: "development",
export_options: {
- method: "app-store",
- signingStyle: "manual",
- provisioningProfiles: {
- "com.pokitmons.pokit" => "match AppStore com.pokitmons.pokit 1721720816"
- }
+ provisioningProfiles: {
+ "com.pokitmons.pokit" => "match Development com.pokitmons.pokit",
+ "com.pokitmons.pokit.ShareExtension" => "match Development com.pokitmons.pokit.ShareExtension"
+ }
}
)
end
@@ -79,18 +71,18 @@ platform :ios do
build_app(
workspace: "Pokit.xcworkspace",
scheme: "App",
+ configuration: "Release",
export_method: "app-store",
export_options: {
- method: "app-store",
- signingStyle: "manual",
- provisioningProfiles: {
- "com.pokitmons.pokit" => "match AppStore com.pokitmons.pokit 1721720816"
- }
+ provisioningProfiles: {
+ "com.pokitmons.pokit" => "match AppStore com.pokitmons.pokit",
+ "com.pokitmons.pokit.ShareExtension" => "match AppStore com.pokitmons.pokit.ShareExtension"
+ }
}
)
end
- lane :release do
+ lane :testflight_release do
api_key = app_store_connect_api_key(
key_id: ENV['APP_STORE_CONNECT_KEY_ID'],
issuer_id: ENV['APP_STORE_CONNECT_ISSUER_ID'],
@@ -99,8 +91,60 @@ platform :ios do
upload_to_testflight(
api_key: api_key,
- distribute_external: false,
+ distribute_external: true,
+ groups: ["Pokitmons"],
changelog: ""
)
end
+
+ lane :appstore_release do
+ api_key = app_store_connect_api_key(
+ key_id: ENV['APP_STORE_CONNECT_KEY_ID'],
+ issuer_id: ENV['APP_STORE_CONNECT_ISSUER_ID'],
+ key_content: ENV['APP_STORE_CONNECT_API']
+ )
+
+ release_notes = File.read("release_notes.txt")
+
+ upload_to_app_store(
+ api_key: api_key,
+ skip_metadata: false,
+ skip_screenshots: true,
+ skip_binary_upload: true,
+ precheck_include_in_app_purchases: false,
+ release_notes: {
+ 'default' => release_notes
+ },
+ submit_for_review: true,
+ automatic_release: true,
+ force: true
+ )
+ end
+
+ lane :update_github_release do
+ require 'spaceship'
+
+ api_key = app_store_connect_api_key(
+ key_id: ENV['APP_STORE_CONNECT_KEY_ID'],
+ issuer_id: ENV['APP_STORE_CONNECT_ISSUER_ID'],
+ key_content: ENV['APP_STORE_CONNECT_API']
+ )
+
+ Spaceship::ConnectAPI.login(api_key: api_key)
+
+ app = Spaceship::ConnectAPI::App.find("com.pokitmons.pokit")
+
+ live_version = app.get_live_version
+ app_state = live_version.app_store_state
+ app_version = live_version.version_string
+
+ if app_state == 'READY_FOR_SALE'
+ pr_number = ENV['PR_NUMBER']
+ pr_body = sh("gh pr view #{pr_number} --json body --jq '.body'")
+ release_notes = pr_body.strip
+
+ # GitHub 릴리즈 버전 및 릴리즈 노트 업데이트
+ sh("gh release create v#{app_version} --notes '#{release_notes}'")
+ end
+ end
end
diff --git a/fastlane/Matchfile b/fastlane/Matchfile
index 591d5fd3..6648ebc2 100644
--- a/fastlane/Matchfile
+++ b/fastlane/Matchfile
@@ -1,8 +1,8 @@
-git_url(ENV['MATCH_URL'])
+git_url('git@github.com:stealmh/Pokit_iOS_Private.git')
storage_mode("git")
-type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
+type("development") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["com.pokitmons.pokit"])
username("shapekim98@gmail.com") # Your Apple Developer Portal username
diff --git a/fastlane/README.md b/fastlane/README.md
index 891747df..dd35e59d 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -15,13 +15,61 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## iOS
-### ios beta
+### ios appstore_profile
```sh
-[bundle exec] fastlane ios beta
+[bundle exec] fastlane ios appstore_profile
```
-Push a new beta build to TestFlight
+
+
+### ios development_profile
+
+```sh
+[bundle exec] fastlane ios development_profile
+```
+
+
+
+### ios build
+
+```sh
+[bundle exec] fastlane ios build
+```
+
+
+
+### ios archive
+
+```sh
+[bundle exec] fastlane ios archive
+```
+
+
+
+### ios testflight_release
+
+```sh
+[bundle exec] fastlane ios testflight_release
+```
+
+
+
+### ios appstore_release
+
+```sh
+[bundle exec] fastlane ios appstore_release
+```
+
+
+
+### ios update_github_release
+
+```sh
+[bundle exec] fastlane ios update_github_release
+```
+
+
----
diff --git a/fastlane/release_notes.txt b/fastlane/release_notes.txt
new file mode 100644
index 00000000..5baff929
--- /dev/null
+++ b/fastlane/release_notes.txt
@@ -0,0 +1,3 @@
+- 이제부터 어디서든 아이폰 공유하기로 링크를 저장할 수 있어요.
+- 인스타그램 링크에서 썸네일이 나오지 않는 문제를 수성했어요.
+- 자잘한 버그 및 사용성을 개선했어요.
\ No newline at end of file
diff --git a/fastlane/report.xml b/fastlane/report.xml
index 89ad2f28..9d5a5bf3 100644
--- a/fastlane/report.xml
+++ b/fastlane/report.xml
@@ -5,12 +5,17 @@
-
+
-
+
+
+
+
+
+
diff --git a/graph.png b/graph.png
index 328dd1ec..d8bc8e12 100644
Binary files a/graph.png and b/graph.png differ