diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..85d62c3b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,93 @@ +language: ko-KR # 언어 설정 + +early_access: true # 미리보기 기능 활성화 +enable_free_tier: true # 프리 티어 활성화 +auto_resolve_threads: false # 자동 해결 비활성화 + +reviews: + profile: chill + request_changes_workflow: true + high_level_summary: true # 리뷰에 대해 요약(high-level summary)를 자동 작성 + high_level_summary_placeholder: '@coderabbitai 요약' + auto_title_placeholder: '@coderabbitai' + poem: true + review_status: true # PR 리뷰 상태를 리뷰 요약란에 표시 + collapse_walkthrough: false # 리뷰 단계 설명을 기본적으로 접지 않음 + + abort_on_close: true # PR이 닫히면 리뷰 수행을 중단(abort) + + + auto_review: + enabled: true # 자동 리뷰 기능을 활성화 + auto_incremental_review: true # 커밋이 추가될 때마다 변경 사항에 대해서만 자동 수행 + ignore_title_keywords: [] # PR 제목에 포함되면 리뷰를 건너뛰는 키워드 목록 + labels: [] # 특정 라벨이 붙은 PR만 자동 리뷰 대상 + drafts: false # Draft 상태인 PR은 자동 리뷰 대상에서 제외(false면 제외) + base_branches: [] # 특정 브랜치만 리뷰하도록 + + tools: + shellcheck: # 셸 스크립트 문법 및 보안 검사 + enabled: true + ruff: # Python 코드 스타일 검사기 + enabled: true + markdownlint: # 마크다운 문법 검사 + enabled: true + github-checks: # GitHub 체크 연동 + 타임아웃(ms 단위) + enabled: true + timeout_ms: 90000 + languagetool: # 맞춤법, 문법 검사 + enabled: true + disabled_rules: + - EN_UNPAIRED_BRACKETS + - EN_UNPAIRED_QUOTES + disabled_categories: + - TYPOS + - TYPOGRAPHY + - CASING + enabled_only: false + level: default + enabled_rules: [] + enabled_categories: [] + biome: # JavaScript/TypeScript 정적 분석 + enabled: true + hadolint: # Dockerfile 코드 스타일 검사 + enabled: true + swiftlint: # Swift 코드 스타일 검사 + enabled: true + phpstan: # PHP 정적 분석 + enabled: true + level: default + golangci-lint: # Go 코드 스타일 검사 + enabled: true + yamllint: # YAML 형식 검사 + enabled: true + gitleaks: # Git 시크릿 노출 탐지 + enabled: true + checkov: # 인프라 보안 검사 + enabled: true + ast-grep: # AST 기반 코드 패턴 검사 + packages: [] + rule_dirs: [] + util_dirs: [] + essential_rules: true + +# CodeRabbit AI 챗 기능을 사용 가능하게 하고, +# 한 번에 처리 가능한 토큰 수를 최대 4096으로 제한 +chat: + enabled: true + max_token_length: 4096 + + +# 지식 기반에 사용할 학습 범위를 지정하십시오. +# 'Local' - Repository +# 'Global'- Organization +# 'Auto' - Repository(users public) + Organization(private) +knowledge_base: + web_search: # AI 웹 검색 허용 + enabled: true + learnings: # 학습 범위 설정 (local, global, auto) + scope: local + issues: # 이슈 자동 참조 범위 설정 (local, global, auto) + scope: auto + jira: + project_keys: [] diff --git "a/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\354\235\264\353\246\204.md" "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\354\235\264\353\246\204.md" new file mode 100644 index 00000000..b685dd47 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\354\235\264\353\246\204.md" @@ -0,0 +1,20 @@ +--- +name: 이슈 이름 +about: 팝풀 기본 템플릿 +title: '' +labels: '' +assignees: '' + +--- + +## 🤔 작업 배경 + +작업 배경을 적어주세요 + +## 📝 작업 내용 + +- 작업 내용을 적어주세요 + +## 👀 ETC (추후 개발해야 할 것, 참고자료 등) + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..321522df --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## 📌 이슈 + +- #이슈번호 + +## ✅ 작업 사항 + +- [ ] 작업 사항을 정리해주세요 + +## 🚀 테스트 방식 + + + +## 👀 ETC (추후 개발해야 할 것, 참고자료 등) -> + + \ No newline at end of file diff --git a/.github/secrets/ExportOptions.plist b/.github/secrets/ExportOptions.plist new file mode 100644 index 00000000..940c9086 --- /dev/null +++ b/.github/secrets/ExportOptions.plist @@ -0,0 +1,29 @@ + + + + + destination + export + manageAppVersionAndBuildNumber + + method + app-store-connect + provisioningProfiles + + com.poppoolIOS.poppool + PoppoolGitHubAction + + signingCertificate + 82F980617C0479150A4BCB89DC90498DCB319F8F + signingStyle + manual + stripSwiftSymbols + + teamID + W5QTRMS954 + testFlightInternalTestingOnly + + uploadSymbols + + + diff --git a/.github/secrets/PoppoolGitHubAction.mobileprovision.gpg b/.github/secrets/PoppoolGitHubAction.mobileprovision.gpg new file mode 100644 index 00000000..71dce081 Binary files /dev/null and b/.github/secrets/PoppoolGitHubAction.mobileprovision.gpg differ diff --git a/.github/secrets/certification.p12.gpg b/.github/secrets/certification.p12.gpg new file mode 100644 index 00000000..05c0e8d5 Binary files /dev/null and b/.github/secrets/certification.p12.gpg differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..04668907 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + pull_request: + branches: [main, develop, 'release/*'] + +jobs: + autocorrect: + name: 🤖 Autocorrect Workflow + runs-on: macos-15 # 최신 macOS 15 환경에서 실행 + if: github.actor != 'github-actions[bot]'&& github.base_ref == 'develop' # Actions 봇 커밋은 무시 && develop에서만 자동 수정 진행 + + steps: + - name: Checkout Repository # 저장소 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 🛠️ Set up Xcode # Xcode 16.2 선택 + run: sudo xcode-select -s /Applications/Xcode_16.2.app + + - name: ⬇️ Install SwiftLint # SwiftLint 설치 + run: brew install swiftlint + + - name: 🎨 Run SwiftLint Autocorrect # SwiftLint 자동 수정 실행 + run: swiftlint --fix + + - name: 🚀 Commit and Push Changes # 변경 사항 자동 커밋 및 푸시 + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git fetch origin "${GITHUB_HEAD_REF}:${GITHUB_HEAD_REF}" + git checkout "${GITHUB_HEAD_REF}" + + BRANCH_NAME="${GITHUB_HEAD_REF}" + if [[ "$BRANCH_NAME" =~ \#([0-9]+) ]]; then + ISSUE_NUMBER="${BASH_REMATCH[1]}" + else + ISSUE_NUMBER="" + fi + + if [ -n "$(git status --porcelain)" ]; then + git add . + git commit -m "style/#${ISSUE_NUMBER}: Apply SwiftLint autocorrect" + git push --set-upstream origin "${GITHUB_HEAD_REF}" + else + echo "No changes to commit" + fi + + build: + name: 🏗️ Build Workflow + runs-on: macos-15 # 최신 macOS 15 환경에서 실행 + if: github.actor != 'github-actions[bot]' # Actions 봇 커밋은 무시 + + steps: + - name: Checkout Repository # 저장소 코드 체크아웃 + uses: actions/checkout@v4 + + - name: ⚙️ Generate xcconfig + run: | + cat < Poppool/Poppool/Resource/Debug.xcconfig + KAKAO_AUTH_APP_KEY=${{ secrets.KAKAO_AUTH_APP_KEY }} + NAVER_MAP_CLIENT_ID=${{ secrets.NAVER_MAP_CLIENT_ID }} + POPPOOL_BASE_URL=${{ secrets.POPPOOL_BASE_URL }} + POPPOOL_S3_BASE_URL=${{ secrets.POPPOOL_S3_BASE_URL }} + POPPOOL_API_KEY=${{ secrets.POPPOOL_API_KEY }} + EOF + + - name: 🛠️ Select Xcode 16.2 # Xcode 16.2 버전 사용 설정 + run: sudo xcode-select -s /Applications/Xcode_16.2.app + + - name: ⬇️ Install SwiftLint # SwiftLint 설치 + run: brew install swiftlint + + - name: 🎨 Run SwiftLint # SwiftLint 코드 스타일 검사 실행 + run: swiftlint + + - name: 🔍 Detect Default Scheme # 기본 scheme 자동 검지 + id: detect_scheme + run: | + SCHEME=$(xcodebuild -list -json | jq -r '.project.schemes[0]') + echo "Detected scheme: $SCHEME" + echo "scheme=$SCHEME" >> "$GITHUB_OUTPUT" + + - name: 🔍 Detect Latest iPhone Simulator # 최신 사용 가능한 iPhone 시뮬레이터 검지 + id: detect_latest_simulator + run: | + DEVICE=$(xcrun simctl list devices available | grep -Eo 'iPhone .* \([0-9A-F\-]+\)' | head -n 1) + UDID=$(echo "$DEVICE" | grep -Eo '[0-9A-F\-]{36}') + NAME=$(echo "$DEVICE" | cut -d '(' -f1 | xargs) + echo "Detected simulator: $NAME ($UDID)" + echo "sim_name=$NAME" >> "$GITHUB_OUTPUT" + echo "sim_udid=$UDID" >> "$GITHUB_OUTPUT" + + - name: 🏗️ Build the project # 자동 검지된 Scheme과 Simulator로 빌드 수행 + run: | + WORKSPACE=$(find . -name "*.xcworkspace" | head -n 1) + xcodebuild -scheme "${{ steps.detect_scheme.outputs.scheme }}" \ + -workspace "$WORKSPACE" \ + -destination "platform=iOS Simulator,id=${{ steps.detect_latest_simulator.outputs.sim_udid }}" \ + clean build | xcpretty diff --git a/.github/workflows/deploy_on_release.yml b/.github/workflows/deploy_on_release.yml new file mode 100644 index 00000000..b6d01a45 --- /dev/null +++ b/.github/workflows/deploy_on_release.yml @@ -0,0 +1,139 @@ +name: Distribution to TestFlight + +on: + pull_request: + branches: [ release/* ] + +jobs: + deploy: + name: 🚀 Distribution to TestFlight Workflow + runs-on: macos-15 # 최신 macOS 15 환경에서 실행 + env: + # app archive 및 export 에 쓰일 환경 변수 설정 + XC_WORKSPACE: Poppool/Poppool.xcworkspace + XC_SCHEME: Poppool + XC_ARCHIVE: Poppool.xcarchive + + # certificate + ENCRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/certification.p12.gpg' }} + DECRYPTED_CERT_FILE_PATH: ${{ '.github/secrets/certification.p12' }} + CERT_ENCRYPTION_KEY: ${{ secrets.CERT_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호 + + # provisioning + ENCRYPTED_PROVISION_FILE_PATH: '.github/secrets/PoppoolGitHubAction.mobileprovision.gpg' + DECRYPTED_PROVISION_FILE_PATH: '.github/secrets/PoppoolGitHubAction.mobileprovision' + PROVISIONING_ENCRYPTION_KEY: ${{ secrets.PROVISION_ENCRYPTION_PWD }} # gpg로 파일 암호화할 때 사용한 암호 + + # certification export key + CERT_EXPORT_KEY: ${{ secrets.CERT_EXPORT_PWD }} + + KEYCHAIN: ${{ 'test.keychain' }} + + steps: + - name: Checkout Repository # 저장소 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 🛠️ Set up Xcode # Xcode 16.2 선택 + run: sudo xcode-select -s /Applications/Xcode_16.2.app + + - name: "#️⃣ Set Build Number" # 자동 빌드 넘버 세팅 + run: | + BUILD_NUMBER=$(TZ=Asia/Seoul date +%y%m%d.%H%M) + cd Poppool + agvtool new-version -all "$BUILD_NUMBER" + + - name: ⚙️ Generate xcconfig # 빌드에 필요한 xcconfig 생성 + run: | + echo "POPPOOL_BASE_URL=${POPPOOL_BASE_URL}" > Poppool/Poppool/Resource/Debug.xcconfig + echo "POPPOOL_S3_BASE_URL=${POPPOOL_S3_BASE_URL}" >> Poppool/Poppool/Resource/Debug.xcconfig + echo "POPPOOL_API_KEY=${POPPOOL_API_KEY}" >> Poppool/Poppool/Resource/Debug.xcconfig + echo "KAKAO_AUTH_APP_KEY=${KAKAO_AUTH_APP_KEY}" >> Poppool/Poppool/Resource/Debug.xcconfig + echo "NAVER_MAP_CLIENT_ID=${NAVER_MAP_CLIENT_ID}" >> Poppool/Poppool/Resource/Debug.xcconfig + env: + POPPOOL_BASE_URL: ${{ secrets.POPPOOL_BASE_URL }} + POPPOOL_S3_BASE_URL: ${{ secrets.POPPOOL_S3_BASE_URL }} + POPPOOL_API_KEY: ${{ secrets.POPPOOL_API_KEY }} + KAKAO_AUTH_APP_KEY: ${{ secrets.KAKAO_AUTH_APP_KEY }} + NAVER_MAP_CLIENT_ID: ${{ secrets.NAVER_MAP_CLIENT_ID }} + + - name: 🔑 Configure Keychain # 키체인 초기화 -> 임시 키체인 생성 + run: | + security create-keychain -p "" "$KEYCHAIN" + security list-keychains -s "$KEYCHAIN" + security default-keychain -s "$KEYCHAIN" + security unlock-keychain -p "" "$KEYCHAIN" + security set-keychain-settings + + - name : ©️ Configure Code Signing # 코드 사이닝 추가 + run: | + # certificate 복호화 + gpg -d -o "$DECRYPTED_CERT_FILE_PATH" --pinentry-mode=loopback --passphrase "$CERT_ENCRYPTION_KEY" "$ENCRYPTED_CERT_FILE_PATH" + + # provisioning 복호화 + gpg -d -o "$DECRYPTED_PROVISION_FILE_PATH" --pinentry-mode=loopback --passphrase "$PROVISIONING_ENCRYPTION_KEY" "$ENCRYPTED_PROVISION_FILE_PATH" + + # security를 사용하여 인증서와 개인 키를 새로 만든 키 체인으로 가져옴 + security import "$DECRYPTED_CERT_FILE_PATH" -k "$KEYCHAIN" -P "$CERT_EXPORT_KEY" -A + security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN" + + # Xcode에서 찾을 수 있는 프로비저닝 프로필 설치하기 위해 우선 프로비저닝 디렉토리를 생성 + mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + + # 디버깅 용 echo 명령어 + echo `ls .github/secrets/*.mobileprovision` + # 모든 프로비저닝 프로파일을 rename 하고 위에서 만든 디렉토리로 복사하는 과정 + for PROVISION in `ls .github/secrets/*.mobileprovision` + do + UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i ./$PROVISION)` + cp "./$PROVISION" "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision" + done + + - name: ⬇️ Archive app # 빌드 및 아카이브 + run: | + xcodebuild clean archive -workspace $XC_WORKSPACE -scheme $XC_SCHEME -configuration release -archivePath $XC_ARCHIVE + + - name: ⬆️ Export app # export 를 통해 ipa 파일 만듦 + run: | + xcodebuild -exportArchive -archivePath $XC_ARCHIVE -exportOptionsPlist .github/secrets/ExportOptions.plist -exportPath . -allowProvisioningUpdates + + - name: 🚀 Upload app to TestFlight # TestFlight에 아카이브된 앱 등록 + uses: apple-actions/upload-testflight-build@v1 + with: + app-path: 'Poppool.ipa' + issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} + api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} + api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} + + - name: 📣 Notify to Discord + if: success() + run: | + MARKETING_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" Poppool.xcarchive/Products/Applications/Poppool.app/Info.plist) + BUNDLE_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" Poppool.xcarchive/Products/Applications/Poppool.app/Info.plist) + + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{ + \"embeds\": [ + { + \"title\": \"🚀 TestFlight 배포 완료\", + \"description\": \"Poppool 앱이 성공적으로 TestFlight에 업로드되었습니다!\", + \"color\": 3066993, + \"fields\": [ + { + \"name\": \"🏷️ 마케팅 버전\", + \"value\": \"$MARKETING_VERSION\", + \"inline\": true + }, + { + \"name\": \"🛠️ 빌드 번호\", + \"value\": \"$BUNDLE_VERSION\", + \"inline\": true + } + ], + \"footer\": { + \"text\": \"TestFlight에서 위 버전을 설치하세요\" + } + } + ] + }" \ + ${{ secrets.TESTFLIGHT_WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore index df2cb547..01fa263e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,47 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings +# Xcode 관련 xcuserdata/ +.DS_Store + +# 개인 설정 및 비밀 정보 +*.xcconfig -## Obj-C/Swift specific +# Objective-C / Swift 관련 *.hmap -## App packaging +# 앱 패키징 *.ipa *.dSYM.zip *.dSYM -## Playgrounds +# Playgrounds timeline.xctimeline playground.xcworkspace -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Swift Package Manager (SPM) +.build/ +# 패키지 관련 파일을 무시하고 싶다면 아래 항목을 활성화하세요. # Packages/ # Package.pins # Package.resolved # *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project # .swiftpm -.build/ - # CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# +# Pods 디렉토리를 무시하고 싶다면 아래 항목을 활성화하세요. # Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - Carthage/Build/ +# Carthage 의존성을 무시하고 싶다면 아래 항목을 활성화하세요. +# Carthage/Checkouts # fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output -.DS_Store -Secrets.swift +# Cursor +**/buildServer.json +.vscode/* diff --git a/Check/modal/infobox/ico/line.png b/Check/modal/infobox/ico/line.png deleted file mode 100644 index 421badb4..00000000 Binary files a/Check/modal/infobox/ico/line.png and /dev/null differ diff --git a/Map/Tmap/modal/ico/logo/logo/square.png b/Map/Tmap/modal/ico/logo/logo/square.png deleted file mode 100644 index 30cb0be8..00000000 Binary files a/Map/Tmap/modal/ico/logo/logo/square.png and /dev/null differ diff --git a/Map/Tmap/naver/modal/ico/logo/logo/square.png b/Map/Tmap/naver/modal/ico/logo/logo/square.png deleted file mode 100644 index 7bfb5658..00000000 Binary files a/Map/Tmap/naver/modal/ico/logo/logo/square.png and /dev/null differ diff --git a/Map/modal/ico/logo/logo/modal/ico/logo/logo/square.png b/Map/modal/ico/logo/logo/modal/ico/logo/logo/square.png deleted file mode 100644 index ed269737..00000000 Binary files a/Map/modal/ico/logo/logo/modal/ico/logo/logo/square.png and /dev/null differ diff --git a/Map/modal/ico/logo/logo/square.png b/Map/modal/ico/logo/logo/square.png deleted file mode 100644 index 5eb0bfb0..00000000 Binary files a/Map/modal/ico/logo/logo/square.png and /dev/null differ diff --git a/Map/modal/ico/search/ico/ico/line.png b/Map/modal/ico/search/ico/ico/line.png deleted file mode 100644 index 10972352..00000000 Binary files a/Map/modal/ico/search/ico/ico/line.png and /dev/null differ diff --git a/Map/modal/ico/search/ico/ico/line@2x.png b/Map/modal/ico/search/ico/ico/line@2x.png deleted file mode 100644 index 4d632200..00000000 Binary files a/Map/modal/ico/search/ico/ico/line@2x.png and /dev/null differ diff --git a/Map/modal/ico/search/ico/ico/line@3x.png b/Map/modal/ico/search/ico/ico/line@3x.png deleted file mode 100644 index fbea4688..00000000 Binary files a/Map/modal/ico/search/ico/ico/line@3x.png and /dev/null differ diff --git a/Marker/UnTapMarker/search/ico/solid.png b/Marker/UnTapMarker/search/ico/solid.png deleted file mode 100644 index 10ce11cd..00000000 Binary files a/Marker/UnTapMarker/search/ico/solid.png and /dev/null differ diff --git a/Marker/search/ico/solid.png b/Marker/search/ico/solid.png deleted file mode 100644 index 39f13b31..00000000 Binary files a/Marker/search/ico/solid.png and /dev/null differ diff --git a/Poppool/.swiftlint.yml b/Poppool/.swiftlint.yml new file mode 100644 index 00000000..4718cfd2 --- /dev/null +++ b/Poppool/.swiftlint.yml @@ -0,0 +1,20 @@ +# 기본 활성화된 룰 중에 비활성화할 룰을 지정 +disabled_rules: + - type_body_length + - function_body_length + - file_length + - line_length + - force_cast + - force_try + - duplicate_conditions + - identifier_name + - cyclomatic_complexity + - redundant_optional_initialization + - function_parameter_count + +# 기본(default) 룰이 아닌 룰들을 활성화 +opt_in_rules: + - sorted_imports + - direct_return + - file_header + - weak_delegate diff --git "a/Poppool/AdminRepository\\.swift" "b/Poppool/AdminRepository\\.swift" deleted file mode 100644 index b5f7eebc..00000000 --- "a/Poppool/AdminRepository\\.swift" +++ /dev/null @@ -1,8 +0,0 @@ -// -// AdminRepository\.swift -// Poppool -// -// Created by 김기현 on 1/6/25. -// - -import Foundation diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure.xcodeproj/project.pbxproj b/Poppool/CoreLayer/Infrastructure/Infrastructure.xcodeproj/project.pbxproj new file mode 100644 index 00000000..49b3761e --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure.xcodeproj/project.pbxproj @@ -0,0 +1,440 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0522C1D72DB67B4F00B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1D62DB67B4F00B141FF /* RxSwift */; }; + 05EC93D92DB6605100771CB3 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC93D82DB6605100771CB3 /* RxCocoa */; }; + 05EC93DE2DB6612100771CB3 /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC93DD2DB6612100771CB3 /* RxGesture */; }; + 4EBC91D32DB8039800495C3B /* OSLog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EBC91D22DB8039800495C3B /* OSLog.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0512596C2DB5629C001342A2 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 058CC9182DB5383C0084221A /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4EBC91D22DB8039800495C3B /* OSLog.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OSLog.framework; path = System/Library/Frameworks/OSLog.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 058CC91A2DB5383C0084221A /* Infrastructure */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Infrastructure; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 058CC9152DB5383C0084221A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4EBC91D32DB8039800495C3B /* OSLog.framework in Frameworks */, + 0522C1D72DB67B4F00B141FF /* RxSwift in Frameworks */, + 05EC93D92DB6605100771CB3 /* RxCocoa in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05125B6A2DB56C32001342A2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4EBC91D22DB8039800495C3B /* OSLog.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 058CC90E2DB5383C0084221A = { + isa = PBXGroup; + children = ( + 058CC91A2DB5383C0084221A /* Infrastructure */, + 05125B6A2DB56C32001342A2 /* Frameworks */, + 058CC9192DB5383C0084221A /* Products */, + ); + sourceTree = ""; + }; + 058CC9192DB5383C0084221A /* Products */ = { + isa = PBXGroup; + children = ( + 058CC9182DB5383C0084221A /* Infrastructure.framework */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 058CC9132DB5383C0084221A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 058CC9172DB5383C0084221A /* Infrastructure */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058CC91F2DB5383C0084221A /* Build configuration list for PBXNativeTarget "Infrastructure" */; + buildPhases = ( + 058CC9132DB5383C0084221A /* Headers */, + 058CC9142DB5383C0084221A /* Sources */, + 058CC9152DB5383C0084221A /* Frameworks */, + 058CC9162DB5383C0084221A /* Resources */, + 0512596C2DB5629C001342A2 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 058CC91A2DB5383C0084221A /* Infrastructure */, + ); + name = Infrastructure; + packageProductDependencies = ( + 05EC93D82DB6605100771CB3 /* RxCocoa */, + 0522C1D62DB67B4F00B141FF /* RxSwift */, + ); + productName = Infrastructure; + productReference = 058CC9182DB5383C0084221A /* Infrastructure.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058CC90F2DB5383C0084221A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 058CC9172DB5383C0084221A = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 058CC9122DB5383C0084221A /* Build configuration list for PBXProject "Infrastructure" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058CC90E2DB5383C0084221A; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 05C1D82E2DB53CE300508FFD /* XCRemoteSwiftPackageReference "RxSwift" */, + 05EC93DC2DB6612100771CB3 /* XCRemoteSwiftPackageReference "RxGesture" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 058CC9192DB5383C0084221A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 058CC9172DB5383C0084221A /* Infrastructure */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 058CC9162DB5383C0084221A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 058CC9142DB5383C0084221A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 058CC91D2DB5383C0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 058CC91E2DB5383C0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 058CC9202DB5383C0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Infrastructure; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 058CC9212DB5383C0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Infrastructure; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 058CC9122DB5383C0084221A /* Build configuration list for PBXProject "Infrastructure" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC91D2DB5383C0084221A /* Debug */, + 058CC91E2DB5383C0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058CC91F2DB5383C0084221A /* Build configuration list for PBXNativeTarget "Infrastructure" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC9202DB5383C0084221A /* Debug */, + 058CC9212DB5383C0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 05C1D82E2DB53CE300508FFD /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; + 05EC93DC2DB6612100771CB3 /* XCRemoteSwiftPackageReference "RxGesture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxGesture"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0522C1D62DB67B4F00B141FF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05C1D82E2DB53CE300508FFD /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 05EC93D82DB6605100771CB3 /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 05C1D82E2DB53CE300508FFD /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 058CC90F2DB5383C0084221A /* Project object */; +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DIContainer.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DIContainer.swift new file mode 100644 index 00000000..66b3e1ef --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DIContainer.swift @@ -0,0 +1,66 @@ +import Foundation + +/// 의존성 주입 컨테이너 +/// +/// 이 컨테이너는 타입 기반으로 의존성을 등록하고 어디서든 안전하게 꺼내 쓸 수 있도록 도와줍니다. +/// +/// 앱 시작 시점에 필요한 구현체를 `register(_:_: )`를 통해 등록하고, +/// 이후에는 `resolve(_:)` 메서드를 통해 원하는 타입의 인스턴스를 꺼낼 수 있습니다. +/// +/// ## 등록 예시 +/// ```swift +/// DIContainer.register(SampleProtocol.self) { +/// SampleImpl() +/// } +/// ``` +/// +/// ## 사용 예시 +/// ```swift +/// // DIContainer의 resolve 메서드를 사용하는 방식 +/// let sample: SampleProtocol = DIContainer.resolve(SampleProtocol.self) +/// ``` +public final class DIContainer { + private static let container = DIContainer() + + private var registrations: [ObjectIdentifier: () -> Any] = [:] + + private let resolveQueue = DispatchQueue(label: "resolveQueue") + + private init() {} + + /// 의존성을 등록합니다. + /// - Parameters: + /// - type: 등록할 프로토콜 또는 클래스 타입 + /// - implementation: 해당 타입에 대응되는 구현체를 생성하는 클로저 + public static func register( + _ type: T.Type, + _ implementation: @escaping () -> T + ) { + container.register(type, implementation) + } + + /// 의존성을 꺼내옵니다. + /// - Parameter type: 요청할 타입 + /// - Returns: 등록된 타입의 인스턴스 + public static func resolve(_ type: T.Type) -> T { + return container.resolve(type) + } + + private func register( + _ type: T.Type, + _ implementation: @escaping () -> T + ) { + let key = ObjectIdentifier(type) + registrations[key] = { implementation() } + } + + private func resolve(_ type: T.Type) -> T { + let key = ObjectIdentifier(type) + + guard let registration = registrations[key], + let instance = registration() as? T + else { fatalError("\(type) does not registered") } + + return instance + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DependencyWrapper.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DependencyWrapper.swift new file mode 100644 index 00000000..7096c486 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/DIContainer/DependencyWrapper.swift @@ -0,0 +1,28 @@ +import Foundation + +/// 의존성 자동 주입을 위한 프로퍼티 래퍼 +/// +/// 사용하는 곳에서 `@Dependency`만 붙이면 등록된 구현체가 자동으로 주입됩니다. +/// +/// Swift의 프로퍼티 래퍼 특성상 `var`로 선언해야 하지만, 실제 인스턴스는 외부에서 변경할 수 없도록 `private(set)`으로 보호되어 불변성을 유지합니다. +/// +/// 사용 예시: +/// ```swift +/// class MyViewModel { +/// @Dependency var sample: SampleProtocol +/// +/// func run() { +/// sample.doSomething() +/// } +/// } +/// ``` +@propertyWrapper +public final class Dependency { + /// DIContainer에서 꺼내온 실제 인스턴스 + public private(set) var wrappedValue: T + + /// DIContainer로부터 자동으로 인스턴스를 꺼내와 초기화합니다. + public init() { + self.wrappedValue = DIContainer.resolve(T.self) + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Collection+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Collection+.swift new file mode 100644 index 00000000..f2c58874 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Collection+.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension Collection { + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Poppool/Poppool/Presentation/Extension/Date?+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Date?+.swift similarity index 89% rename from Poppool/Poppool/Presentation/Extension/Date?+.swift rename to Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Date?+.swift index f8117e7e..c0a294a2 100644 --- a/Poppool/Poppool/Presentation/Extension/Date?+.swift +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Date?+.swift @@ -1,13 +1,6 @@ -// -// Date+.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import Foundation -extension Optional where Wrapped == Date { +public extension Optional where Wrapped == Date { /// `yyyy. MM. dd` 형식으로 날짜를 문자열로 변환합니다. /// - Parameter defaultString: 날짜가 nil일 경우 반환할 기본 문자열 (기본값: 빈 문자열 "") /// - Returns: 형식화된 날짜 문자열 또는 기본 문자열 @@ -19,7 +12,7 @@ extension Optional where Wrapped == Date { formatter.dateFormat = "yyyy. MM. dd" return formatter.string(from: date) } - + func toPPDateMonthString(defaultString: String = "") -> String { guard let date = self else { return defaultString @@ -28,7 +21,7 @@ extension Optional where Wrapped == Date { formatter.dateFormat = "MM월 dd일" return formatter.string(from: date) } - + func toPPTimeeString(defaultString: String = "") -> String { guard let date = self else { return defaultString diff --git a/Poppool/Poppool/Presentation/Extension/Reactive+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Reactive+.swift similarity index 85% rename from Poppool/Poppool/Presentation/Extension/Reactive+.swift rename to Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Reactive+.swift index b3c44553..68a22b6c 100644 --- a/Poppool/Poppool/Presentation/Extension/Reactive+.swift +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/Reactive+.swift @@ -1,37 +1,30 @@ -// -// Reactive+.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/26/24. -// - import UIKit import RxCocoa import RxSwift -extension Reactive where Base: UIViewController { - +public extension Reactive where Base: UIViewController { + var viewDidLoad: ControlEvent { let source = self.methodInvoked(#selector(Base.viewDidLoad)).map( { _ in }) return ControlEvent(events: source) } - + var viewWillAppear: ControlEvent { let source = self.methodInvoked(#selector(Base.viewWillAppear)).map( { _ in }) return ControlEvent(events: source) } - + var viewDidAppear: ControlEvent { let source = self.methodInvoked(#selector(Base.viewDidAppear)).map( { _ in }) return ControlEvent(events: source) } - + var viewWillDisappear: ControlEvent { let source = self.methodInvoked(#selector(Base.viewWillDisappear)).map( { _ in }) return ControlEvent(events: source) } - + var viewDidDisappear: ControlEvent { let source = self.methodInvoked(#selector(Base.viewDidDisappear)).map( { _ in }) return ControlEvent(events: source) diff --git a/Poppool/Poppool/Presentation/Extension/String?+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/String?+.swift similarity index 89% rename from Poppool/Poppool/Presentation/Extension/String?+.swift rename to Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/String?+.swift index c1c5a916..c6bb485a 100644 --- a/Poppool/Poppool/Presentation/Extension/String?+.swift +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/String?+.swift @@ -1,22 +1,13 @@ -// -// String?+.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import Kingfisher - -extension Optional where Wrapped == String { +public extension Optional where Wrapped == String { /// ISO 8601 형식의 문자열을 `Date`로 변환하는 메서드 func toDate() -> Date? { guard let self = self else { return nil } // 옵셔널 해제 - + let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") - + if self.contains(".") { // 밀리초 포함 형식 dateFormatter.dateFormat = "yyyy.MM.dd'T'HH:mm:ss.SSS" @@ -24,10 +15,10 @@ extension Optional where Wrapped == String { // 밀리초 없는 형식 dateFormatter.dateFormat = "yyyy.MM.dd'T'HH:mm:ss" } - + return dateFormatter.date(from: self) } - + func isBrightImagePath(completion: @escaping (Bool) -> Void) { if let self = self { let imageView = UIImageView() @@ -50,7 +41,7 @@ extension String { } extension String { /// ISO 8601 형식의 문자열을 `Date`로 변환하는 메서드 - func toDate() -> Date? { + public func toDate() -> Date? { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") diff --git a/Poppool/Poppool/Presentation/Extension/UIImage+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImage+.swift similarity index 94% rename from Poppool/Poppool/Presentation/Extension/UIImage+.swift rename to Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImage+.swift index be80b2a2..cb0bce59 100644 --- a/Poppool/Poppool/Presentation/Extension/UIImage+.swift +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImage+.swift @@ -8,7 +8,7 @@ import UIKit // UIImage를 색상으로 생성하는 Helper Extension -extension UIImage { +public extension UIImage { convenience init(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { UIGraphicsBeginImageContext(size) UIGraphicsGetCurrentContext()?.setFillColor(color.cgColor) @@ -19,10 +19,10 @@ extension UIImage { } } -extension UIImage { +public extension UIImage { func isBright(threshold: CGFloat = 0.5) -> Bool? { guard let cgImage = self.cgImage else { return nil } - + let width = 1 let height = 1 let bitsPerComponent = 8 @@ -30,9 +30,9 @@ extension UIImage { let bytesPerRow = bytesPerPixel * width let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue - + var pixelData = [UInt8](repeating: 0, count: width * height * bytesPerPixel) - + guard let context = CGContext( data: &pixelData, width: width, @@ -42,16 +42,16 @@ extension UIImage { space: colorSpace, bitmapInfo: bitmapInfo ) else { return nil } - + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) - + let red = CGFloat(pixelData[0]) / 255.0 let green = CGFloat(pixelData[1]) / 255.0 let blue = CGFloat(pixelData[2]) / 255.0 - + // Brightness calculation formula let brightness = (red * 0.299 + green * 0.587 + blue * 0.114) - + return brightness > threshold } } diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImageView+.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImageView+.swift new file mode 100644 index 00000000..651ebfde --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Extension/UIImageView+.swift @@ -0,0 +1,72 @@ +import ObjectiveC +import UIKit + +private var currentURLKey: UInt8 = 0 +private var placeholderImageKey: UInt8 = 0 + +public extension UIImageView { + + private var currentImageURL: String? { + get { objc_getAssociatedObject(self, ¤tURLKey) as? String } + set { objc_setAssociatedObject(self, ¤tURLKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + private var placeholderImage: UIImage? { + get { objc_getAssociatedObject(self, &placeholderImageKey) as? UIImage } + set { objc_setAssociatedObject(self, &placeholderImageKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + func setPPImage(path: String?) { + loadImageFromImageLoader(path: path, completion: nil) + } + + func setPPImage(path: String?, completion: @escaping () -> Void) { + loadImageFromImageLoader(path: path, completion: completion) + } + + func loadImageFromImageLoader(path: String?, completion: (() -> Void)? = nil) { + // 기본 이미지 저장 + if placeholderImage == nil { + placeholderImage = UIImage(named: "image_default") + completion?() + } + + guard let path = path, !path.isEmpty else { + image = placeholderImage + currentImageURL = nil + completion?() + return + } + + let imageURLString = Secrets.popPoolS3BaseURL + path + guard let encodedURL = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + image = placeholderImage + currentImageURL = nil + completion?() + return + } + + // 이미 같은 URL을 로딩했고 이미지가 있으면 재로딩 방지 + if currentImageURL == encodedURL && self.image != nil && self.image != placeholderImage { + completion?() + return + } + + // 현재 이미지 URL을 업데이트 + currentImageURL = encodedURL + + ImageLoader.shared.loadImage(with: encodedURL, defaultImage: placeholderImage, imageQuality: .origin) { [weak self] image in + guard let self else { return } + defer { completion?() } + // 현재 요청 ID와 캡처된 ID가 일치하는지 확인 (다른 이미지로 변경되었으면 무시) + if self.currentImageURL == encodedURL { + if let image = image { + self.image = image + } else if self.image == nil { + // 이미지 로드 실패 시 기본 이미지 표시 + self.image = self.placeholderImage + } + } + } + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/DiskStorage.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/DiskStorage.swift new file mode 100644 index 00000000..127639b3 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/DiskStorage.swift @@ -0,0 +1,145 @@ +import CryptoKit +import UIKit + +/// 디스크에 이미지를 캐싱하는 클래스 +final class DiskStorage { + + /// 싱글톤 인스턴스 + static let shared = DiskStorage() + + /// 파일 관리 객체 + private let fileManager = FileManager.default + + /// 이미지 캐시 디렉터리 경로 + private let cacheDirectory: URL + + /// 초기화 메서드 (캐시 디렉터리 생성 및 자동 삭제 스케줄 시작) + private init() { + let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask) + cacheDirectory = urls[0].appendingPathComponent("ImageCache") + + // 디렉터리가 존재하지 않으면 생성 + if !fileManager.fileExists(atPath: cacheDirectory.path) { + try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) + } + startCacheCleanup() + } + + /// URL을 안전한 파일명으로 변환하는 메서드 + /// - Parameter url: 원본 URL 문자열 + /// - Returns: 파일명으로 변환된 문자열 + private func cacheFileName(for url: String) -> String { + let data = Data(url.utf8) + let hashed = SHA256.hash(data: data) + return hashed.compactMap { String(format: "%02x", $0) }.joined() + } + + /// 이미지를 디스크에 저장하는 메서드 + /// - Parameters: + /// - image: 저장할 UIImage 객체 + /// - url: 해당 이미지의 원본 URL 문자열 + func store(image: UIImage, url: String) { + let fileName = cacheFileName(for: url) + let fileURL = cacheDirectory.appendingPathComponent(fileName) + let metadataURL = cacheDirectory.appendingPathComponent("\(fileName).metadata") + + // 이미지 데이터를 JPEG 형식으로 변환하여 저장 + if let data = image.jpegData(compressionQuality: 0.8) { + do { + try data.write(to: fileURL) + } catch { + print("Error writing image data to disk: \(error)") + } + } + + // 만료 시간 기록 + let expirationDate = Date().addingTimeInterval(ImageLoader.shared.configure.diskCacheExpiration) + let metadata = ["expiration": expirationDate.timeIntervalSince1970] + + // 만료 정보를 JSON 형태로 저장 + if let metadataData = try? JSONSerialization.data(withJSONObject: metadata) { + do { + try metadataData.write(to: metadataURL) + } catch { + print("Error writing metadata: \(error)") + } + } + } + + /// 디스크에서 이미지를 불러오는 메서드 (만료된 경우 자동 삭제) + /// - Parameter url: 이미지의 원본 URL 문자열 + /// - Returns: UIImage 객체 (없거나 만료된 경우 nil) + func fetchImage(url: String) -> UIImage? { + let fileName = cacheFileName(for: url) + let fileURL = cacheDirectory.appendingPathComponent(fileName) + let metadataURL = cacheDirectory.appendingPathComponent("\(fileName).metadata") + + // 만료 시간 확인 + if let metadataData = try? Data(contentsOf: metadataURL), + let metadata = try? JSONSerialization.jsonObject(with: metadataData) as? [String: TimeInterval], + let expirationTime = metadata["expiration"] { + + // 만료 시간이 현재 시각을 초과하면 삭제 후 nil 반환 + if Date().timeIntervalSince1970 > expirationTime { + removeImage(url: url) + return nil + } + } + + // 이미지 파일이 존재하면 로드하여 반환 + if let data = try? Data(contentsOf: fileURL) { + return UIImage(data: data) + } + + return nil + } + + /// 특정 URL에 해당하는 이미지를 디스크에서 삭제하는 메서드 + /// - Parameter url: 삭제할 이미지의 원본 URL 문자열 + func removeImage(url: String) { + let fileName = cacheFileName(for: url) + let fileURL = cacheDirectory.appendingPathComponent(fileName) + let metadataURL = cacheDirectory.appendingPathComponent("\(fileName).metadata") + + do { + try fileManager.removeItem(at: fileURL) // 이미지 파일 삭제 + try fileManager.removeItem(at: metadataURL) // 메타데이터 파일 삭제 + } catch { + print("Failed to remove image: \(error)") + } + } + + /// 모든 캐시 데이터를 삭제하는 메서드 + func clearCache() { + do { + try fileManager.removeItem(at: cacheDirectory) + try fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Failed to clear cache: \(error)") + } + } + + /// 주기적으로 만료된 캐시를 삭제하는 메서드 + private func startCacheCleanup() { + let files = (try? self.fileManager.contentsOfDirectory(at: self.cacheDirectory, includingPropertiesForKeys: nil)) ?? [] + + for file in files { + if file.pathExtension == "metadata", + let metadataData = try? Data(contentsOf: file), + let metadata = try? JSONSerialization.jsonObject(with: metadataData) as? [String: TimeInterval], + let expirationTime = metadata["expiration"] { + + // 만료 시간이 지나면 이미지와 메타데이터 삭제 + if Date().timeIntervalSince1970 > expirationTime { + let imageFileURL = file.deletingPathExtension() // 메타데이터와 동일한 이름의 이미지 파일 + do { + try self.fileManager.removeItem(at: imageFileURL) + try self.fileManager.removeItem(at: file) // 메타데이터 삭제 + } catch { + print("Failed to delete expired cache: \(error)") + } + } + } + } + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/ImageLoader.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/ImageLoader.swift new file mode 100644 index 00000000..203d009b --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/ImageLoader.swift @@ -0,0 +1,151 @@ +import UIKit + +enum ImageLoaderError: Error { + case invalidURL + case networkError(description: String?) + case convertError(description: String?) +} + +enum ImageSizeOption { + case low + case middle + case high + case origin + + var size: CGSize { + switch self { + case .low: + return CGSize(width: 100, height: 100) + case .middle: + return CGSize(width: 200, height: 200) + case .high: + return CGSize(width: 400, height: 400) + case .origin: + return CGSize(width: 1000, height: 1000) + } + } +} + +/// 이미지 로더 설정 클래스 +/// - `memoryCacheExpiration`: 메모리 캐시 만료 시간 (기본값 300초) +class ImageLoaderConfigure { + var memoryCacheExpiration: TimeInterval = 300 + var diskCacheExpiration: TimeInterval = 86_400 +} + +/// URL을 통해 이미지를 비동기적으로 로드하는 클래스 +final class ImageLoader { + + static let shared = ImageLoader() + + /// 이미지 로더 설정 객체 + let configure = ImageLoaderConfigure() + + private init() {} + + /// URL을 통해 이미지를 로드하고, 실패 시 기본 이미지를 반환하는 메서드 + /// - Parameters: + /// - stringURL: 이미지 URL 문자열 + /// - defaultImage: 로드 실패 시 반환할 기본 이미지 + /// - completion: 로드 완료 후 호출되는 클로저 + func loadImage( + with stringURL: String?, + defaultImage: UIImage?, + imageQuality: ImageSizeOption = .origin, + completion: @escaping (UIImage?) -> Void + ) { + loadImage(with: stringURL) { [weak self] result in + switch result { + case .success(let image): + completion(self?.resizeImage(image, defaultImage: defaultImage, with: imageQuality)) + case .failure: + completion(defaultImage) + } + } + } +} + +private extension ImageLoader { + + /// URL을 통해 이미지를 로드하는 내부 메서드 + /// - Parameters: + /// - stringURL: 이미지 URL 문자열 + /// - completion: 로드 완료 후 호출되는 클로저 + func loadImage(with stringURL: String?, completion: @escaping (Result) -> Void) { + guard let stringURL = stringURL, let url = URL(string: stringURL) else { + completion(.failure(ImageLoaderError.invalidURL)) + return + } + + // 메모리 캐시에서 이미지 조회 + if let cachedImage = MemoryStorage.shared.fetchImage(url: stringURL) { + completion(.success(cachedImage)) + return + } + + // 디스크 캐시 확인 + if let diskImage = DiskStorage.shared.fetchImage(url: stringURL) { + // 메모리 캐시에 저장 후 반환 + MemoryStorage.shared.store(image: diskImage, url: stringURL) + completion(.success(diskImage)) + return + } + + // 네트워크에서 데이터 요청 + fetchDataFrom(url: url) { result in + switch result { + case .success(let data): + if let data = data, let image = UIImage(data: data) { + MemoryStorage.shared.store(image: image, url: stringURL) + DiskStorage.shared.store(image: image, url: stringURL) + DispatchQueue.main.async { completion(.success(image)) } + } else { + DispatchQueue.main.async { + completion(.failure(ImageLoaderError.convertError(description: "Failed to convert data to UIImage"))) + } + } + case .failure(let error): + DispatchQueue.main.async { completion(.failure(error)) } + } + } + } + + /// URL을 통해 데이터를 요청하는 메서드 + /// - Parameters: + /// - url: 요청할 URL 객체 + /// - completion: 요청 완료 후 호출되는 클로저 + func fetchDataFrom(url: URL, completion: @escaping (Result) -> Void) { + let task = URLSession.shared.dataTask(with: url) { data, _, error in + if let error = error { + completion(.failure(ImageLoaderError.networkError(description: "Network Error: \(error.localizedDescription)"))) + return + } + completion(.success(data)) + } + task.resume() + } + + func resizeImage(_ image: UIImage?, defaultImage: UIImage?, with sizeOption: ImageSizeOption) -> UIImage? { + guard let image else { return defaultImage } + + if sizeOption == .origin { return image } + + let targetSize = sizeOption.size + + // 비율 유지 리사이징 + let aspectRatio = image.size.width / image.size.height + var newSize = targetSize + + if aspectRatio > 1 { // 가로 이미지 + newSize.height = targetSize.width / aspectRatio + } else { // 세로 이미지 + newSize.width = targetSize.height * aspectRatio + } + + let renderer = UIGraphicsImageRenderer(size: newSize) + + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: newSize)) + } + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/MemoryStorage.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/MemoryStorage.swift new file mode 100644 index 00000000..2d4b30dc --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/ImageLoader/MemoryStorage.swift @@ -0,0 +1,95 @@ +import UIKit + +/// 캐시할 이미지와 만료 시간을 저장하는 클래스 +class StorageData: NSObject { + let image: UIImage? /// 캐시된 이미지 + let expirationDate: Date /// 캐시 만료 시간 + + /// 초기화 메서드 + /// - Parameters: + /// - image: 저장할 이미지 + /// - expiration: 만료 시간 (초 단위) + init(image: UIImage?, expiration: TimeInterval) { + self.image = image + self.expirationDate = Date().addingTimeInterval(expiration) + } + + /// 캐시가 만료되었는지 확인하는 메서드 + /// - Returns: 만료 여부 (true: 만료됨, false: 유효함) + func isExpired() -> Bool { + return Date() > expirationDate + } +} + +/// 메모리 캐시를 관리하는 클래스 +final class MemoryStorage { + + /// 싱글톤 인스턴스 + static let shared = MemoryStorage() + + /// 이미지 캐시 저장소 + private let cache = NSCache() + + /// 현재 캐시에 저장된 키 목록 + private var cachedKeys: Set = [] + + /// 초기화 (자동 캐시 정리 시작) + private init() { + startCacheCleanup() + } + + /// 이미지를 캐시에 저장하는 메서드 + /// - Parameters: + /// - image: 저장할 이미지 + /// - url: 이미지 URL 문자열 + func store(image: UIImage?, url: String) { + let cachedData = StorageData(image: image, expiration: ImageLoader.shared.configure.memoryCacheExpiration) + cache.setObject(cachedData, forKey: url as NSString) + cachedKeys.insert(url) + } + + /// 캐시에서 이미지를 가져오는 메서드 + /// - Parameter url: 이미지 URL 문자열 + /// - Returns: 캐시된 UIImage (없으면 nil) + func fetchImage(url: String) -> UIImage? { + if let cachedData = cache.object(forKey: url as NSString), !cachedData.isExpired() { + return cachedData.image + } else { + removeData(url: url) + return nil + } + } + + /// 특정 URL의 캐시 데이터를 제거하는 메서드 + /// - Parameter url: 제거할 이미지의 URL 문자열 + func removeData(url: String) { + cache.removeObject(forKey: url as NSString) + cachedKeys.remove(url) + } + + /// 모든 캐시 데이터를 삭제하는 메서드 + func clearCache() { + cache.removeAllObjects() + cachedKeys.removeAll() + } + + /// 주기적으로 만료된 캐시를 정리하는 메서드 + private func startCacheCleanup() { + DispatchQueue.global(qos: .background).async { [weak self] in + guard let self = self else { return } + + let cleanTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in + for key in self.cachedKeys { + let nsKey = key as NSString + if let cachedData = self.cache.object(forKey: nsKey), cachedData.isExpired() { + self.cache.removeObject(forKey: nsKey) + self.cachedKeys.remove(key) + } + } + } + // 백그라운드에서 실행되는 타이머를 메인 루프에 추가 + RunLoop.current.add(cleanTimer, forMode: .common) + RunLoop.current.run() // 백그라운드 스레드에서 타이머를 계속 실행하기 위해 RunLoop를 유지 + } + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/Logger/Logger.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Logger/Logger.swift new file mode 100644 index 00000000..c636a5e6 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Logger/Logger.swift @@ -0,0 +1,107 @@ +import Foundation +import OSLog + +public struct Logger { + private static let subsystem = Bundle.main.bundleIdentifier ?? "com.poppoolIOS.poppool" + + public enum Level: Hashable { + case info + case debug + case network + case error + case event + case custom(name: String) + + var categoryName: String { + switch self { + case .info: + return "Info" + case .debug: + return "Debug" + case .network: + return "Network" + case .error: + return "Error" + case .event: + return "Event" + case .custom(let name): return name + } + } + + var categoryIcon: String { + switch self { + case .info: + return "✅" + case .debug: + return "⚠️" + case .network: + return "🌎" + case .error: + return "⛔️" + case .event: + return "🎉" + case .custom: + return "🍎" + } + } + } + + public enum LogLevel { + case debug + case info + case error + case fault + + var osLogType: OSLogType { + switch self { + case .debug: + return .debug + case .info: + return .info + case .error: return .error + case .fault: return .fault + } + } + } + + private static var isShowFileName: Bool = false + private static var isShowLine: Bool = true + private static var isShowLog: Bool = true + + private static var loggers: [Level: os.Logger] = [:] + private static func getLogger(for category: Level) -> os.Logger { + let categoryName = category.categoryName + + if let cachedLogger = loggers[category] { + return cachedLogger + } + + let logger = os.Logger(subsystem: subsystem, category: categoryName) + loggers[category] = logger + return logger + } + + public static func log( + _ message: Any, + category: Level, + level: LogLevel = .info, + file: String = #file, + line: Int = #line + ) { + guard isShowLog else { return } + + let logger = getLogger(for: category) + var fullMessage = "\(category.categoryIcon) \(message)" + + if isShowFileName { + let fileNameOnly = (file as NSString).lastPathComponent + fullMessage += " | 📁 \(fileNameOnly)" + } + + if isShowLine { + fullMessage += " | 📍 \(line)" + } + + logger.log(level: level.osLogType, "\(fullMessage, privacy: .public)") + } +} diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/Secrets.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Secrets.swift new file mode 100644 index 00000000..80a48514 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Secrets.swift @@ -0,0 +1,30 @@ +import Foundation + +public enum Secrets { + public static var kakaoAuthAppKey: String { + return getValue(forKey: "KAKAO_AUTH_APP_KEY") + } + + public static var popPoolBaseURL: String { + return getValue(forKey: "POPPOOL_BASE_URL") + } + + public static var popPoolS3BaseURL: String { + return getValue(forKey: "POPPOOL_S3_BASE_URL") + } + + public static var popPoolAPIKey: String { + return getValue(forKey: "POPPOOL_API_KEY") + } + + public static var naverMapClientID: String { + return getValue(forKey: "NAVER_MAP_CLIENT_ID") + } + + private static func getValue(forKey key: String) -> String { + guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String else { + fatalError("Missing key: \(key) in Info.plist") + } + return value + } +} diff --git a/Poppool/Poppool/Infrastructure/KeyChainService.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Service/KeyChainService.swift similarity index 81% rename from Poppool/Poppool/Infrastructure/KeyChainService.swift rename to Poppool/CoreLayer/Infrastructure/Infrastructure/Service/KeyChainService.swift index 7717acea..822263e7 100644 --- a/Poppool/Poppool/Infrastructure/KeyChainService.swift +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Service/KeyChainService.swift @@ -1,16 +1,9 @@ -// -// KeyChainService.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/2/24. -// - import Foundation import Security import RxSwift -final class KeyChainService { +public final class KeyChainService { // KeyChain에서 발생할 수 있는 오류를 정의 enum KeyChainError: Error { @@ -18,14 +11,16 @@ final class KeyChainService { case unhandledError(status: OSStatus) // 예상치 못한 OSStatus 오류가 발생했을 때 발생 case dataConversionError(message: String) // 데이터 변환 중 오류가 발생했을 때 발생 } - + // KeyChain 서비스 이름 private let service = "keyChain" - + + public init() { } + /// KeyChain에서 특정 타입의 토큰을 가져오는 메서드 /// - Parameter type: 가져오려는 토큰의 타입 (`accessToken` 또는 `refreshToken`) /// - Returns: 가져온 토큰을 담은 `Single` - func fetchToken(type: TokenType) -> Result { + public func fetchToken(type: TokenType) -> Result { // 1. query 작성 let keyChainQuery: NSDictionary = [ kSecClass: kSecClassGenericPassword, @@ -34,11 +29,11 @@ final class KeyChainService { kSecReturnData: true, // CFData 타입으로 불러오라는 의미 kSecMatchLimit: kSecMatchLimitOne // 중복되는 경우 하나의 값만 가져오라는 의미 ] - + // 2. Read var dataTypeRef: AnyObject? let status = SecItemCopyMatching(keyChainQuery, &dataTypeRef) - + // 3. Result if status == errSecItemNotFound { return .failure(KeyChainError.noValueFound(message: "No value found for the specified key.")) @@ -48,10 +43,8 @@ final class KeyChainService { if let data = dataTypeRef as? Data { if let value = String(data: data, encoding: .utf8) { Logger.log( - message: "Successfully fetched \(type.rawValue) from KeyChain: \(value)", - category: .info, - fileName: #file, - line: #line + "Successfully fetched \(type.rawValue) from KeyChain", + category: .info ) return .success(value) } else { @@ -67,12 +60,12 @@ final class KeyChainService { /// - Parameter type: 저장하려는 토큰의 타입 (`accessToken` 또는 `refreshToken`) /// - Parameter value: 저장할 토큰의 값 /// - Returns: 완료 시 `Completable` - func saveToken(type: TokenType, value: String) -> Result { + public func saveToken(type: TokenType, value: String) -> Result { // allowLossyConversion은 인코딩 과정에서 손실이 되는 것을 허용할 것인지 설정 guard let convertValue = value.data(using: .utf8, allowLossyConversion: false) else { return .failure(KeyChainError.dataConversionError(message: "Failed to convert value to Data.")) } - + // 1. query 작성 let keyChainQuery: NSDictionary = [ kSecClass: kSecClassGenericPassword, @@ -80,46 +73,43 @@ final class KeyChainService { kSecAttrAccount: type.rawValue, kSecValueData: convertValue ] - + // 2. Delete // KeyChain은 Key값에 중복이 생기면 저장할 수 없기 때문에 먼저 Delete SecItemDelete(keyChainQuery) - + // 3. Create let status = SecItemAdd(keyChainQuery, nil) if status == errSecSuccess { Logger.log( - message: "Successfully saved \(type.rawValue) to KeyChain: \(value)", - category: .info, - fileName: #file, - line: #line + "Successfully fetched \(type.rawValue) from KeyChain: \(value)", + category: .info + ) return .success(()) } else { return .failure(KeyChainError.unhandledError(status: status)) } } - + /// KeyChain에서 특정 타입의 토큰을 삭제하는 메서드 /// - Parameter type: 삭제하려는 토큰의 타입 (`accessToken` 또는 `refreshToken`) /// - Returns: 완료 시 `Completable` - func deleteToken(type: TokenType) -> Result { + public func deleteToken(type: TokenType) -> Result { // 1. query 작성 let keyChainQuery: NSDictionary = [ kSecClass: kSecClassGenericPassword, kSecAttrService: self.service, kSecAttrAccount: type.rawValue ] - + // 2. Delete let status = SecItemDelete(keyChainQuery) - + if status == errSecSuccess { Logger.log( - message: "Successfully deleted \(type.rawValue) from KeyChain", - category: .info, - fileName: #file, - line: #line + "Successfully deleted \(type.rawValue) from KeyChain", + category: .info ) return .success(()) } else { @@ -128,7 +118,7 @@ final class KeyChainService { } } -enum TokenType: String { +public enum TokenType: String { case accessToken // 액세스 토큰 case refreshToken // 리프레시 토큰 } diff --git a/Poppool/CoreLayer/Infrastructure/Infrastructure/Service/UserDefaultService.swift b/Poppool/CoreLayer/Infrastructure/Infrastructure/Service/UserDefaultService.swift new file mode 100644 index 00000000..52211c19 --- /dev/null +++ b/Poppool/CoreLayer/Infrastructure/Infrastructure/Service/UserDefaultService.swift @@ -0,0 +1,128 @@ +// +// UserDefaultService.swift +// MomsVillage +// +// Created by SeoJunYoung on 9/2/24. +// + +import Foundation + +import RxSwift + +public final class UserDefaultService { + + public init() { } + + /// Userdefault 데이터 저장 메서드 + /// - Parameters: + /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 + /// - value: 저장하는 데이터 값 i.e) access token 등 + /// - to: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func save(key: String, value: String) { + UserDefaults.standard.set(value, forKey: key) + } + + /// Userdefault 데이터 저장 메서드 + /// - Parameters: + /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 + /// - value: 저장하는 데이터 값 i.e) access token 등 + /// - to: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func save(key: String, value: [String]) { + UserDefaults.standard.set(value, forKey: key) + } + + /// Userdefault 데이터 발견 메서드 + /// - Parameters: + /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 찾은 데이터 - String 타입 + public func fetch(key: String) -> String? { + if let token = UserDefaults.standard.string(forKey: key) { + return token + } + return nil + } + + /// Userdefault 데이터 발견 메서드 + /// - Parameters: + /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 찾은 데이터 - String 타입 + public func fetchArray(key: String) -> [String]? { + if let token = UserDefaults.standard.array(forKey: key) as? [String] { + return token + } + return nil + } + + /// Userdefault 데이터 삭제 메서드 + /// - Parameters: + /// - key: 삭제하는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func delete(key: String) { + UserDefaults.standard.removeObject(forKey: key) + } +} + +// MARK: - Key base +extension UserDefaultService { + public enum Key: String { + case searchKeyword = "searchList" + } + + /// Userdefault 데이터 저장 메서드 + /// - Parameters: + /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 + /// - value: 저장하는 데이터 값 i.e) access token 등 + /// - to: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func save(keyType: Key, value: String) { + UserDefaults.standard.set(value, forKey: keyType.rawValue) + } + + /// Userdefault 데이터 저장 메서드 + /// - Parameters: + /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 + /// - value: 저장하는 데이터 값 i.e) access token 등 + /// - to: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func save(keyType: Key, value: [String]) { + UserDefaults.standard.set(value, forKey: keyType.rawValue) + } + + /// Userdefault 데이터 발견 메서드 + /// - Parameters: + /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 찾은 데이터 - String 타입 + public func fetch(keyType: Key) -> String? { + if let token = UserDefaults.standard.string(forKey: keyType.rawValue) { + return token + } + return nil + } + + /// Userdefault 데이터 발견 메서드 + /// - Parameters: + /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 찾은 데이터 - String 타입 + public func fetchArray(keyType: Key) -> [String]? { + if let token = UserDefaults.standard.array(forKey: keyType.rawValue) as? [String] { + return token + } + return nil + } + + /// Userdefault 데이터 삭제 메서드 + /// - Parameters: + /// - key: 삭제하는 데이터의 키 값 i.e) 유저 id 등 + /// - from: 로컬 데이터베이스 타입 - DatabaseType + /// - Returns: 별도 안내 없음 + public func delete(keyType: Key) { + UserDefaults.standard.removeObject(forKey: keyType.rawValue) + } +} diff --git a/Poppool/DataLayer/Data/Data.xcodeproj/project.pbxproj b/Poppool/DataLayer/Data/Data.xcodeproj/project.pbxproj new file mode 100644 index 00000000..66df90ab --- /dev/null +++ b/Poppool/DataLayer/Data/Data.xcodeproj/project.pbxproj @@ -0,0 +1,543 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0522C1DB2DB67C6100B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1DA2DB67C6100B141FF /* RxSwift */; }; + 05BBA73A2DB75A320047A061 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 05BBA7392DB75A320047A061 /* KakaoSDKAuth */; }; + 05BBA73C2DB75A320047A061 /* KakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 05BBA73B2DB75A320047A061 /* KakaoSDKUser */; }; + 05BDD3DC2DB66EB500C1E192 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 05BDD3DB2DB66EB500C1E192 /* Alamofire */; }; + 05C1D6172DB53A5600508FFD /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6152DB53A5600508FFD /* DomainInterface.framework */; }; + 05C1D6192DB53A5600508FFD /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6162DB53A5600508FFD /* Infrastructure.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058CC8DC2DB5376A0084221A /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6152DB53A5600508FFD /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6162DB53A5600508FFD /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 05C1D67A2DB53AB000508FFD /* Exceptions for "Data" folder in "Data" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Network/API/AdminAPI/AdminAPIEndpoint.swift, + Network/API/AdminAPI/ResponseDTO/AdminResponseDTO.swift, + Network/API/AdminAPI/ResponseDTO/GetAdminPopUpStoreListResponseDTO.swift, + Network/API/AuthAPI/AuthAPIEndPoint.swift, + Network/API/AuthAPI/ResponseDTO/LoginResponseDTO.swift, + Network/API/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift, + Network/API/CategoryAPI/CategoryAPIEndpoint.swift, + Network/API/CategoryAPI/ResponseDTO/GetCategoryListResponseDTO.swift, + Network/API/CommentAPI/CommentAPIEndPoint.swift, + Network/API/CommentAPI/RequestDTO/DeleteCommentRequestDTO.swift, + Network/API/CommentAPI/RequestDTO/PostCommentRequestDTO.swift, + Network/API/CommentAPI/RequestDTO/PutCommentRequestDTO.swift, + Network/API/HomeAPI/HomeAPIEndpoint.swift, + Network/API/HomeAPI/RequestDTO/HomeSortedRequestDTO.swift, + Network/API/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift, + Network/API/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift, + Network/API/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift, + Network/API/MapAPI/FindDirectionEndPoint.swift, + Network/API/MapAPI/MapAPIEndpoint.swift, + Network/API/MapAPI/ResponseDTO/GetPopUpDirectionResponseDTO.swift, + Network/API/MapAPI/ResponseDTO/MapPopUpStoreDTO.swift, + Network/API/PopUpAPI/PopUpAPIEndPoint.swift, + Network/API/PopUpAPI/RequestDTO/GetPopUpCommentRequestDTO.swift, + Network/API/PopUpAPI/RequestDTO/GetPopUpDetailRequestDTO.swift, + Network/API/PopUpAPI/RequestDTO/GetSearchPopUpListRequestDTO.swift, + Network/API/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift, + Network/API/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift, + Network/API/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift, + Network/API/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift, + Network/API/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift, + Network/API/PreSignedAPI/PreSignedAPIEndPoint.swift, + Network/API/PreSignedAPI/RequestDTO/PresignedURLRequestDTO.swift, + Network/API/PreSignedAPI/ResponseDTO/PreSignedURLDTO.swift, + Network/API/PreSignedAPI/ResponseDTO/PreSignedURLResponseDTO.swift, + Network/API/SearchAPI/RequestDTO/GetSearchPopupStoreRequestDTO.swift, + Network/API/SearchAPI/ResponseDTO/GetSearchPopupStoreResponseDTO.swift, + Network/API/SearchAPI/SearchAPIEndPoint.swift, + Network/API/SignUpAPI/RequestDTO/CheckNickNameRequestDTO.swift, + Network/API/SignUpAPI/RequestDTO/SignUpRequestDTO.swift, + Network/API/SignUpAPI/SignUpAPIEndpoint.swift, + Network/API/UserAPI/RequesetDTO/CommentLikeRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/GetMyCommentRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/GetOtherUserCommentListRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/PostBookmarkPopUpRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/PostUserBlockRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/PutUserCategoryRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/PutUserProfileRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/PutUserTailoredInfoRequestDTO.swift, + Network/API/UserAPI/RequesetDTO/UserSortedRequestDTO.swift, + Network/API/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift, + Network/API/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift, + Network/API/UserAPI/UserAPIEndPoint.swift, + Network/Common/NetworkError.swift, + Network/Common/Requestable.swift, + Network/Common/Responsable.swift, + Network/EndPoint/Endpoint.swift, + Network/EndPoint/MultipartEndPoint.swift, + Network/EndPoint/RequestEndpoint.swift, + Network/Interceptor/TokenInterceptor.swift, + Network/Provider/Provider.swift, + Network/Provider/ProviderImpl.swift, + Network/Service/AppleLoginService.swift, + Network/Service/AuthServiceable.swift, + Network/Service/KakaoLoginService.swift, + Network/Service/PreSignedService.swift, + RepositoryImpl/AdminRepositoryImpl.swift, + RepositoryImpl/AppleLoginRepositoryImpl.swift, + RepositoryImpl/AuthAPIRepositoryImpl.swift, + RepositoryImpl/CategoryRepositoryImpl.swift, + RepositoryImpl/CommentAPIRepositoryImpl.swift, + RepositoryImpl/HomeAPIRepositoryImpl.swift, + RepositoryImpl/KakaoLoginRepositoryImpl.swift, + RepositoryImpl/MapDirectionRepositoryImpl.swift, + RepositoryImpl/MapRepositoryImpl.swift, + RepositoryImpl/PopUpAPIRepositoryImpl.swift, + RepositoryImpl/PreSignedRepositoryImpl.swift, + RepositoryImpl/Search/SearchAPIRepositoryImpl.swift, + RepositoryImpl/SignUpRepositoryImpl.swift, + RepositoryImpl/UserAPIRepositoryImpl.swift, + ); + target = 058CC8DB2DB5376A0084221A /* Data */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 05C1D05C2DB5387500508FFD /* Data */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 05C1D67A2DB53AB000508FFD /* Exceptions for "Data" folder in "Data" target */, + ); + path = Data; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 058CC8D92DB5376A0084221A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05BDD3DC2DB66EB500C1E192 /* Alamofire in Frameworks */, + 05C1D6172DB53A5600508FFD /* DomainInterface.framework in Frameworks */, + 05BBA73C2DB75A320047A061 /* KakaoSDKUser in Frameworks */, + 0522C1DB2DB67C6100B141FF /* RxSwift in Frameworks */, + 05C1D6192DB53A5600508FFD /* Infrastructure.framework in Frameworks */, + 05BBA73A2DB75A320047A061 /* KakaoSDKAuth in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058CC8D22DB5376A0084221A = { + isa = PBXGroup; + children = ( + 05C1D05C2DB5387500508FFD /* Data */, + 05C1D6142DB53A5600508FFD /* Frameworks */, + 058CC8DD2DB5376A0084221A /* Products */, + ); + sourceTree = ""; + }; + 058CC8DD2DB5376A0084221A /* Products */ = { + isa = PBXGroup; + children = ( + 058CC8DC2DB5376A0084221A /* Data.framework */, + ); + name = Products; + sourceTree = ""; + }; + 05C1D6142DB53A5600508FFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 05C1D6152DB53A5600508FFD /* DomainInterface.framework */, + 05C1D6162DB53A5600508FFD /* Infrastructure.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 058CC8D72DB5376A0084221A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 058CC8DB2DB5376A0084221A /* Data */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058CC8E32DB5376A0084221A /* Build configuration list for PBXNativeTarget "Data" */; + buildPhases = ( + 058CC8D72DB5376A0084221A /* Headers */, + 058CC8D82DB5376A0084221A /* Sources */, + 058CC8D92DB5376A0084221A /* Frameworks */, + 058CC8DA2DB5376A0084221A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Data; + packageProductDependencies = ( + 05BDD3DB2DB66EB500C1E192 /* Alamofire */, + 0522C1DA2DB67C6100B141FF /* RxSwift */, + 05BBA7392DB75A320047A061 /* KakaoSDKAuth */, + 05BBA73B2DB75A320047A061 /* KakaoSDKUser */, + ); + productName = Data; + productReference = 058CC8DC2DB5376A0084221A /* Data.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058CC8D32DB5376A0084221A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 058CC8DB2DB5376A0084221A = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 058CC8D62DB5376A0084221A /* Build configuration list for PBXProject "Data" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058CC8D22DB5376A0084221A; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 08B2A3532DB66B1D00E57EFA /* XCRemoteSwiftPackageReference "Alamofire" */, + 05BDD5DA2DB6786900C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */, + 05BBA7382DB75A320047A061 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 058CC8DD2DB5376A0084221A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 058CC8DB2DB5376A0084221A /* Data */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 058CC8DA2DB5376A0084221A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 058CC8D82DB5376A0084221A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 058CC8E12DB5376A0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 058CC8E22DB5376A0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 058CC8E42DB5376A0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Data; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 058CC8E52DB5376A0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Data; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 058CC8D62DB5376A0084221A /* Build configuration list for PBXProject "Data" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC8E12DB5376A0084221A /* Debug */, + 058CC8E22DB5376A0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058CC8E32DB5376A0084221A /* Build configuration list for PBXNativeTarget "Data" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC8E42DB5376A0084221A /* Debug */, + 058CC8E52DB5376A0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 05BBA7382DB75A320047A061 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kakao/kakao-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.24.1; + }; + }; + 05BDD5DA2DB6786900C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; + 08B2A3532DB66B1D00E57EFA /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.10.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0522C1DA2DB67C6100B141FF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05BDD5DA2DB6786900C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 05BBA7392DB75A320047A061 /* KakaoSDKAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 05BBA7382DB75A320047A061 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKAuth; + }; + 05BBA73B2DB75A320047A061 /* KakaoSDKUser */ = { + isa = XCSwiftPackageProductDependency; + package = 05BBA7382DB75A320047A061 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKUser; + }; + 05BDD3DB2DB66EB500C1E192 /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3532DB66B1D00E57EFA /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 058CC8D32DB5376A0084221A /* Project object */; +} diff --git a/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/AdminAPIEndpoint.swift similarity index 84% rename from Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/AdminAPI/AdminAPIEndpoint.swift index cfb9baf3..1b7175d1 100644 --- a/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/AdminAPIEndpoint.swift @@ -1,5 +1,9 @@ import Foundation +import Infrastructure + +struct EmptyResponse: Decodable {} + struct AdminAPIEndpoint { // MARK: - Store List @@ -14,7 +18,7 @@ struct AdminAPIEndpoint { size: size ) return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/popup-stores/list", method: .get, queryParameters: params @@ -26,7 +30,7 @@ struct AdminAPIEndpoint { id: Int64 ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/popup-stores", method: .get, queryParameters: ["popUpStoreId": id] @@ -38,7 +42,7 @@ struct AdminAPIEndpoint { request: CreatePopUpStoreRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/popup-stores", method: .post, bodyParameters: request @@ -50,7 +54,7 @@ struct AdminAPIEndpoint { request: UpdatePopUpStoreRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/popup-stores", method: .put, bodyParameters: request @@ -62,7 +66,7 @@ struct AdminAPIEndpoint { id: Int64 ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/popup-stores", method: .delete, queryParameters: ["popUpStoreId": id] @@ -74,7 +78,7 @@ struct AdminAPIEndpoint { request: CreateNoticeRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/notice", method: .post, bodyParameters: request @@ -86,7 +90,7 @@ struct AdminAPIEndpoint { request: UpdateNoticeRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/notice/\(id)", method: .put, bodyParameters: request @@ -97,7 +101,7 @@ struct AdminAPIEndpoint { id: Int64 ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/admin/notice/\(id)", method: .delete ) diff --git a/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/AdminResponseDTO.swift similarity index 93% rename from Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/AdminResponseDTO.swift index 5924f809..338ec553 100644 --- a/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/AdminResponseDTO.swift @@ -13,7 +13,6 @@ struct GetAdminPopUpStoreListResponseDTO: Decodable { let mainImageUrl: String } - } // MARK: - Store Detail Response @@ -41,6 +40,3 @@ struct GetAdminPopUpStoreDetailResponseDTO: Decodable { let imageUrl: String } } - -// MARK: - Empty Response -struct EmptyResponse: Decodable {} diff --git a/Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/GetAdminPopUpStoreListResponseDTO.swift similarity index 99% rename from Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/GetAdminPopUpStoreListResponseDTO.swift index 8be73a2b..6d14302a 100644 --- a/Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AdminAPI/ResponseDTO/GetAdminPopUpStoreListResponseDTO.swift @@ -1,4 +1,3 @@ - import Foundation // MARK: - Store List Request @@ -10,7 +9,7 @@ struct StoreListRequestDTO: Encodable { enum CodingKeys: String, CodingKey { case query case page - case size + case size } } @@ -62,7 +61,6 @@ struct CreatePopUpStoreRequestDTO: Encodable { } } - // MARK: - Update Store Request struct UpdatePopUpStoreRequestDTO: Encodable { let popUpStore: PopUpStore @@ -123,7 +121,6 @@ struct UpdatePopUpStoreRequestDTO: Encodable { } } - // MARK: - Notice Request struct CreateNoticeRequestDTO: Encodable { let title: String diff --git a/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/AuthAPIEndPoint.swift similarity index 78% rename from Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/AuthAPI/AuthAPIEndPoint.swift index ead5cbee..5d52d9b6 100644 --- a/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/AuthAPIEndPoint.swift @@ -1,16 +1,11 @@ -// -// AuthAPIEndPoint.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation +import Infrastructure + struct AuthAPIEndPoint { - + // MARK: - Auth API - + /// 로그인을 시도합니다. /// - Parameters: /// - userCredential: 사용자 자격 증명 @@ -18,17 +13,17 @@ struct AuthAPIEndPoint { /// - Returns: Endpoint static func auth_tryLogin(with userCredential: Encodable, path: String) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/auth/\(path)", method: .post, bodyParameters: userCredential, headers: ["Content-Type": "application/json"] ) } - + static func postTokenReissue() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/auth/token/reissue", method: .post ) diff --git a/Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/LoginResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/LoginResponseDTO.swift similarity index 89% rename from Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/LoginResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/LoginResponseDTO.swift index 198eccd0..d88322d6 100644 --- a/Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/LoginResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/LoginResponseDTO.swift @@ -1,12 +1,7 @@ -// -// LoginResponseDTO.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation +import DomainInterface + struct LoginResponseDTO: Decodable { var userId: String var grantType: String diff --git a/Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift similarity index 84% rename from Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift index 3640caf4..73ccf5ea 100644 --- a/Poppool/Poppool/Data/Network/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/AuthAPI/ResponseDTO/PostTokenReissueResponseDTO.swift @@ -1,12 +1,7 @@ -// -// PostTokenReissueResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/16/25. -// - import Foundation +import DomainInterface + struct PostTokenReissueResponseDTO: Decodable { var accessToken: String? var refreshToken: String? diff --git a/Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/CategoryAPIEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/CategoryAPIEndpoint.swift new file mode 100644 index 00000000..5ad42b7d --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/CategoryAPIEndpoint.swift @@ -0,0 +1,16 @@ +import Foundation + +import Infrastructure + +struct CategoryAPIEndpoint { + + /// 관심사 목록을 가져옵니다. + /// - Returns: Endpoint + static func getCategoryList() -> Endpoint { + return Endpoint( + baseURL: Secrets.popPoolBaseURL, + path: "/categories", + method: .get + ) + } +} diff --git a/Poppool/Poppool/Data/Network/SignUpAPI/GetCategoryListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/ResponseDTO/GetCategoryListResponseDTO.swift similarity index 56% rename from Poppool/Poppool/Data/Network/SignUpAPI/GetCategoryListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/ResponseDTO/GetCategoryListResponseDTO.swift index b437e186..63e0520b 100644 --- a/Poppool/Poppool/Data/Network/SignUpAPI/GetCategoryListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/CategoryAPI/ResponseDTO/GetCategoryListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetCategoryListResponseDTO.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation +import DomainInterface + // MARK: - GetCategoryListResponseDTO struct GetCategoryListResponseDTO: Codable { let categoryResponseList: [CategoryResponseDTO] @@ -14,12 +9,12 @@ struct GetCategoryListResponseDTO: Codable { // MARK: - InterestResponse struct CategoryResponseDTO: Codable { - let categoryId: Int64 + let categoryId: Int32 let categoryName: String } extension CategoryResponseDTO { - func toDomain() -> Category { - return Category(categoryId: categoryId, category: categoryName) + func toDomain() -> CategoryResponse { + return CategoryResponse(categoryId: Int(categoryId), category: categoryName) } } diff --git a/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/CommentAPI/CommentAPIEndPoint.swift similarity index 73% rename from Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/CommentAPI/CommentAPIEndPoint.swift index fda16452..558686aa 100644 --- a/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/CommentAPI/CommentAPIEndPoint.swift @@ -1,37 +1,32 @@ -// -// CommentAPIEndPoint.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - import Foundation +import Infrastructure + import RxSwift struct CommentAPIEndPoint { - + static func postCommentAdd(request: PostCommentRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/comments", method: .post, bodyParameters: request ) } - + static func deleteComment(request: DeleteCommentRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/comments", method: .delete, queryParameters: request ) } - + static func editComment(request: PutCommentRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/comments", method: .put, bodyParameters: request diff --git a/Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/DeleteCommentRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/DeleteCommentRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/DeleteCommentRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/DeleteCommentRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/PostCommentRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/PostCommentRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/PostCommentRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/PostCommentRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/PutCommentRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/PutCommentRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/CommentAPI/RequestDTO/PutCommentRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/CommentAPI/RequestDTO/PutCommentRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/HomeAPIEndpoint.swift similarity index 67% rename from Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/HomeAPI/HomeAPIEndpoint.swift index 94a63b0c..82dd7985 100644 --- a/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/HomeAPIEndpoint.swift @@ -1,52 +1,47 @@ -// -// HomeRepositoryImpl.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import Foundation +import Infrastructure + struct HomeAPIEndpoint { - + static func fetchHome( - request: SortedRequestDTO + request: HomeSortedRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/home", method: .get, queryParameters: request ) } - + static func fetchPopularPopUp( - request: SortedRequestDTO + request: HomeSortedRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/home/popular/popup-stores", method: .get, queryParameters: request ) } - + static func fetchNewPopUp( - request: SortedRequestDTO + request: HomeSortedRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/home/new/popup-stores", method: .get, queryParameters: request ) } - + static func fetchCustomPopUp( - request: SortedRequestDTO + request: HomeSortedRequestDTO ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/home/custom/popup-stores", method: .get, queryParameters: request diff --git a/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/RequestDTO/HomeSortedRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/RequestDTO/HomeSortedRequestDTO.swift new file mode 100644 index 00000000..1f7b5f72 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/RequestDTO/HomeSortedRequestDTO.swift @@ -0,0 +1,7 @@ +import Foundation + +struct HomeSortedRequestDTO: Encodable { + var page: Int32? + var size: Int32? + var sort: String? +} diff --git a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift similarity index 78% rename from Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift index f1eacb95..6f2841ac 100644 --- a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/BannerPopUpStoreDTO.swift @@ -1,12 +1,7 @@ -// -// BannerPopUpStoreDTO.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import Foundation +import DomainInterface + struct BannerPopUpStoreDTO: Decodable { var id: Int64 var name: String diff --git a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift similarity index 94% rename from Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift index 01660653..351faf9f 100644 --- a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetHomeInfoResponseDTO.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import Foundation +import DomainInterface + struct GetHomeInfoResponseDTO: Decodable { var bannerPopUpStoreList: [BannerPopUpStoreDTO] var nickname: String? @@ -40,4 +35,3 @@ extension GetHomeInfoResponseDTO { ) } } - diff --git a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift similarity index 87% rename from Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift index 830f3423..4cfb0832 100644 --- a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/HomeAPI/ResponseDTO/PopUpStoreResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetHomeInfoDataResponseDTO.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import Foundation +import DomainInterface + struct PopUpStoreResponseDTO: Decodable { let id: Int64 let categoryName: String? diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/FindDirectionEndPoint.swift similarity index 67% rename from Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/MapAPI/FindDirectionEndPoint.swift index 973a81e5..e9680478 100644 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/FindDirectionEndPoint.swift @@ -1,22 +1,16 @@ -// -// FindDirectionEndPoint.swift -// Poppool -// -// Created by 김기현 on 1/23/25. -// - import Foundation +import Infrastructure + struct FindDirectionEndPoint { // MARK: - Direction static func fetchDirection( popUpStoreId: Int64 ) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/popup/\(popUpStoreId)/directions", method: .get ) } } - diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/MapAPIEndpoint.swift similarity index 91% rename from Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/MapAPI/MapAPIEndpoint.swift index e9da01f6..2d3f2f7b 100644 --- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/MapAPIEndpoint.swift @@ -1,11 +1,7 @@ -// -// MapAPIEndpoint.swift -// Poppool -// -// Created by 김기현 on 12/4/24. -// - import Foundation + +import Infrastructure + import Alamofire struct MapAPIEndpoint { @@ -26,7 +22,7 @@ struct MapAPIEndpoint { ) return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/locations/popup-stores", method: .get, queryParameters: params @@ -44,7 +40,7 @@ struct MapAPIEndpoint { ) return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/locations/search", method: .get, queryParameters: params @@ -52,7 +48,6 @@ struct MapAPIEndpoint { } } -// MARK: - Query DTOs struct BoundQueryDTO: Encodable { let northEastLat: Double let northEastLon: Double @@ -84,4 +79,3 @@ struct SearchQueryDTO: Encodable { let query: String let categories: [Int64]? } - diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/GetPopUpDirectionResponseDTO.swift similarity index 65% rename from Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/GetPopUpDirectionResponseDTO.swift index fa31b5d8..15c4c206 100644 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/GetPopUpDirectionResponseDTO.swift @@ -1,12 +1,6 @@ -// -// GetPopUpDirectionResponseDTO.swift -// Poppool -// -// Created by 김기현 on 1/23/25. -// - import Foundation +import DomainInterface struct GetPopUpDirectionResponseDTO: Decodable { let id: Int64 @@ -37,17 +31,3 @@ struct GetPopUpDirectionResponseDTO: Decodable { ) } } - -struct GetPopUpDirectionResponse { - let id: Int64 - let categoryName: String - let name: String - let address: String - let startDate: String - let endDate: String - let latitude: Double - let longitude: Double - let markerId: Int64 - let markerTitle: String - let markerSnippet: String -} diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/MapPopUpStoreDTO.swift similarity index 96% rename from Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/MapPopUpStoreDTO.swift index 1db431dd..c5d4a7c7 100644 --- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/MapAPI/ResponseDTO/MapPopUpStoreDTO.swift @@ -1,5 +1,7 @@ import Foundation +import DomainInterface + struct MapPopUpStoreDTO: Codable { let id: Int64 let categoryName: String @@ -15,7 +17,6 @@ struct MapPopUpStoreDTO: Codable { let mainImageUrl: String? let bookmarkYn: Bool? - // toDomain() 메서드 추가 func toDomain() -> MapPopUpStore { return MapPopUpStore( id: id, @@ -30,11 +31,9 @@ struct MapPopUpStoreDTO: Codable { markerTitle: markerTitle, markerSnippet: markerSnippet, mainImageUrl: mainImageUrl - ) } } - struct GetViewBoundPopUpStoreListResponse: Decodable { let popUpStoreList: [MapPopUpStoreDTO] } diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/PopUpAPIEndPoint.swift similarity index 78% rename from Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/PopUpAPIEndPoint.swift index e163f336..70a968f6 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/PopUpAPIEndPoint.swift @@ -1,55 +1,50 @@ -// -// PopUpAPIEndPoint.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - import Foundation +import Infrastructure + import RxSwift struct PopUpAPIEndPoint { - + static func getClosePopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/popup/closed", method: .get, queryParameters: request ) } - + static func getOpenPopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/popup/open", method: .get, queryParameters: request ) } - + static func getSearchPopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/search/popup-stores", method: .get, queryParameters: request ) } - + static func getPopUpDetail(request: GetPopUpDetailRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/popup/\(request.popUpStoreId)/detail", method: .get, queryParameters: request ) } - + static func getPopUpComment(request: GetPopUpCommentRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/popup/\(request.popUpStoreId)/comments", method: .get, queryParameters: request diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetPopUpCommentRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetPopUpCommentRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetPopUpCommentRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetPopUpCommentRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetPopUpDetailRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetPopUpDetailRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetPopUpDetailRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetPopUpDetailRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetSearchPopUpListRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetSearchPopUpListRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/PopUpAPI/RequestDTO/GetSearchPopUpListRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/RequestDTO/GetSearchPopUpListRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift similarity index 82% rename from Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift index eab1fac5..a57fee94 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetClosePopUpListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetClosePopUpListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - import Foundation +import DomainInterface + struct GetClosePopUpListResponseDTO: Decodable { var closedPopUpStoreList: [PopUpStoreResponseDTO] var loginYn: Bool diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift similarity index 82% rename from Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift index a7194b4b..40d983fc 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetOpenPopUpListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetOpenPopUpListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - import Foundation +import DomainInterface + struct GetOpenPopUpListResponseDTO: Decodable { var openPopUpStoreList: [PopUpStoreResponseDTO] var loginYn: Bool diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift similarity index 74% rename from Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift index fef0aa36..3461f17d 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpCommentResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetPopUpCommentResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import Foundation +import DomainInterface + struct GetPopUpCommentResponseDTO: Decodable { let commentList: [GetPopUpDetailCommentResponseDTO] } diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift similarity index 96% rename from Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift index e08b6338..53ea4dee 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetPopUpDetailResponseDTO.swift @@ -1,11 +1,8 @@ -// -// GetPopUpDetailResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - import Foundation + +import DomainInterface +import Infrastructure + // MARK: - Main Model struct GetPopUpDetailResponseDTO: Decodable { let name: String? diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift similarity index 76% rename from Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift index b3deaf5f..841ca801 100644 --- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetSearchPopUpListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - import Foundation +import DomainInterface + struct GetSearchPopUpListResponseDTO: Decodable { var popUpStoreList: [PopUpStoreResponseDTO] var loginYn: Bool @@ -17,5 +12,3 @@ extension GetSearchPopUpListResponseDTO { return .init(popUpStoreList: popUpStoreList.map { $0.toDomain() }, loginYn: loginYn) } } - - diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/PreSignedAPIEndPoint.swift similarity index 61% rename from Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/PreSignedAPIEndPoint.swift index a1bba549..47bd16a8 100644 --- a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/PreSignedAPIEndPoint.swift @@ -1,17 +1,12 @@ -// -// PreSignedAPIEndPoint.swift -// Poppool -// -// Created by SeoJunYoung on 11/29/24. -// - import Foundation +import Infrastructure + struct PreSignedAPIEndPoint { static func presigned_upload(request: PresignedURLRequestDTO) -> Endpoint { - Logger.log(message: "Presigned URL 생성 - Request: \(request)", category: .debug) + Logger.log("Presigned URL 생성 - Request: \(request)", category: .debug) return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/files/upload-preSignedUrl", method: .post, bodyParameters: request @@ -19,9 +14,9 @@ struct PreSignedAPIEndPoint { } static func presigned_download(request: PresignedURLRequestDTO) -> Endpoint { - Logger.log(message: "Presigned Download URL 생성 - Request: \(request)", category: .debug) + Logger.log("Presigned Download URL 생성 - Request: \(request)", category: .debug) return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/files/download-preSignedUrl", method: .post, bodyParameters: request @@ -29,9 +24,9 @@ struct PreSignedAPIEndPoint { } static func presigned_delete(request: PresignedURLRequestDTO) -> RequestEndpoint { - Logger.log(message: "Presigned Delete 생성 - Request: \(request)", category: .debug) + Logger.log("Presigned Delete 생성 - Request: \(request)", category: .debug) return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/files/delete", method: .post, bodyParameters: request diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PresignedURLRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/RequestDTO/PresignedURLRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Infrastructure/PreSignedService/PresignedURLRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/RequestDTO/PresignedURLRequestDTO.swift diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedURLDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/ResponseDTO/PreSignedURLDTO.swift similarity index 100% rename from Poppool/Poppool/Infrastructure/PreSignedService/PreSignedURLDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/ResponseDTO/PreSignedURLDTO.swift diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedURLResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/ResponseDTO/PreSignedURLResponseDTO.swift similarity index 100% rename from Poppool/Poppool/Infrastructure/PreSignedService/PreSignedURLResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/PreSignedAPI/ResponseDTO/PreSignedURLResponseDTO.swift diff --git a/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/RequestDTO/GetSearchPopupStoreRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/RequestDTO/GetSearchPopupStoreRequestDTO.swift new file mode 100644 index 00000000..679b1e63 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/RequestDTO/GetSearchPopupStoreRequestDTO.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GetSearchPopupStoreRequestDTO: Encodable { + let query: String +} diff --git a/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/ResponseDTO/GetSearchPopupStoreResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/ResponseDTO/GetSearchPopupStoreResponseDTO.swift new file mode 100644 index 00000000..2872bf8a --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/ResponseDTO/GetSearchPopupStoreResponseDTO.swift @@ -0,0 +1,17 @@ +import Foundation + +import DomainInterface + +struct GetSearchPopupStoreResponseDTO: Decodable { + var popUpStoreList: [PopUpStoreResponseDTO] + var loginYn: Bool +} + +extension GetSearchPopupStoreResponseDTO { + func toDomain() -> KeywordBasePopupStoreListResponse { + return KeywordBasePopupStoreListResponse( + popupStoreList: popUpStoreList.map { $0.toDomain() }, + loginYn: loginYn + ) + } +} diff --git a/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/SearchAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/SearchAPIEndPoint.swift new file mode 100644 index 00000000..9235420d --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/SearchAPI/SearchAPIEndPoint.swift @@ -0,0 +1,17 @@ +import Foundation + +import Infrastructure + +import RxSwift + +struct SearchAPIEndPoint { + + static func getSearchPopUpList(request: GetSearchPopupStoreRequestDTO) -> Endpoint { + return Endpoint( + baseURL: Secrets.popPoolBaseURL, + path: "/search/popup-stores", + method: .get, + queryParameters: request + ) + } +} diff --git a/Poppool/Poppool/Data/Network/SignUpAPI/CheckNickNameRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/RequestDTO/CheckNickNameRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/SignUpAPI/CheckNickNameRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/RequestDTO/CheckNickNameRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/RequestDTO/SignUpRequestDTO.swift similarity index 89% rename from Poppool/Poppool/Data/Network/SignUpAPI/SignUpRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/RequestDTO/SignUpRequestDTO.swift index 74cd1019..044c86ff 100644 --- a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpRequestDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/RequestDTO/SignUpRequestDTO.swift @@ -13,6 +13,6 @@ struct SignUpRequestDTO: Encodable { var age: Int32 var socialEmail: String var socialType: String - var interestCategories: [Int64] + var interestCategories: [Int] var appleAuthorizationCode: String? } diff --git a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/SignUpAPIEndpoint.swift similarity index 80% rename from Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/SignUpAPIEndpoint.swift index dc29870e..36527751 100644 --- a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/SignUpAPI/SignUpAPIEndpoint.swift @@ -1,42 +1,37 @@ -// -// SignUpAPIEndpoint.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation +import Infrastructure + struct SignUpAPIEndpoint { - + /// 닉네임 중복을 확인합니다. /// - Parameter request: 닉네임 체크 요청 DTO /// - Returns: Endpoint static func signUp_checkNickName(with request: CheckNickNameRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/signup/check-nickname", method: .get, queryParameters: request ) } - + /// 관심사 목록을 가져옵니다. /// - Returns: Endpoint static func signUp_getCategoryList() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/signup/categories", method: .get ) } - + /// 회원가입을 시도합니다. /// - Parameter request: 회원가입 요청 DTO /// - Returns: RequestEndpoint static func signUp_trySignUp(with request: SignUpRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/signup", method: .post, bodyParameters: request diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/CommentLikeRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/CommentLikeRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/CommentLikeRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/CommentLikeRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetMyCommentRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/GetMyCommentRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetMyCommentRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/GetMyCommentRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetOtherUserCommentListRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/GetOtherUserCommentListRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetOtherUserCommentListRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/GetOtherUserCommentListRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PostBookmarkPopUpRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PostBookmarkPopUpRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PostBookmarkPopUpRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PostBookmarkPopUpRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PostUserBlockRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PostUserBlockRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PostUserBlockRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PostUserBlockRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserCategoryRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserCategoryRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserCategoryRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserCategoryRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserProfileRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserProfileRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserProfileRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserProfileRequestDTO.swift diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserTailoredInfoRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserTailoredInfoRequestDTO.swift similarity index 100% rename from Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/PutUserTailoredInfoRequestDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/PutUserTailoredInfoRequestDTO.swift diff --git a/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/UserSortedRequestDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/UserSortedRequestDTO.swift new file mode 100644 index 00000000..aee72ce5 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/RequesetDTO/UserSortedRequestDTO.swift @@ -0,0 +1,7 @@ +import Foundation + +struct UserSortedRequestDTO: Encodable { + var page: Int32? + var size: Int32? + var sort: String? +} diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift similarity index 89% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift index 7d862c5d..a375ca41 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetBlockUserListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetBlockUserListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import Foundation +import DomainInterface + struct GetBlockUserListResponseDTO: Decodable { var blockedUserInfoList: [GetBlockUserListDataResponseDTO] var totalPages: Int32 diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift similarity index 91% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift index 26323fbd..5d6591e4 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift @@ -1,13 +1,7 @@ -// -// GetMyCommentedPopUpResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - - import Foundation +import DomainInterface + struct GetMyCommentedPopUpResponseDTO: Decodable { var popUpInfoList: [GetMyCommentedPopUpDataResponseDTO] } diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift similarity index 91% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift index 11f511c0..64a1f071 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyPageResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetMyPageResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/30/24. -// - import Foundation +import DomainInterface + struct GetMyPageResponseDTO: Decodable { var nickname: String? var profileImageUrl: String? diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift similarity index 52% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift index 02b18df9..1ffb4133 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetMyProfileResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import Foundation +import DomainInterface + struct GetMyProfileResponseDTO: Decodable { var profileImageUrl: String? var nickname: String? @@ -20,6 +15,15 @@ struct GetMyProfileResponseDTO: Decodable { extension GetMyProfileResponseDTO { func toDomain() -> GetMyProfileResponse { - return .init(profileImageUrl: profileImageUrl, nickname: nickname, email: email, instagramId: instagramId, intro: intro, gender: gender, age: age, interestCategoryList: interestCategoryList.map { $0.toDomain() }) + return .init( + profileImageUrl: profileImageUrl, + nickname: nickname, + email: email, + instagramId: instagramId, + intro: intro, + gender: gender, + age: age, + interestCategoryList: interestCategoryList.map { $0.toDomain() } + ) } } diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift similarity index 79% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift index d8e9baba..f50ff39b 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeDetailResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetNoticeDetailResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import Foundation +import DomainInterface + struct GetNoticeDetailResponseDTO: Decodable { var id: Int64 var title: String? diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift similarity index 86% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift index f2314de9..c280a0a7 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetNoticeListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetNoticeListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import Foundation +import DomainInterface + struct GetNoticeListResponseDTO: Decodable { var noticeInfoList: [GetNoticeListDataResponseDTO] } diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift similarity index 90% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift index 45948742..382485bc 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetOtherUserCommentedPopUpListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import Foundation +import DomainInterface + struct GetOtherUserCommentedPopUpListResponseDTO: Decodable { var popUpInfoList: [GetOtherUserCommentedPopUpResponseDTO] } @@ -42,6 +37,3 @@ extension GetOtherUserCommentedPopUpResponseDTO { ) } } - - - diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift similarity index 92% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift index 20b4f454..8d02dc95 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetRecentPopUpResponseDTO.swift @@ -1,12 +1,8 @@ -// -// GetRecentPopUpResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import Foundation +import DomainInterface +import Infrastructure + struct GetRecentPopUpResponseDTO: Decodable { var popUpInfoList: [GetRecentPopUpDataResponseDTO] var totalPages: Int32 diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift similarity index 87% rename from Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift index f50372d7..d1c01819 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift @@ -1,12 +1,7 @@ -// -// GetWithdrawlListResponseDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import Foundation +import DomainInterface + struct GetWithdrawlListResponseDTO: Decodable { var withDrawlSurveyList: [GetWithdrawlListDataResponseDTO] } @@ -26,8 +21,6 @@ extension GetWithdrawlListDataResponseDTO { } } - - struct PostWithdrawlListRequestDTO: Encodable { var checkedSurveyList: [GetWithdrawlListDataResponseDTO] } diff --git a/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/UserAPIEndPoint.swift similarity index 71% rename from Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/API/UserAPI/UserAPIEndPoint.swift index fce30a7d..d063d383 100644 --- a/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/API/UserAPI/UserAPIEndPoint.swift @@ -1,193 +1,188 @@ -// -// UserAPIEndPoint.swift -// Poppool -// -// Created by SeoJunYoung on 12/3/24. -// - import Foundation +import Infrastructure + import RxSwift struct UserAPIEndPoint { - + static func postBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/bookmark-popupstores", method: .post, queryParameters: request ) } - + static func deleteBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/bookmark-popupstores", method: .delete, queryParameters: request ) } - + static func postCommentLike(request: CommentLikeRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/likes", method: .post, queryParameters: request ) } - + static func deleteCommentLike(request: CommentLikeRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/likes", method: .delete, queryParameters: request ) } - + static func postUserBlock(request: PostUserBlockRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/block", method: .post, queryParameters: request ) } - + static func deleteUserBlock(request: PostUserBlockRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/unblock", method: .delete, queryParameters: request ) } - + static func getOtherUserCommentPopUpList(request: GetOtherUserCommentListRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/\(request.commenterId ?? "")/comments", method: .get, queryParameters: request ) } - + static func getMyPage() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/my-page", method: .get ) } - + static func postLogout() -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/logout", method: .post ) } - + static func getWithdrawlList() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/withdrawl/surveys", method: .get ) } - + static func postWithdrawl(request: PostWithdrawlListRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/delete", method: .post, bodyParameters: request ) } - + static func getMyProfile() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/profiles", method: .get ) } - + static func putUserTailoredInfo(request: PutUserTailoredInfoRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/tailored-info", method: .put, bodyParameters: request ) - } - + } + static func putUserCategory(request: PutUserCategoryRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/interests", method: .put, bodyParameters: request ) - } - + } + static func putUserProfile(request: PutUserProfileRequestDTO) -> RequestEndpoint { return RequestEndpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/profiles", method: .put, bodyParameters: request ) } - - static func getMyCommentedPopUp(request: SortedRequestDTO) -> Endpoint { + + static func getMyCommentedPopUp(request: UserSortedRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/commented/popup", method: .get, queryParameters: request ) } - - static func getBlockUserList(request: GetBlockUserListRequestDTO) -> Endpoint { + + static func getBlockUserList(request: UserSortedRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/blocked", method: .get, queryParameters: request ) } - + static func getNoticeList() -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/notice/list", method: .get ) } - + static func getNoticeDetail(noticeID: Int64) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/notice/\(noticeID)", method: .get ) - } - - static func getRecentPopUp(request: SortedRequestDTO) -> Endpoint { + } + + static func getRecentPopUp(request: UserSortedRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/recent-popupstores", method: .get, queryParameters: request ) } - - static func getBookmarkPopUp(request: SortedRequestDTO) -> Endpoint { + + static func getBookmarkPopUp(request: UserSortedRequestDTO) -> Endpoint { return Endpoint( - baseURL: Secrets.popPoolBaseUrl.rawValue, + baseURL: Secrets.popPoolBaseURL, path: "/users/bookmark-popupstores", method: .get, queryParameters: request diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/NetworkError.swift b/Poppool/DataLayer/Data/Data/Network/Common/NetworkError.swift similarity index 100% rename from Poppool/Poppool/Infrastructure/NetworkLayer/Common/NetworkError.swift rename to Poppool/DataLayer/Data/Data/Network/Common/NetworkError.swift diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift b/Poppool/DataLayer/Data/Data/Network/Common/Requestable.swift similarity index 91% rename from Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift rename to Poppool/DataLayer/Data/Data/Network/Common/Requestable.swift index cd902b29..2172c43b 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift +++ b/Poppool/DataLayer/Data/Data/Network/Common/Requestable.swift @@ -1,15 +1,10 @@ -// -// Requestable.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/16/24. -// - import Foundation +import Infrastructure + import Alamofire -protocol Requestable { +public protocol Requestable { var baseURL: String { get } var path: String { get } var method: HTTPMethod { get } @@ -24,14 +19,12 @@ extension Requestable { /// - Returns: URLRequest 반환 func getUrlRequest() throws -> URLRequest { let url = try url() - + Logger.log( - message: "\(url) URL 생성", - category: .network, - fileName: #file, - line: #line + "\(url) URL 생성", + category: .network ) - + var urlRequest = URLRequest(url: url) // httpBody if let bodyParameters = try bodyParameters?.toDictionary() { @@ -46,7 +39,7 @@ extension Requestable { headers?.forEach { urlRequest.setValue($1, forHTTPHeaderField: $0) } return urlRequest } - + /// APIEndpoint에서 전달받은 DTO를 URL로 변환하는 메서드 /// - Returns: URL 반환 func url() throws -> URL { @@ -80,7 +73,7 @@ extension Requestable { } extension Encodable { - + /// URL에 요청할 쿼리 데이터를 JSON 형식에 맞게 딕셔너리 구조로 변환하는 메서드 /// - Returns: jsonData func toDictionary() throws -> [String: Any]? { diff --git a/Poppool/DataLayer/Data/Data/Network/Common/Responsable.swift b/Poppool/DataLayer/Data/Data/Network/Common/Responsable.swift new file mode 100644 index 00000000..9f7c310c --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/Common/Responsable.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol Responsable { + associatedtype Response +} diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift b/Poppool/DataLayer/Data/Data/Network/EndPoint/Endpoint.swift similarity index 82% rename from Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift rename to Poppool/DataLayer/Data/Data/Network/EndPoint/Endpoint.swift index cdbe56d6..4e835db9 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/EndPoint/Endpoint.swift @@ -1,19 +1,12 @@ -// -// Endpoint.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/16/24. -// - import Foundation import Alamofire -protocol RequesteResponsable: Requestable, Responsable where Response: Decodable {} +public protocol RequesteResponsable: Requestable, Responsable where Response: Decodable {} class Endpoint: RequesteResponsable { typealias Response = R - + var baseURL: String var path: String var method: HTTPMethod diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift b/Poppool/DataLayer/Data/Data/Network/EndPoint/MultipartEndPoint.swift similarity index 83% rename from Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift rename to Poppool/DataLayer/Data/Data/Network/EndPoint/MultipartEndPoint.swift index 42d83dd0..4c9e7011 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift +++ b/Poppool/DataLayer/Data/Data/Network/EndPoint/MultipartEndPoint.swift @@ -1,15 +1,10 @@ -// -// MultipartEndPoint.swift -// MomsVillage -// -// Created by SeoJunYoung on 10/25/24. -// - import UIKit +import Infrastructure + import Alamofire -class MultipartEndPoint: URLRequestConvertible { +public class MultipartEndPoint: URLRequestConvertible { var baseURL: String var path: String var method: HTTPMethod @@ -17,7 +12,7 @@ class MultipartEndPoint: URLRequestConvertible { var jsonData: [String: Any]? var images: [UIImage] var headers: [String: String]? - + init( baseURL: String, path: String, @@ -35,22 +30,22 @@ class MultipartEndPoint: URLRequestConvertible { self.images = images self.headers = headers } - - func asURLRequest() throws -> URLRequest { + + public func asURLRequest() throws -> URLRequest { let url = try baseURL.asURL().appendingPathComponent(path) var request = URLRequest(url: url) - Logger.log(message: "\(request) URL 생성", category: .network) + Logger.log("\(request) URL 생성", category: .network) request.method = method - + if let headers = headers { for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } } - + return request } - + func asMultipartFormData(multipartFormData: MultipartFormData) { // JSON 데이터를 data 필드로 추가 if let jsonData = jsonData { @@ -62,10 +57,10 @@ class MultipartEndPoint: URLRequestConvertible { multipartFormData.append(jsonString.data(using: .utf8)!, withName: "data") } } catch { - Logger.log(message: "JSON 변환 오류: \(error)", category: .network) + Logger.log("JSON 변환 오류: \(error)", category: .network) } } - + // 이미지 파일 추가 for (index, image) in images.enumerated() { if let imageData = image.jpegData(compressionQuality: 0.8) { diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/RequestEndpoint.swift b/Poppool/DataLayer/Data/Data/Network/EndPoint/RequestEndpoint.swift similarity index 100% rename from Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/RequestEndpoint.swift rename to Poppool/DataLayer/Data/Data/Network/EndPoint/RequestEndpoint.swift diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift b/Poppool/DataLayer/Data/Data/Network/Interceptor/TokenInterceptor.swift similarity index 77% rename from Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift rename to Poppool/DataLayer/Data/Data/Network/Interceptor/TokenInterceptor.swift index 9b4d8c55..2f1c68dc 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift +++ b/Poppool/DataLayer/Data/Data/Network/Interceptor/TokenInterceptor.swift @@ -1,22 +1,18 @@ -// -// TokenInterceptor.swift -// MomsVillage -// -// Created by SeoJunYoung on 10/14/24. -// - import Foundation + +import Infrastructure + import Alamofire import RxSwift final class TokenInterceptor: RequestInterceptor { - + func adapt( _ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - Logger.log(message: "TokenInterceptor Adapt Token", category: .network) - let keyChainService = KeyChainService() + Logger.log("TokenInterceptor Adapt Token", category: .network) + @Dependency var keyChainService: KeyChainService var urlRequest = urlRequest let accessTokenResult = keyChainService.fetchToken(type: .accessToken) switch accessTokenResult { @@ -29,14 +25,14 @@ final class TokenInterceptor: RequestInterceptor { completion(.success(urlRequest)) } } - + func retry( _ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void ) { - Logger.log(message: "TokenInterceptor Retry Start", category: .network) + Logger.log("TokenInterceptor Retry Start", category: .network) completion(.doNotRetry) } } diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift b/Poppool/DataLayer/Data/Data/Network/Provider/Provider.swift similarity index 91% rename from Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift rename to Poppool/DataLayer/Data/Data/Network/Provider/Provider.swift index edc12bfd..5567cb93 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift +++ b/Poppool/DataLayer/Data/Data/Network/Provider/Provider.swift @@ -1,17 +1,10 @@ -// -// Provider.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/16/24. -// - import Foundation -import RxSwift import Alamofire +import RxSwift + +public protocol Provider { -protocol Provider { - /// 네트워크 요청을 수행하고 결과를 반환하는 메서드 /// - Parameters: /// - endpoint: 요청할 엔드포인트 @@ -22,7 +15,7 @@ protocol Provider { with endpoint: E, interceptor: RequestInterceptor? ) -> Observable where R == E.Response - + /// 네트워크 요청을 수행하고 결과를 반환하는 메서드 /// - Parameters: /// - request: 요청할 Requestable 객체 @@ -33,7 +26,7 @@ protocol Provider { with request: E, interceptor: RequestInterceptor? ) -> Completable - + /// 이미지와 데이터를 `multipart/form-data`로 업로드하는 메서드 func uploadImages( with request: MultipartEndPoint, diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift b/Poppool/DataLayer/Data/Data/Network/Provider/ProviderImpl.swift similarity index 67% rename from Poppool/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift rename to Poppool/DataLayer/Data/Data/Network/Provider/ProviderImpl.swift index efa81ca6..e46126b9 100644 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift +++ b/Poppool/DataLayer/Data/Data/Network/Provider/ProviderImpl.swift @@ -1,20 +1,18 @@ -// -// ProviderImpl.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/16/24. -// - import Foundation -import RxSwift + +import Infrastructure + import Alamofire +import RxSwift -final class ProviderImpl: Provider { +public final class ProviderImpl: Provider { private let disposeBag = DisposeBag() var timeoutTimer: Timer? - func requestData( + public init(timeoutTimer: Timer? = nil) { self.timeoutTimer = timeoutTimer } + + public func requestData( with endpoint: E, interceptor: RequestInterceptor? = nil ) -> Observable where R == E.Response { @@ -24,22 +22,11 @@ final class ProviderImpl: Provider { /// 1) endpoint -> urlRequest 생성 let urlRequest = try endpoint.getUrlRequest() - Logger.log( - message: """ - [Provider] 최종 요청 URL: - - URL: \(urlRequest.url?.absoluteString ?? "URL이 없습니다.") - - Method: \(urlRequest.httpMethod ?? "알 수 없음") - - Headers: \(urlRequest.allHTTPHeaderFields ?? [:]) - 요청 시각: \(Date()) - """, - category: .debug - ) - let request = AF.request(urlRequest, interceptor: interceptor) .validate() .responseData { [weak self] response in Logger.log( - message: """ + """ [Provider] 응답 수신: - URL: \(urlRequest.url?.absoluteString ?? "URL이 없습니다.") - 응답 시각: \(Date()) @@ -50,9 +37,13 @@ final class ProviderImpl: Provider { case .success(let data): // 빈 응답 처리 if R.self == EmptyResponse.self && data.isEmpty { - observer.onNext(EmptyResponse() as! R) - observer.onCompleted() - return + if let response = EmptyResponse() as? R { + observer.onNext(response) + observer.onCompleted() + return + } else { + observer.onError(NetworkError.decodeError) + } } do { // JSON 디코딩 @@ -61,14 +52,14 @@ final class ProviderImpl: Provider { observer.onCompleted() } catch { Logger.log( - message: "디코딩 실패: \(error.localizedDescription)", + "디코딩 실패: \(error.localizedDescription)", category: .error ) observer.onError(NetworkError.decodeError) } case .failure(let error): - Logger.log(message: "요청 실패 Error:\(error)", category: .error) + Logger.log("요청 실패 Error:\(error)", category: .error) observer.onError(error) } } @@ -78,14 +69,14 @@ final class ProviderImpl: Provider { } } catch { - Logger.log(message: "[Provider] URLRequest 생성 실패: \(error.localizedDescription)", category: .error) + Logger.log("[Provider] URLRequest 생성 실패: \(error.localizedDescription)", category: .error) observer.onError(NetworkError.urlRequest(error)) return Disposables.create() } } } - func request( + public func request( with request: E, interceptor: RequestInterceptor? = nil ) -> Completable { @@ -99,19 +90,9 @@ final class ProviderImpl: Provider { do { let urlRequest = try request.getUrlRequest() - Logger.log( - message: """ - [Provider] 최종 요청 URL(Completable): - - URL: \(urlRequest.url?.absoluteString ?? "URL이 없습니다.") - - Method: \(urlRequest.httpMethod ?? "알 수 없음") - 요청 시각: \(Date()) - """, - category: .debug - ) - self.executeRequest(urlRequest, interceptor: interceptor) { response in Logger.log( - message: "응답 시각 :\(Date())", + "응답 시각 :\(Date())", category: .network ) @@ -121,7 +102,7 @@ final class ProviderImpl: Provider { accessToken = accessToken.replacingOccurrences(of: "Bearer ", with: "") refreshToken = refreshToken.replacingOccurrences(of: "Bearer ", with: "") - let keyChainService = KeyChainService() + @Dependency var keyChainService: KeyChainService keyChainService.saveToken(type: .accessToken, value: accessToken) keyChainService.saveToken(type: .refreshToken, value: refreshToken) } @@ -130,12 +111,12 @@ final class ProviderImpl: Provider { case .success: observer(.completed) case .failure(let error): - Logger.log(message: "요청 실패 Error:\(error)", category: .error) + Logger.log("요청 실패 Error:\(error)", category: .error) observer(.error(self.handleRequestError(response: response, error: error))) } } } catch { - Logger.log(message: "[Provider] URLRequest 생성 실패 (Completable): \(error.localizedDescription)", category: .error) + Logger.log("[Provider] URLRequest 생성 실패 (Completable): \(error.localizedDescription)", category: .error) observer(.error(NetworkError.urlRequest(error))) } @@ -144,7 +125,7 @@ final class ProviderImpl: Provider { } // multipart 업로드는 기존 코드와 동일 - func uploadImages( + public func uploadImages( with request: MultipartEndPoint, interceptor: RequestInterceptor? = nil ) -> Completable { @@ -156,23 +137,15 @@ final class ProviderImpl: Provider { do { let urlRequest = try request.asURLRequest() - Logger.log( - message: """ - [Provider] 이미지 업로드 요청: - - URL: \(urlRequest.url?.absoluteString ?? "URL이 없습니다.") - - Method: \(urlRequest.httpMethod ?? "알 수 없음") - """, - category: .network - ) AF.upload(multipartFormData: { multipartFormData in request.asMultipartFormData(multipartFormData: multipartFormData) - Logger.log(message: "업로드 시각 :\(Date())", category: .network) + Logger.log("업로드 시각 :\(Date())", category: .network) }, with: urlRequest, interceptor: interceptor) .validate() .response { response in Logger.log( - message: "이미지 업로드 응답 시각 :\(Date())", + "이미지 업로드 응답 시각 :\(Date())", category: .network ) switch response.result { @@ -198,15 +171,6 @@ private extension ProviderImpl { interceptor: RequestInterceptor?, completion: @escaping (AFDataResponse) -> Void ) { - // 여기서도 최종 URL 찍을 수 있음 - Logger.log( - message: """ - [Provider] executeRequest: - - URL: \(urlRequest.url?.absoluteString ?? "URL이 없습니다.") - 요청 시각: \(Date()) - """, - category: .debug - ) AF.request(urlRequest, interceptor: interceptor) .validate() diff --git a/Poppool/Poppool/Infrastructure/AppleLoginService.swift b/Poppool/DataLayer/Data/Data/Network/Service/AppleLoginService.swift similarity index 68% rename from Poppool/Poppool/Infrastructure/AppleLoginService.swift rename to Poppool/DataLayer/Data/Data/Network/Service/AppleLoginService.swift index e58e20b5..a119d861 100644 --- a/Poppool/Poppool/Infrastructure/AppleLoginService.swift +++ b/Poppool/DataLayer/Data/Data/Network/Service/AppleLoginService.swift @@ -1,29 +1,27 @@ -// -// AppleLoginService.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/20/24. -// +import DomainInterface +import Infrastructure -import RxSwift import AuthenticationServices +import RxSwift + +public final class AppleLoginService: NSObject, AuthServiceable { + + public override init() { } -final class AppleLoginService: NSObject, AuthServiceable { - // 사용자 자격 증명 정보를 방출할 subject private var authServiceResponse: PublishSubject = .init() - + func fetchUserCredential() -> Observable { performRequest() return authServiceResponse } - + // Apple 인증 요청을 수행하는 함수 private func performRequest() { let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] - + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.presentationContextProvider = self @@ -34,23 +32,21 @@ final class AppleLoginService: NSObject, AuthServiceable { extension AppleLoginService: ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate { // 인증 컨트롤러의 프레젠테이션 앵커를 반환하는 함수 - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { let scenes = UIApplication.shared.connectedScenes let windowSecne = scenes.first as? UIWindowScene guard let window = windowSecne?.windows.first else { Logger.log( - message: "\(#function) UIWindow fetch Fail", - category: .error, - fileName: #file, - line: #line + "\(#function) UIWindow fetch Fail", + category: .error ) return UIWindow() } return window } - + // 인증 성공 시 호출되는 함수 - func authorizationController( + public func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { @@ -58,49 +54,36 @@ extension AppleLoginService: ASAuthorizationControllerPresentationContextProvidi case let appleIDCredential as ASAuthorizationAppleIDCredential: guard let idToken = appleIDCredential.identityToken else { // 토큰이 없는 경우 오류 방출 - Logger.log( - message: "AppleLogin Token is Not Found", - category: .error, - fileName: #file, - line: #line - ) - authServiceResponse.onError(AuthError.unknownError) + authServiceResponse.onError(AuthError.unknownError(description: "AppleLogin Token is Not Found")) return } guard let idToken = String(data: idToken, encoding: .utf8) else { - Logger.log( - message: "AppleLogin Token Convert Fail", - category: .error, - fileName: #file, - line: #line - ) - authServiceResponse.onError(AuthError.unknownError) + // 토큰 convert가 실패할 경우 오류 방출 + authServiceResponse.onError(AuthError.unknownError(description: "AppleLogin Token Convert Fail")) return } guard let authorizationCode = appleIDCredential.authorizationCode else { return } - + guard let convertAuthorizationCode = String(data: authorizationCode, encoding: .utf8) else { return } - Logger.log(message: "IDToken: \(idToken)", category: .info) - Logger.log(message: "Auth Code: \(convertAuthorizationCode)", category: .info) + Logger.log("IDToken: \(idToken)", category: .info) + Logger.log("Auth Code: \(convertAuthorizationCode)", category: .info) authServiceResponse.onNext(.init(idToken: idToken, authorizationCode: convertAuthorizationCode)) default: break } } // 인증 실패 시 호출되는 함수 - func authorizationController( + public func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error ) { Logger.log( - message: "AppleLogin Fail", - category: .error, - fileName: #file, - line: #line + "AppleLogin Fail", + category: .error ) authServiceResponse.onError(error) } diff --git a/Poppool/Poppool/Infrastructure/AuthServiceable.swift b/Poppool/DataLayer/Data/Data/Network/Service/AuthServiceable.swift similarity index 50% rename from Poppool/Poppool/Infrastructure/AuthServiceable.swift rename to Poppool/DataLayer/Data/Data/Network/Service/AuthServiceable.swift index 79c2330e..ca1cc728 100644 --- a/Poppool/Poppool/Infrastructure/AuthServiceable.swift +++ b/Poppool/DataLayer/Data/Data/Network/Service/AuthServiceable.swift @@ -1,11 +1,6 @@ -// -// AuthService.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/22/24. -// +import Foundation -import UIKit +import DomainInterface import RxSwift @@ -15,14 +10,7 @@ protocol AuthServiceable: AnyObject { func fetchUserCredential() -> Observable } -struct AuthServiceResponse: Encodable { - var idToken: String? - var authorizationCode: String? - var kakaoUserId: Int64? - var kakaoAccessToken: String? -} - enum AuthError: Error { case notInstalled - case unknownError + case unknownError(description: String?) } diff --git a/Poppool/DataLayer/Data/Data/Network/Service/KakaoLoginService.swift b/Poppool/DataLayer/Data/Data/Network/Service/KakaoLoginService.swift new file mode 100644 index 00000000..4427a467 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/Network/Service/KakaoLoginService.swift @@ -0,0 +1,104 @@ +import DomainInterface +import Infrastructure + +import KakaoSDKAuth +import KakaoSDKUser +import RxSwift + +public final class KakaoLoginService: AuthServiceable { + + public init() { } + + var disposeBag = DisposeBag() + + func unlink() -> Observable { + return Observable.create { observer in + UserApi.shared.unlink { error in + if let error = error { + observer.onNext(()) + Logger.log(error.localizedDescription, category: .error) + } else { + observer.onNext(()) + observer.onCompleted() + } + } + + return Disposables.create() + } + } + + public func fetchUserCredential() -> Observable { + return Observable.create { [weak self] observer in + guard let self else { + Logger.log( + "KakaoTalk login Error", + category: .error + ) + return Disposables.create() + } + + // 카카오톡 설치 유무 확인 + guard UserApi.isKakaoTalkLoginAvailable() else { + Logger.log( + "KakaoTalk is not install", + category: .error + ) + + // 카카오톡 미설치시 웹으로 인증 시도 + loginWithKakaoTalkWeb(observer: observer) + return Disposables.create() + } + + // 카카오톡 설치시 앱으로 인증 시도 + loginWithKakaoTalkApp(observer: observer) + + return Disposables.create() + } + } +} + +private extension KakaoLoginService { + + /// 제공된 액세스 토큰을 사용하여 사용자의 카카오 ID를 가져옵니다. + /// - Parameters: + /// - observer: 인증 응답을 처리할 옵저버. + /// - accessToken: 카카오 로그인 과정에서 얻은 액세스 토큰. + func fetchUserId(observer: AnyObserver, accessToken: String) { + UserApi.shared.me { user, error in + if let error = error { + observer.onError(AuthError.unknownError(description: error.localizedDescription)) + } else { + observer.onNext(.init(kakaoUserId: user?.id, kakaoAccessToken: accessToken)) + observer.onCompleted() + } + } + } + + /// 카카오톡 앱을 사용하여 로그인하고 액세스 토큰을 가져옵니다. + /// - Parameter observer: 인증 응답을 처리할 옵저버. + func loginWithKakaoTalkApp(observer: AnyObserver) { + UserApi.shared.loginWithKakaoTalk { [weak self] oauthToken, error in + if let error = error { + observer.onError(AuthError.unknownError(description: error.localizedDescription)) + } else { + if let accessToken = oauthToken?.accessToken { + self?.fetchUserId(observer: observer, accessToken: accessToken) + } + } + } + } + + /// 카카오톡 웹을 사용하여 로그인하고 액세스 토큰을 가져옵니다. + /// - Parameter observer: 인증 응답을 처리할 옵저버. + func loginWithKakaoTalkWeb(observer: AnyObserver) { + UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in + if let error = error { + observer.onError(AuthError.unknownError(description: error.localizedDescription)) + } else { + if let accessToken = oauthToken?.accessToken { + self?.fetchUserId(observer: observer, accessToken: accessToken) + } + } + } + } +} diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift b/Poppool/DataLayer/Data/Data/Network/Service/PreSignedService.swift similarity index 77% rename from Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift rename to Poppool/DataLayer/Data/Data/Network/Service/PreSignedService.swift index 73343587..017c76c4 100644 --- a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift +++ b/Poppool/DataLayer/Data/Data/Network/Service/PreSignedService.swift @@ -1,16 +1,10 @@ -// -// PreSignedService.swift -// PopPool -// -// Created by SeoJunYoung on 9/5/24. -// - -import Foundation import UIKit -import RxSwift -import RxCocoa +import Infrastructure + import Alamofire +import RxCocoa +import RxSwift class ImageCache { static let shared = NSCache() @@ -24,9 +18,9 @@ class PreSignedService { } let tokenInterceptor = TokenInterceptor() - - let provider = ProviderImpl() - + + @Dependency private var provider: Provider + let disposeBag = DisposeBag() func tryDelete(targetPaths: PresignedURLRequestDTO) -> Completable { @@ -35,20 +29,20 @@ class PreSignedService { } func tryUpload(datas: [PresignedURLRequest]) -> Single { - Logger.log(message: "tryUpload 호출됨 - 요청 데이터 수: \(datas.count)", category: .debug) + Logger.log("tryUpload 호출됨 - 요청 데이터 수: \(datas.count)", category: .debug) return Single.create { [weak self] observer in - Logger.log(message: "tryUpload 내부 흐름 시작", category: .debug) + Logger.log("tryUpload 내부 흐름 시작", category: .debug) guard let self = self else { - Logger.log(message: "self가 nil입니다. 작업을 중단합니다.", category: .error) + Logger.log("self가 nil입니다. 작업을 중단합니다.", category: .error) return Disposables.create() } // 1. 업로드 링크 요청 self.getUploadLinks(request: .init(objectKeyList: datas.map { $0.filePath })) .subscribe { response in - Logger.log(message: "getUploadLinks 성공: \(response.preSignedUrlList)", category: .debug) + Logger.log("getUploadLinks 성공: \(response.preSignedUrlList)", category: .debug) let responseList = response.preSignedUrlList let inputList = datas @@ -57,23 +51,23 @@ class PreSignedService { let requestList = zip(responseList, inputList).compactMap { zipResponse in let urlResponse = zipResponse.0 let inputResponse = zipResponse.1 - Logger.log(message: "업로드 준비 - URL: \(urlResponse.preSignedUrl)", category: .debug) + Logger.log("업로드 준비 - URL: \(urlResponse.preSignedUrl)", category: .debug) return self.uploadFromS3(url: urlResponse.preSignedUrl, image: inputResponse.image) } // 3. 병렬 업로드 실행 Single.zip(requestList) .subscribe(onSuccess: { _ in - Logger.log(message: "모든 이미지 업로드 성공", category: .info) + Logger.log("모든 이미지 업로드 성공", category: .info) observer(.success(())) }, onFailure: { error in - Logger.log(message: "이미지 업로드 실패: \(error.localizedDescription)", category: .error) + Logger.log("이미지 업로드 실패: \(error.localizedDescription)", category: .error) observer(.failure(error)) }) .disposed(by: self.disposeBag) } onError: { error in - Logger.log(message: "getUploadLinks 실패: \(error.localizedDescription)", category: .error) + Logger.log("getUploadLinks 실패: \(error.localizedDescription)", category: .error) observer(.failure(error)) } .disposed(by: self.disposeBag) @@ -82,7 +76,6 @@ class PreSignedService { } } - func tryDownload(filePaths: [String]) -> Single<[UIImage]> { return Single.create { [weak self] observer in @@ -130,16 +123,16 @@ class PreSignedService { return filePaths.compactMap { imageMap[$0] } } .subscribe(onSuccess: { sortedImages in - Logger.log(message: "All images downloaded successfully", category: .info) + Logger.log("All images downloaded successfully", category: .info) observer(.success(sortedImages)) }, onFailure: { error in - Logger.log(message: "Image download failed: \(error.localizedDescription)", category: .error) + Logger.log("Image download failed: \(error.localizedDescription)", category: .error) observer(.failure(error)) }) .disposed(by: self.disposeBag) } onError: { error in - Logger.log(message: "getDownloadLinks Fail: \(error.localizedDescription)", category: .error) + Logger.log("getDownloadLinks Fail: \(error.localizedDescription)", category: .error) observer(.failure(error)) } .disposed(by: disposeBag) @@ -149,14 +142,13 @@ class PreSignedService { } } - private extension PreSignedService { func uploadFromS3(url: String, image: UIImage) -> Single { return Single.create { single in if let imageData = image.jpegData(compressionQuality: 0), let url = URL(string: url) { - Logger.log(message: "S3 업로드 요청 URL: \(url.absoluteString)", category: .debug) + Logger.log("S3 업로드 요청 URL: \(url.absoluteString)", category: .debug) let headers: HTTPHeaders = [ "Content-Type": "image/jpeg" @@ -164,19 +156,19 @@ private extension PreSignedService { AF.upload(imageData, to: url, method: .put, headers: headers) .response { response in - Logger.log(message: "S3 업로드 응답 상태: \(response.response?.statusCode ?? -1)", category: .debug) + Logger.log("S3 업로드 응답 상태: \(response.response?.statusCode ?? -1)", category: .debug) switch response.result { case .success: - Logger.log(message: "S3 업로드 성공 - URL: \(url.absoluteString)", category: .info) + Logger.log("S3 업로드 성공 - URL: \(url.absoluteString)", category: .info) single(.success(())) case .failure(let error): - Logger.log(message: "S3 업로드 실패: \(error.localizedDescription)", category: .error) + Logger.log("S3 업로드 실패: \(error.localizedDescription)", category: .error) single(.failure(error)) } } return Disposables.create() } else { - Logger.log(message: "S3 업로드 실패 - 잘못된 URL 또는 데이터", category: .error) + Logger.log("S3 업로드 실패 - 잘못된 URL 또는 데이터", category: .error) single(.failure(NSError(domain: "InvalidDataOrURL", code: -1, userInfo: nil))) return Disposables.create() } @@ -204,40 +196,36 @@ private extension PreSignedService { } } - - func getUploadLinks(request: PresignedURLRequestDTO) -> Observable { - Logger.log(message: "Presigned URL 생성 요청 데이터: \(request)", category: .debug) - let provider = ProviderImpl() + Logger.log("Presigned URL 생성 요청 데이터: \(request)", category: .debug) let endPoint = PreSignedAPIEndPoint.presigned_upload(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor) .do(onNext: { response in - Logger.log(message: "Presigned URL 응답 데이터: \(response.preSignedUrlList)", category: .debug) + Logger.log("Presigned URL 응답 데이터: \(response.preSignedUrlList)", category: .debug) }, onError: { error in - Logger.log(message: "Presigned URL 요청 실패: \(error.localizedDescription)", category: .error) + Logger.log("Presigned URL 요청 실패: \(error.localizedDescription)", category: .error) }) } func getDownloadLinks(request: PresignedURLRequestDTO) -> Observable { - let provider = ProviderImpl() let endPoint = PreSignedAPIEndPoint.presigned_download(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor) } } extension PreSignedService { func deleteImage(filePath: String, completion: @escaping (Result) -> Void) { - Logger.log(message: "이미지 삭제 시작 - 경로: \(filePath)", category: .debug) + Logger.log("이미지 삭제 시작 - 경로: \(filePath)", category: .debug) let request = PresignedURLRequestDTO(objectKeyList: [filePath]) tryDelete(targetPaths: request) .subscribe( onCompleted: { - Logger.log(message: "이미지 삭제 성공: \(filePath)", category: .debug) + Logger.log("이미지 삭제 성공: \(filePath)", category: .debug) completion(.success(())) }, onError: { error in - Logger.log(message: "이미지 삭제 실패: \(error.localizedDescription)", category: .error) + Logger.log("이미지 삭제 실패: \(error.localizedDescription)", category: .error) completion(.failure(error)) } ) @@ -291,21 +279,20 @@ extension PreSignedService { } } func fullImageURL(from filePath: String) -> URL? { - let baseURL = Secrets.popPoolS3BaseURL.rawValue + let baseURL = Secrets.popPoolS3BaseURL // URL 인코딩 처리를 더 엄격하게 guard let encodedPath = filePath .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)? .replacingOccurrences(of: "+", with: "%2B") else { - Logger.log(message: "URL 인코딩 실패: \(filePath)", category: .error) + Logger.log("URL 인코딩 실패: \(filePath)", category: .error) return nil } let fullString = baseURL + encodedPath - Logger.log(message: "생성된 URL: \(fullString)", category: .debug) + Logger.log("생성된 URL: \(fullString)", category: .debug) return URL(string: fullString) } } - diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/AdminRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/AdminRepositoryImpl.swift new file mode 100644 index 00000000..838ddbad --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/AdminRepositoryImpl.swift @@ -0,0 +1,181 @@ +import Foundation + +import DomainInterface + +import Alamofire +import RxSwift + +public final class AdminRepositoryImpl: AdminRepository { + + // MARK: - Properties + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + // MARK: - Init + public init(provider: Provider) { + self.provider = provider + } + + // MARK: - Store Methods + public func fetchStoreList(query: String?, page: Int, size: Int) -> Observable<[AdminStore]> { + let endpoint = AdminAPIEndpoint.fetchStoreList( + query: query, + page: page, + size: size + ) + return provider.requestData( + with: endpoint, + interceptor: tokenInterceptor + ) + .map { response in + response.popUpStoreList?.map { + AdminStore(id: $0.id, name: $0.name, categoryName: $0.categoryName, mainImageUrl: $0.mainImageUrl) + } ?? [] + } + } + + public func fetchStoreDetail(id: Int64) -> Observable { + let endpoint = AdminAPIEndpoint.fetchStoreDetail(id: id) + return provider.requestData( + with: endpoint, + interceptor: tokenInterceptor + ) + .map { dto in + AdminStoreDetail( + id: dto.id, + name: dto.name, + categoryId: Int(dto.categoryId), + categoryName: dto.categoryName, + description: dto.desc, + address: dto.address, + startDate: dto.startDate, + endDate: dto.endDate, + createUserId: dto.createUserId, + createDateTime: dto.createDateTime, + mainImageUrl: dto.mainImageUrl, + bannerYn: dto.bannerYn, + images: dto.imageList.map { + AdminStoreDetail.StoreImage( + id: $0.id, + imageUrl: $0.imageUrl + ) + }, + latitude: dto.latitude, + longitude: dto.longitude, + markerTitle: dto.markerTitle, + markerSnippet: dto.markerSnippet + ) + } + .catch { error in + if case .responseSerializationFailed = error as? AFError { + return Observable.empty() + } + throw error + } + } + + public func createStore(params: CreateStoreParams) -> Completable { + let dto = CreatePopUpStoreRequestDTO( + name: params.name, + categoryId: Int64(params.categoryId), + desc: params.desc, + address: params.address, + startDate: params.startDate, + endDate: params.endDate, + mainImageUrl: params.mainImageUrl, + imageUrlList: params.imageUrlList, + latitude: params.latitude, + longitude: params.longitude, + markerTitle: params.markerTitle, + markerSnippet: params.markerSnippet, + startDateBeforeEndDate: params.startDateBeforeEndDate + ) + let endpoint = AdminAPIEndpoint.createStore(request: dto) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } + + public func updateStore(params: UpdateStoreParams) -> Completable { + let dto = UpdatePopUpStoreRequestDTO( + popUpStore: UpdatePopUpStoreRequestDTO.PopUpStore( + id: params.id, + name: params.name, + categoryId: Int64(params.categoryId), + desc: params.desc, + address: params.address, + startDate: params.startDate, + endDate: params.endDate, + mainImageUrl: params.mainImageUrl, + bannerYn: !params.mainImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + imageUrl: params.imageUrlList.compactMap { $0 }, + startDateBeforeEndDate: params.startDateBeforeEndDate + ), + location: UpdatePopUpStoreRequestDTO.Location( + latitude: params.latitude, + longitude: params.longitude, + markerTitle: params.markerTitle, + markerSnippet: params.markerSnippet + ), + imagesToAdd: params.imageUrlList.compactMap { $0 }, + imagesToDelete: params.imagesToDelete + ) + let endpoint = AdminAPIEndpoint.updateStore(request: dto) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } + + public func deleteStore(id: Int64) -> Completable { + let endpoint = AdminAPIEndpoint.deleteStore(id: id) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } + + // MARK: - Notice Methods + public func createNotice(params: CreateNoticeParams) -> Completable { + let dto = CreateNoticeRequestDTO( + title: params.title, + content: params.content, + imageUrlList: params.imageUrlList + ) + let endpoint = AdminAPIEndpoint.createNotice(request: dto) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } + + public func updateNotice(params: UpdateNoticeParams) -> Completable { + let dto = UpdateNoticeRequestDTO( + title: params.title, + content: params.content, + imageUrlList: params.imageUrlList, + imagesToDelete: params.imagesToDelete + ) + let endpoint = AdminAPIEndpoint.updateNotice(id: params.id, request: dto) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } + + public func deleteNotice(id: Int64) -> Completable { + let endpoint = AdminAPIEndpoint.deleteNotice(id: id) + return provider.request(with: endpoint, interceptor: tokenInterceptor) + } +} + +// Helper extension - keeping this for utility purposes +extension GetAdminPopUpStoreDetailResponseDTO { + static var empty: GetAdminPopUpStoreDetailResponseDTO { + return GetAdminPopUpStoreDetailResponseDTO( + id: 0, + name: "", + categoryId: 0, + categoryName: "", + desc: "", + address: "", + startDate: "", + endDate: "", + createUserId: "", + createDateTime: "", + mainImageUrl: "", + bannerYn: false, + imageList: [], + latitude: 0.0, + longitude: 0.0, + markerTitle: "", + markerSnippet: "" + ) + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/AppleLoginRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/AppleLoginRepositoryImpl.swift new file mode 100644 index 00000000..8e306c22 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/AppleLoginRepositoryImpl.swift @@ -0,0 +1,15 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public class AppleLoginRepositoryImpl: AppleLoginRepository { + private let service = AppleLoginService() + + public init() { } + + public func fetchUserCredential() -> Observable { + return service.fetchUserCredential() + } +} diff --git a/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/AuthAPIRepositoryImpl.swift similarity index 50% rename from Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift rename to Poppool/DataLayer/Data/Data/RepositoryImpl/AuthAPIRepositoryImpl.swift index 7c9e2628..0449cdf0 100644 --- a/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/AuthAPIRepositoryImpl.swift @@ -1,24 +1,19 @@ -// -// AuthRepository.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation + +import DomainInterface + import RxSwift -final class AuthAPIRepositoryImpl { - - var provider: Provider - - var tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { +public final class AuthAPIRepositoryImpl: AuthAPIRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + public init(provider: Provider) { self.provider = provider } - - func tryLogIn(userCredential: Encodable, socialType: String) -> Observable { + + public func tryLogIn(userCredential: Encodable, socialType: String) -> Observable { let endPoint = AuthAPIEndPoint.auth_tryLogin(with: userCredential, path: socialType) return provider .requestData(with: endPoint, interceptor: nil) @@ -26,9 +21,12 @@ final class AuthAPIRepositoryImpl { return responseDTO.toDomain() } } - - func postTokenReissue() -> Observable { + + public func postTokenReissue() -> Observable { let endPoint = AuthAPIEndPoint.postTokenReissue() return provider.requestData(with: endPoint, interceptor: tokenInterceptor) + .map { responseDTO in + return responseDTO.toDomain() + } } } diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/CategoryRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/CategoryRepositoryImpl.swift new file mode 100644 index 00000000..5616be38 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/CategoryRepositoryImpl.swift @@ -0,0 +1,21 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class CategoryRepositoryImpl: CategoryRepository { + + private let provider: Provider + + public init(provider: Provider) { + self.provider = provider + } + + public func fetchCategoryList() -> Observable<[CategoryResponse]> { + let endPoint = CategoryAPIEndpoint.getCategoryList() + return provider.requestData(with: endPoint, interceptor: TokenInterceptor()).map { responseDTO in + return responseDTO.categoryResponseList.map({ $0.toDomain() }) + } + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/CommentAPIRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/CommentAPIRepositoryImpl.swift new file mode 100644 index 00000000..142b61c0 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/CommentAPIRepositoryImpl.swift @@ -0,0 +1,40 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class CommentAPIRepositoryImpl: CommentAPIRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + public init(provider: Provider) { + self.provider = provider + } + + public func postCommentAdd(popUpStoreId: Int64, content: String?, commentType: String?, imageUrlList: [String?]) -> Completable { + let requestDTO = PostCommentRequestDTO(popUpStoreId: popUpStoreId, content: content, commentType: commentType, imageUrlList: imageUrlList) + let endPoint = CommentAPIEndPoint.postCommentAdd(request: requestDTO) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func deleteComment(popUpStoreId: Int64, commentId: Int64) -> Completable { + let requestDTO = DeleteCommentRequestDTO(popUpStoreId: popUpStoreId, commentId: commentId) + let endPoint = CommentAPIEndPoint.deleteComment(request: requestDTO) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func editComment(popUpStoreId: Int64, commentId: Int64, content: String?, imageUrlList: [String?]?) -> Completable { + let dtoList: [PutCommentImageDataRequestDTO]? = imageUrlList?.compactMap { $0 }.map { PutCommentImageDataRequestDTO(imageUrl: $0) } + + let requestDTO = PutCommentRequestDTO( + popUpStoreId: popUpStoreId, + commentId: commentId, + content: content, + imageUrlList: dtoList + ) + let endPoint = CommentAPIEndPoint.editComment(request: requestDTO) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } +} diff --git a/Poppool/Poppool/Data/Repository/HomeAPIRepository.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/HomeAPIRepositoryImpl.swift similarity index 50% rename from Poppool/Poppool/Data/Repository/HomeAPIRepository.swift rename to Poppool/DataLayer/Data/Data/RepositoryImpl/HomeAPIRepositoryImpl.swift index 17c9a453..c9020451 100644 --- a/Poppool/Poppool/Data/Repository/HomeAPIRepository.swift +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/HomeAPIRepositoryImpl.swift @@ -1,38 +1,38 @@ -// -// HomeAPIRepository.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import Foundation + +import DomainInterface + import RxSwift -final class HomeAPIRepository { - +public final class HomeAPIRepositoryImpl: HomeAPIRepository { + private let provider: Provider private let tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { + + public init(provider: Provider) { self.provider = provider } - - func fetchHome(request: SortedRequestDTO) -> Observable { + + public func fetchHome(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = HomeSortedRequestDTO(page: page, size: size, sort: sort) let endPoint = HomeAPIEndpoint.fetchHome(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() }) } - - func fetchCustomPopUp(request: SortedRequestDTO) -> Observable { + + public func fetchCustomPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = HomeSortedRequestDTO(page: page, size: size, sort: sort) let endPoint = HomeAPIEndpoint.fetchCustomPopUp(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() }) } - - func fetchNewPopUp(request: SortedRequestDTO) -> Observable { + + public func fetchNewPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = HomeSortedRequestDTO(page: page, size: size, sort: sort) let endPoint = HomeAPIEndpoint.fetchNewPopUp(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() }) } - - func fetchPopularPopUp(request: SortedRequestDTO) -> Observable { + + public func fetchPopularPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = HomeSortedRequestDTO(page: page, size: size, sort: sort) let endPoint = HomeAPIEndpoint.fetchPopularPopUp(request: request) return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() }) } diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/KakaoLoginRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/KakaoLoginRepositoryImpl.swift new file mode 100644 index 00000000..86145f69 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/KakaoLoginRepositoryImpl.swift @@ -0,0 +1,15 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public class KakaoLoginRepositoryImpl: KakaoLoginRepository { + let service = KakaoLoginService() + + public init() { } + + public func fetchUserCredential() -> Observable { + return service.fetchUserCredential() + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/MapDirectionRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/MapDirectionRepositoryImpl.swift new file mode 100644 index 00000000..cccd8f73 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/MapDirectionRepositoryImpl.swift @@ -0,0 +1,20 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class MapDirectionRepositoryImpl: MapDirectionRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + public init(provider: Provider) { + self.provider = provider + } + + public func getPopUpDirection(popUpStoreId: Int64) -> Observable { + let endpoint = FindDirectionEndPoint.fetchDirection(popUpStoreId: popUpStoreId) + return provider.requestData(with: endpoint, interceptor: tokenInterceptor).map({ $0.toDomain() }) + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/MapRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/MapRepositoryImpl.swift new file mode 100644 index 00000000..c6a9ba90 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/MapRepositoryImpl.swift @@ -0,0 +1,74 @@ +import Foundation + +import DomainInterface +import Infrastructure + +import RxSwift + +public final class MapRepositoryImpl: MapRepository { + + private let provider: Provider + + public init(provider: Provider) { + self.provider = provider + } + + public func fetchStoresInBounds( + northEastLat: Double, + northEastLon: Double, + southWestLat: Double, + southWestLon: Double, + categories: [Int] + ) -> Observable<[MapPopUpStore]> { + return provider.requestData( + with: MapAPIEndpoint.locations_fetchStoresInBounds( + northEastLat: northEastLat, + northEastLon: northEastLon, + southWestLat: southWestLat, + southWestLon: southWestLon, + categories: categories.map { Int64($0 ) } + ), + interceptor: TokenInterceptor() + ) + .map { $0.popUpStoreList.map { $0.toDomain() } } + } + + public func searchStores( + query: String, + categories: [Int] + ) -> Observable<[MapPopUpStore]> { + return provider.requestData( + with: MapAPIEndpoint.locations_searchStores( + query: query, + categories: categories.map { Int64($0 ) } + ), + interceptor: TokenInterceptor() + ) + .map { $0.popUpStoreList.map { $0.toDomain() } } + } + + public func fetchCategories() -> Observable<[CategoryResponse]> { + Logger.log("카테고리 목록 요청을 시작합니다.", category: .network) + + return provider.requestData( + with: SignUpAPIEndpoint.signUp_getCategoryList(), + interceptor: TokenInterceptor() + ) + .do(onNext: { _ in + Logger.log( + "카테고리 목록 응답 성공", + category: .debug + ) + }) + .map { responseDTO in + responseDTO.categoryResponseList.map { $0.toDomain() } + } + .catch { error in + Logger.log( + "카테고리 목록 요청 실패: \(error.localizedDescription)", + category: .error + ) + throw error + } + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/PopUpAPIRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/PopUpAPIRepositoryImpl.swift new file mode 100644 index 00000000..782c7880 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/PopUpAPIRepositoryImpl.swift @@ -0,0 +1,72 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class PopUpAPIRepositoryImpl: PopUpAPIRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + public init(provider: Provider) { + self.provider = provider + } + + public func postBookmarkPopUp(popUpStoreId: Int64) -> Completable { + let request = PostBookmarkPopUpRequestDTO(popUpStoreId: popUpStoreId) + let endPoint = UserAPIEndPoint.postBookmarkPopUp(request: request) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func getClosePopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable { + let request = GetSearchPopUpListRequestDTO(categories: categories, page: page, size: size, sort: sort, query: query, sortCode: sortCode) + let endPoint = PopUpAPIEndPoint.getClosePopUpList(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getOpenPopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable { + let request = GetSearchPopUpListRequestDTO(categories: categories, page: page, size: size, sort: sort, query: query, sortCode: sortCode) + let endPoint = PopUpAPIEndPoint.getOpenPopUpList(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getSearchPopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable { + let request = GetSearchPopUpListRequestDTO(categories: categories, page: page, size: size, sort: sort, query: query, sortCode: sortCode) + let endPoint = PopUpAPIEndPoint.getSearchPopUpList(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getPopUpDetail(commentType: String?, popUpStoreId: Int64, viewCountYn: Bool?) -> Observable { + let request = GetPopUpDetailRequestDTO(commentType: commentType, popUpStoreId: popUpStoreId, viewCountYn: viewCountYn) + let endPoint = PopUpAPIEndPoint.getPopUpDetail(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getPopUpComment(commentType: String?, page: Int32?, size: Int32?, sort: String?, popUpStoreId: Int64) -> Observable { + let request = GetPopUpCommentRequestDTO(commentType: commentType, page: page, size: size, sort: sort, popUpStoreId: popUpStoreId) + let endPoint = PopUpAPIEndPoint.getPopUpComment(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/PreSignedRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/PreSignedRepositoryImpl.swift new file mode 100644 index 00000000..3c08b22b --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/PreSignedRepositoryImpl.swift @@ -0,0 +1,31 @@ +import UIKit + +import DomainInterface + +import RxSwift + +public final class PreSignedRepositoryImpl: PreSignedRepository { + + private let service = PreSignedService() + + public init() { } + + public func tryUpload(presignedURLRequest: [(filePath: String, image: UIImage)]) -> Single { + return service.tryUpload(datas: presignedURLRequest.map { + PreSignedService.PresignedURLRequest( + filePath: $0.filePath, + image: $0.image + ) + }) + } + + public func tryDelete(objectKeyList: [String]) -> Completable { + return service.tryDelete( + targetPaths: PresignedURLRequestDTO(objectKeyList: objectKeyList) + ) + } + + public func fullImageURL(from filePath: String) -> URL? { + return service.fullImageURL(from: filePath) + } +} diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/Search/SearchAPIRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/Search/SearchAPIRepositoryImpl.swift new file mode 100644 index 00000000..1e190673 --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/Search/SearchAPIRepositoryImpl.swift @@ -0,0 +1,41 @@ +import Foundation + +import DomainInterface +import Infrastructure + +import RxSwift + +public final class SearchAPIRepositoryImpl: SearchAPIRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + private let userDefaultService: UserDefaultService + + public init( + provider: Provider, + userDefaultService: UserDefaultService + ) { + self.provider = provider + self.userDefaultService = userDefaultService + } + + public func fetchSearchResult(by query: String) -> Observable { + + let request = GetSearchPopupStoreRequestDTO(query: query) + let endPoint = SearchAPIEndPoint.getSearchPopUpList(request: request) + return provider.requestData( + with: endPoint, + interceptor: tokenInterceptor + ) + .map { $0.toDomain() } + .do { _ in self.saveSearchKeyword(keyword: query) } + } +} + +private extension SearchAPIRepositoryImpl { + func saveSearchKeyword(keyword: String) { + let existingList = userDefaultService.fetchArray(keyType: .searchKeyword) ?? [] + let updatedList = [keyword] + existingList.filter { $0 != keyword } + userDefaultService.save(keyType: .searchKeyword, value: updatedList) + } +} diff --git a/Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/SignUpRepositoryImpl.swift similarity index 75% rename from Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift rename to Poppool/DataLayer/Data/Data/RepositoryImpl/SignUpRepositoryImpl.swift index 87455fbd..8b769303 100644 --- a/Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/SignUpRepositoryImpl.swift @@ -1,40 +1,36 @@ -// -// SignUpRepositoryImpl.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation + +import DomainInterface + import RxSwift -final class SignUpRepositoryImpl { - - var provider: Provider - - init(provider: Provider) { +public final class SignUpRepositoryImpl: SignUpRepository { + + private let provider: Provider + + public init(provider: Provider) { self.provider = provider } - - func checkNickName(nickName: String) -> Observable { + + public func checkNickName(nickName: String) -> Observable { let endPoint = SignUpAPIEndpoint.signUp_checkNickName(with: .init(nickName: nickName)) return provider.requestData(with: endPoint, interceptor: TokenInterceptor()) } - - func fetchCategoryList() -> Observable<[Category]> { + + public func fetchCategoryList() -> Observable<[CategoryResponse]> { let endPoint = SignUpAPIEndpoint.signUp_getCategoryList() return provider.requestData(with: endPoint, interceptor: TokenInterceptor()).map { responseDTO in return responseDTO.categoryResponseList.map({ $0.toDomain() }) } } - - func trySignUp( + + public func trySignUp( nickName: String, gender: String, age: Int32, socialEmail: String, socialType: String, - interests: [Int64], + interests: [Int], appleAuthorizationCode: String? ) -> Completable { let endPoint = SignUpAPIEndpoint.signUp_trySignUp(with: .init( diff --git a/Poppool/DataLayer/Data/Data/RepositoryImpl/UserAPIRepositoryImpl.swift b/Poppool/DataLayer/Data/Data/RepositoryImpl/UserAPIRepositoryImpl.swift new file mode 100644 index 00000000..ae4713ae --- /dev/null +++ b/Poppool/DataLayer/Data/Data/RepositoryImpl/UserAPIRepositoryImpl.swift @@ -0,0 +1,151 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class UserAPIRepositoryImpl: UserAPIRepository { + + private let provider: Provider + private let tokenInterceptor = TokenInterceptor() + + public init(provider: Provider) { + self.provider = provider + } + + public func postBookmarkPopUp(popUpStoreId: Int64) -> Completable { + let endPoint = UserAPIEndPoint.postBookmarkPopUp(request: .init(popUpStoreId: popUpStoreId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func deleteBookmarkPopUp(popUpStoreId: Int64) -> Completable { + let endPoint = UserAPIEndPoint.deleteBookmarkPopUp(request: .init(popUpStoreId: popUpStoreId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func postCommentLike(commentId: Int64) -> Completable { + let endPoint = UserAPIEndPoint.postCommentLike(request: .init(commentId: commentId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func deleteCommentLike(commentId: Int64) -> Completable { + let endPoint = UserAPIEndPoint.deleteCommentLike(request: .init(commentId: commentId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func postUserBlock(blockedUserId: String?) -> Completable { + let endPoint = UserAPIEndPoint.postUserBlock(request: .init(blockedUserId: blockedUserId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func deleteUserBlock(blockedUserId: String?) -> Completable { + let endPoint = UserAPIEndPoint.deleteUserBlock(request: .init(blockedUserId: blockedUserId)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func getOtherUserCommentList( + commenterId: String?, + commentType: String?, + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + let request = GetOtherUserCommentListRequestDTO(commenterId: commenterId, commentType: commentType, page: page, size: size, sort: sort) + let endPoint = UserAPIEndPoint.getOtherUserCommentPopUpList(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getMyPage() -> Observable { + let endPoint = UserAPIEndPoint.getMyPage() + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func postLogout() -> Completable { + let endPoint = UserAPIEndPoint.postLogout() + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func getWithdrawlList() -> Observable { + let endPoint = UserAPIEndPoint.getWithdrawlList() + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func postWithdrawl(list: [(Int64, String?)]) -> Completable { + let endPoint = UserAPIEndPoint.postWithdrawl(request: .init(checkedSurveyList: list.map { .init(id: $0.0, survey: $0.1)})) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func getMyProfile() -> Observable { + let endPoint = UserAPIEndPoint.getMyProfile() + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func putUserTailoredInfo(gender: String?, age: Int32) -> Completable { + let endPoint = UserAPIEndPoint.putUserTailoredInfo(request: .init(gender: gender, age: age)) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func putUserCategory( + interestCategoriesToAdd: [Int], + interestCategoriesToDelete: [Int], + interestCategoriesToKeep: [Int] + ) -> Completable { + let request = PutUserCategoryRequestDTO( + interestCategoriesToAdd: interestCategoriesToAdd.map { Int64($0 ) }, + interestCategoriesToDelete: interestCategoriesToDelete.map { Int64($0 ) }, + interestCategoriesToKeep: interestCategoriesToKeep.map { Int64($0 ) } + ) + let endPoint = UserAPIEndPoint.putUserCategory(request: request) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func putUserProfile( + profileImageUrl: String?, + nickname: String?, + email: String?, + instagramId: String?, + intro: String? + ) -> Completable { + let request = PutUserProfileRequestDTO(profileImageUrl: profileImageUrl, nickname: nickname, email: email, instagramId: instagramId, intro: intro) + let endPoint = UserAPIEndPoint.putUserProfile(request: request) + return provider.request(with: endPoint, interceptor: tokenInterceptor) + } + + public func getMyCommentedPopUp( + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + let request = UserSortedRequestDTO(page: page, size: size, sort: sort) + let endPoint = UserAPIEndPoint.getMyCommentedPopUp(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getBlockUserList(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = UserSortedRequestDTO(page: page, size: size, sort: sort) + let endPoint = UserAPIEndPoint.getBlockUserList(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getNoticeList() -> Observable { + let endPoint = UserAPIEndPoint.getNoticeList() + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getNoticeDetail(noticeID: Int64) -> Observable { + let endPoint = UserAPIEndPoint.getNoticeDetail(noticeID: noticeID) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getRecentPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = UserSortedRequestDTO(page: page, size: size, sort: sort) + let endPoint = UserAPIEndPoint.getRecentPopUp(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } + + public func getBookmarkPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + let request = UserSortedRequestDTO(page: page, size: size, sort: sort) + let endPoint = UserAPIEndPoint.getBookmarkPopUp(request: request) + return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } + } +} diff --git a/Poppool/DomainLayer/Domain/Domain.xcodeproj/project.pbxproj b/Poppool/DomainLayer/Domain/Domain.xcodeproj/project.pbxproj new file mode 100644 index 00000000..21286382 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain.xcodeproj/project.pbxproj @@ -0,0 +1,610 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0522C1DD2DB67C6E00B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1DC2DB67C6E00B141FF /* RxSwift */; }; + 0522C1DF2DB67C7700B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1DE2DB67C7700B141FF /* RxSwift */; }; + 05C1D61E2DB53A6700508FFD /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D0DB2DB538A600508FFD /* DomainInterface.framework */; }; + 05C1D6222DB53A6700508FFD /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D61D2DB53A6700508FFD /* Infrastructure.framework */; }; + 05C1D6262DB53A6E00508FFD /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6252DB53A6E00508FFD /* Infrastructure.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 05C1D6202DB53A6700508FFD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 058CC8E72DB5377F0084221A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05C1D0DA2DB538A600508FFD; + remoteInfo = DomainInterface; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 05BDD5E02DB678D100C1E192 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 05BDD5E42DB678DC00C1E192 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 058CC8F02DB5377F0084221A /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D0DB2DB538A600508FFD /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D61D2DB53A6700508FFD /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6252DB53A6E00508FFD /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 058CC8F22DB5377F0084221A /* Domain */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Domain; + sourceTree = ""; + }; + 05C1D0DC2DB538A600508FFD /* DomainInterface */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DomainInterface; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 058CC8ED2DB5377F0084221A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0522C1DD2DB67C6E00B141FF /* RxSwift in Frameworks */, + 05C1D61E2DB53A6700508FFD /* DomainInterface.framework in Frameworks */, + 05C1D6222DB53A6700508FFD /* Infrastructure.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05C1D0D82DB538A600508FFD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0522C1DF2DB67C7700B141FF /* RxSwift in Frameworks */, + 05C1D6262DB53A6E00508FFD /* Infrastructure.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058CC8E62DB5377F0084221A = { + isa = PBXGroup; + children = ( + 058CC8F22DB5377F0084221A /* Domain */, + 05C1D0DC2DB538A600508FFD /* DomainInterface */, + 05C1D61C2DB53A6700508FFD /* Frameworks */, + 058CC8F12DB5377F0084221A /* Products */, + ); + sourceTree = ""; + }; + 058CC8F12DB5377F0084221A /* Products */ = { + isa = PBXGroup; + children = ( + 058CC8F02DB5377F0084221A /* Domain.framework */, + 05C1D0DB2DB538A600508FFD /* DomainInterface.framework */, + ); + name = Products; + sourceTree = ""; + }; + 05C1D61C2DB53A6700508FFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 05C1D6252DB53A6E00508FFD /* Infrastructure.framework */, + 05C1D61D2DB53A6700508FFD /* Infrastructure.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 058CC8EB2DB5377F0084221A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05C1D0D62DB538A600508FFD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 058CC8EF2DB5377F0084221A /* Domain */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058CC8F72DB5377F0084221A /* Build configuration list for PBXNativeTarget "Domain" */; + buildPhases = ( + 058CC8EB2DB5377F0084221A /* Headers */, + 058CC8EC2DB5377F0084221A /* Sources */, + 058CC8ED2DB5377F0084221A /* Frameworks */, + 058CC8EE2DB5377F0084221A /* Resources */, + 05BDD5E42DB678DC00C1E192 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 05C1D6212DB53A6700508FFD /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 058CC8F22DB5377F0084221A /* Domain */, + ); + name = Domain; + packageProductDependencies = ( + 0522C1DC2DB67C6E00B141FF /* RxSwift */, + ); + productName = Domain; + productReference = 058CC8F02DB5377F0084221A /* Domain.framework */; + productType = "com.apple.product-type.framework"; + }; + 05C1D0DA2DB538A600508FFD /* DomainInterface */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05C1D0DF2DB538A600508FFD /* Build configuration list for PBXNativeTarget "DomainInterface" */; + buildPhases = ( + 05C1D0D62DB538A600508FFD /* Headers */, + 05C1D0D72DB538A600508FFD /* Sources */, + 05C1D0D82DB538A600508FFD /* Frameworks */, + 05C1D0D92DB538A600508FFD /* Resources */, + 05BDD5E02DB678D100C1E192 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 05C1D0DC2DB538A600508FFD /* DomainInterface */, + ); + name = DomainInterface; + packageProductDependencies = ( + 0522C1DE2DB67C7700B141FF /* RxSwift */, + ); + productName = DomainInterface; + productReference = 05C1D0DB2DB538A600508FFD /* DomainInterface.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058CC8E72DB5377F0084221A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 058CC8EF2DB5377F0084221A = { + CreatedOnToolsVersion = 16.3; + }; + 05C1D0DA2DB538A600508FFD = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 058CC8EA2DB5377F0084221A /* Build configuration list for PBXProject "Domain" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058CC8E62DB5377F0084221A; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 05C1D8282DB53CC100508FFD /* XCRemoteSwiftPackageReference "RxSwift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 058CC8F12DB5377F0084221A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 058CC8EF2DB5377F0084221A /* Domain */, + 05C1D0DA2DB538A600508FFD /* DomainInterface */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 058CC8EE2DB5377F0084221A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05C1D0D92DB538A600508FFD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 058CC8EC2DB5377F0084221A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05C1D0D72DB538A600508FFD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 05C1D6212DB53A6700508FFD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05C1D0DA2DB538A600508FFD /* DomainInterface */; + targetProxy = 05C1D6202DB53A6700508FFD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 058CC8F52DB5377F0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 058CC8F62DB5377F0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 058CC8F82DB5377F0084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Domain; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 058CC8F92DB5377F0084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Domain; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 05C1D0E02DB538A600508FFD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.DomainInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 05C1D0E12DB538A600508FFD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.DomainInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 058CC8EA2DB5377F0084221A /* Build configuration list for PBXProject "Domain" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC8F52DB5377F0084221A /* Debug */, + 058CC8F62DB5377F0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058CC8F72DB5377F0084221A /* Build configuration list for PBXNativeTarget "Domain" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC8F82DB5377F0084221A /* Debug */, + 058CC8F92DB5377F0084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05C1D0DF2DB538A600508FFD /* Build configuration list for PBXNativeTarget "DomainInterface" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05C1D0E02DB538A600508FFD /* Debug */, + 05C1D0E12DB538A600508FFD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 05C1D8282DB53CC100508FFD /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0522C1DC2DB67C6E00B141FF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05C1D8282DB53CC100508FFD /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 0522C1DE2DB67C7700B141FF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05C1D8282DB53CC100508FFD /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 058CC8E72DB5377F0084221A /* Project object */; +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AdminUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AdminUseCaseImpl.swift new file mode 100644 index 00000000..c2482298 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AdminUseCaseImpl.swift @@ -0,0 +1,64 @@ +import Foundation + +import DomainInterface +import Infrastructure + +import RxSwift + +public final class AdminUseCaseImpl: AdminUseCase { + + private let repository: AdminRepository + + public init(repository: AdminRepository) { + self.repository = repository + } + + public func fetchStoreList(query: String?, page: Int, size: Int) -> Observable<[AdminStore]> { + return repository.fetchStoreList(query: query, page: page, size: size) + } + + public func fetchStoreDetail(id: Int64) -> Observable { + return repository.fetchStoreDetail(id: id) + } + + public func createStore(params: CreateStoreParams) -> Completable { + Logger.log("createStore 호출 - 스토어명: \(params.name)", category: .debug) + return repository.createStore(params: params) + .do(onError: { error in + Logger.log("createStore 실패 - Error: \(error)", category: .error) + }, onCompleted: { + Logger.log("createStore 성공", category: .info) + }) + } + + public func updateStore(params: UpdateStoreParams) -> Completable { + Logger.log(""" + Updating store with location: + Latitude: \(params.latitude) + Longitude: \(params.longitude) + """, category: .debug) + return repository.updateStore(params: params) + .do(onError: { error in + Logger.log("Store update failed: \(error)", category: .error) + }, onCompleted: { + Logger.log("Store update successful", category: .debug) + }) + } + + public func deleteStore(id: Int64) -> Completable { + return repository.deleteStore(id: id) + } + + // Notice + public func createNotice(params: CreateNoticeParams) -> Completable { + return repository.createNotice(params: params) + } + + public func updateNotice(params: UpdateNoticeParams) -> Completable { + return repository.updateNotice(params: params) + } + + public func deleteNotice(id: Int64) -> Completable { + return repository.deleteNotice(id: id) + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AppleLoginUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AppleLoginUseCaseImpl.swift new file mode 100644 index 00000000..2f0e4f9f --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AppleLoginUseCaseImpl.swift @@ -0,0 +1,17 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public class AppleLoginUseCaseImpl: AppleLoginUseCase { + private let repository: AppleLoginRepository + + public init(repository: AppleLoginRepository) { + self.repository = repository + } + + public func fetchUserCredential() -> Observable { + return repository.fetchUserCredential() + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AuthAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AuthAPIUseCaseImpl.swift new file mode 100644 index 00000000..6ec0781d --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/AuthAPIUseCaseImpl.swift @@ -0,0 +1,22 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class AuthAPIUseCaseImpl: AuthAPIUseCase { + + private let repository: AuthAPIRepository + + public init(repository: AuthAPIRepository) { + self.repository = repository + } + + public func postTryLogin(userCredential: Encodable, socialType: String) -> Observable { + return repository.tryLogIn(userCredential: userCredential, socialType: socialType) + } + + public func postTokenReissue() -> Observable { + return repository.postTokenReissue() + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/CommentAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/CommentAPIUseCaseImpl.swift new file mode 100644 index 00000000..479fbdd0 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/CommentAPIUseCaseImpl.swift @@ -0,0 +1,26 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class CommentAPIUseCaseImpl: CommentAPIUseCase { + + private let repository: CommentAPIRepository + + public init(repository: CommentAPIRepository) { + self.repository = repository + } + + public func postCommentAdd(popUpStoreId: Int64, content: String?, commentType: String?, imageUrlList: [String?]) -> Completable { + return repository.postCommentAdd(popUpStoreId: popUpStoreId, content: content, commentType: commentType, imageUrlList: imageUrlList) + } + + public func deleteComment(popUpStoreId: Int64, commentId: Int64) -> Completable { + return repository.deleteComment(popUpStoreId: popUpStoreId, commentId: commentId) + } + + public func editComment(popUpStoreId: Int64, commentId: Int64, content: String?, imageUrlList: [String?]?) -> Completable { + return repository.editComment(popUpStoreId: popUpStoreId, commentId: commentId, content: content, imageUrlList: imageUrlList) + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/FetchCategoryListUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/FetchCategoryListUseCaseImpl.swift new file mode 100644 index 00000000..aab8b580 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/FetchCategoryListUseCaseImpl.swift @@ -0,0 +1,18 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class FetchCategoryListUseCaseImpl: FetchCategoryListUseCase { + + private let repository: CategoryRepository + + public init(repository: CategoryRepository) { + self.repository = repository + } + + public func execute() -> Observable<[CategoryResponse]> { + return repository.fetchCategoryList() + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/HomeAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/HomeAPIUseCaseImpl.swift new file mode 100644 index 00000000..be56469f --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/HomeAPIUseCaseImpl.swift @@ -0,0 +1,46 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class HomeAPIUseCaseImpl: HomeAPIUseCase { + + private let repository: HomeAPIRepository + + public init(repository: HomeAPIRepository) { + self.repository = repository + } + + public func fetchHome( + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + return repository.fetchHome(page: page, size: size, sort: sort) + } + + public func fetchCustomPopUp( + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + return repository.fetchCustomPopUp(page: page, size: size, sort: sort) + } + + public func fetchNewPopUp( + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + return repository.fetchNewPopUp(page: page, size: size, sort: sort) + } + + public func fetchPopularPopUp( + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + return repository.fetchPopularPopUp(page: page, size: size, sort: sort) + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/KakaoLoginUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/KakaoLoginUseCaseImpl.swift new file mode 100644 index 00000000..261dc3a9 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/KakaoLoginUseCaseImpl.swift @@ -0,0 +1,17 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public class KakaoLoginUseCaseImpl: KakaoLoginUseCase { + private let repository: KakaoLoginRepository + + public init(repository: KakaoLoginRepository) { + self.repository = repository + } + + public func fetchUserCredential() -> Observable { + return repository.fetchUserCredential() + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/MapUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/MapUseCaseImpl.swift new file mode 100644 index 00000000..ac9a4998 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/MapUseCaseImpl.swift @@ -0,0 +1,71 @@ +import Foundation + +import DomainInterface +import Infrastructure + +import RxSwift + +public final class MapUseCaseImpl: MapUseCase { + + private let repository: MapRepository + + public init(repository: MapRepository) { + self.repository = repository + } + + public func fetchCategories() -> Observable<[CategoryResponse]> { + return repository.fetchCategories() + } + + public func fetchStoresInBounds( + northEastLat: Double, + northEastLon: Double, + southWestLat: Double, + southWestLon: Double, + categories: [Int] + ) -> Observable<[MapPopUpStore]> { + return repository.fetchStoresInBounds( + northEastLat: northEastLat, + northEastLon: northEastLon, + southWestLat: southWestLat, + southWestLon: southWestLon, + categories: categories + ) + .do(onNext: { stores in + Logger.log("맵 범위 내 스토어 \(stores.count)개 로드됨", category: .debug) + }, onError: { error in + Logger.log("맵 범위 내 스토어 로드 실패: \(error)", category: .error) + }) + } + + public func searchStores( + query: String, + categories: [Int] + ) -> Observable<[MapPopUpStore]> { + return repository.searchStores( + query: query, + categories: categories + ) + .do(onNext: { stores in + Logger.log("'\(query)' 검색 결과 \(stores.count)개 로드됨", category: .debug) + }, onError: { error in + Logger.log("스토어 검색 실패: \(error)", category: .error) + }) + } + + public func filterStoresByLocation(_ stores: [MapPopUpStore], selectedRegions: [String]) -> [MapPopUpStore] { + guard !selectedRegions.isEmpty else { return stores } + + return stores.filter { store in + let components = store.address.components(separatedBy: " ") + guard components.count >= 2 else { return false } + + let mainRegion = components[0].replacingOccurrences(of: "특별시", with: "") + .replacingOccurrences(of: "광역시", with: "") + let subRegion = components[1] + + return selectedRegions.contains("\(mainRegion)전체") || + selectedRegions.contains(subRegion) + } + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PopUpAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PopUpAPIUseCaseImpl.swift new file mode 100644 index 00000000..ee20c061 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PopUpAPIUseCaseImpl.swift @@ -0,0 +1,38 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class PopUpAPIUseCaseImpl: PopUpAPIUseCase { + + private let repository: PopUpAPIRepository + + public init(repository: PopUpAPIRepository) { + self.repository = repository + } + + public func getSearchBottomPopUpList(isOpen: Bool, categories: [Int], page: Int32?, size: Int32, sort: String?) -> Observable { + var categoryString: String? + if !categories.isEmpty { + categoryString = categories.map { String($0) + "," }.reduce("", +) + } + if isOpen { + return repository.getOpenPopUpList(categories: categoryString, page: page, size: size, sort: nil, query: nil, sortCode: sort) + } else { + return repository.getClosePopUpList(categories: categoryString, page: page, size: size, sort: nil, query: nil, sortCode: sort) + } + } + + public func getSearchPopUpList(query: String?) -> Observable { + return repository.getSearchPopUpList(categories: nil, page: nil, size: nil, sort: nil, query: query, sortCode: nil) + } + + public func getPopUpDetail(commentType: String?, popUpStoredId: Int64, isViewCount: Bool? = true) -> Observable { + return repository.getPopUpDetail(commentType: commentType, popUpStoreId: popUpStoredId, viewCountYn: isViewCount) + } + + public func getPopUpComment(commentType: String?, page: Int32?, size: Int32?, sort: String?, popUpStoreId: Int64) -> Observable { + return repository.getPopUpComment(commentType: commentType, page: page, size: size, sort: sort, popUpStoreId: popUpStoreId) + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PreSignedUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PreSignedUseCaseImpl.swift new file mode 100644 index 00000000..c84a23bf --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/PreSignedUseCaseImpl.swift @@ -0,0 +1,23 @@ +import UIKit + +import DomainInterface + +import RxSwift + +public final class PreSignedUseCaseImpl: PreSignedUseCase { + private let repository: PreSignedRepository + + public init(repository: PreSignedRepository) { + self.repository = repository + } + + public func tryUpload(presignedURLRequest: [(filePath: String, image: UIImage)]) -> Single { + return repository.tryUpload(presignedURLRequest: presignedURLRequest) + } + public func tryDelete(objectKeyList: [String]) -> Completable { + return repository.tryDelete(objectKeyList: objectKeyList) + } + public func fullImageURL(from filePath: String) -> URL? { + repository.fullImageURL(from: filePath) + } +} diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/Search/FetchKeywordBasePopupStoreListImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/Search/FetchKeywordBasePopupStoreListImpl.swift new file mode 100644 index 00000000..4b723585 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/Search/FetchKeywordBasePopupStoreListImpl.swift @@ -0,0 +1,18 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class FetchKeywordBasePopupListUseCaseImpl: FetchKeywordBasePopupListUseCase { + + private let repository: SearchAPIRepository + + public init(repository: SearchAPIRepository) { + self.repository = repository + } + + public func execute(keyword: String) -> Observable { + return repository.fetchSearchResult(by: keyword) + } +} diff --git a/Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/SignUpAPIUseCaseImpl.swift similarity index 64% rename from Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift rename to Poppool/DomainLayer/Domain/Domain/UseCaseImpl/SignUpAPIUseCaseImpl.swift index e67b1868..f1b3497e 100644 --- a/Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/SignUpAPIUseCaseImpl.swift @@ -1,26 +1,23 @@ -// -// SignUpAPIUseCaseImpl.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - import Foundation + +import DomainInterface + import RxSwift -final class SignUpAPIUseCaseImpl { - var repository: SignUpRepositoryImpl - - init(repository: SignUpRepositoryImpl) { +public final class SignUpAPIUseCaseImpl: SignUpAPIUseCase { + private let repository: SignUpRepository + + public init(repository: SignUpRepository) { self.repository = repository } - func trySignUp( + + public func trySignUp( nickName: String, gender: String, age: Int32, socialEmail: String, socialType: String, - interests: [Int64], + interests: [Int], appleAuthorizationCode: String? ) -> Completable { return repository.trySignUp( @@ -33,11 +30,12 @@ final class SignUpAPIUseCaseImpl { appleAuthorizationCode: appleAuthorizationCode ) } - func checkNickName(nickName: String) -> Observable { + + public func checkNickName(nickName: String) -> Observable { return repository.checkNickName(nickName: nickName) } - - func fetchCategoryList() -> Observable<[Category]> { + + public func fetchCategoryList() -> Observable<[CategoryResponse]> { return repository.fetchCategoryList() } } diff --git a/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/UserAPIUseCaseImpl.swift b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/UserAPIUseCaseImpl.swift new file mode 100644 index 00000000..b42fed78 --- /dev/null +++ b/Poppool/DomainLayer/Domain/Domain/UseCaseImpl/UserAPIUseCaseImpl.swift @@ -0,0 +1,117 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public final class UserAPIUseCaseImpl: UserAPIUseCase { + private let repository: UserAPIRepository + + public init(repository: UserAPIRepository) { + self.repository = repository + } + + public func postBookmarkPopUp(popUpID: Int64) -> Completable { + return repository.postBookmarkPopUp(popUpStoreId: popUpID) + } + + public func deleteBookmarkPopUp(popUpID: Int64) -> Completable { + return repository.deleteBookmarkPopUp(popUpStoreId: popUpID) + } + + public func postCommentLike(commentId: Int64) -> Completable { + return repository.postCommentLike(commentId: commentId) + } + + public func deleteCommentLike(commentId: Int64) -> Completable { + return repository.deleteCommentLike(commentId: commentId) + } + + public func postUserBlock(blockedUserId: String?) -> Completable { + return repository.postUserBlock(blockedUserId: blockedUserId) + } + + public func deleteUserBlock(blockedUserId: String?) -> Completable { + return repository.deleteUserBlock(blockedUserId: blockedUserId) + } + + public func getOtherUserCommentedPopUpList( + commenterId: String?, + commentType: String?, + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable { + return repository.getOtherUserCommentList( + commenterId: commenterId, + commentType: commentType, + page: page, + size: size, + sort: sort + ) + } + + public func getMyPage() -> Observable { + return repository.getMyPage() + } + + public func postLogout() -> Completable { + return repository.postLogout() + } + + public func getWithdrawlList() -> Observable { + return repository.getWithdrawlList() + } + + public func postWithdrawl(surveyList: [GetWithdrawlListDataResponse]) -> Completable { + return repository.postWithdrawl(list: surveyList.map { ($0.id, $0.survey)}) + } + + public func getMyProfile() -> Observable { + return repository.getMyProfile() + } + + public func putUserTailoredInfo(gender: String?, age: Int32) -> Completable { + return repository.putUserTailoredInfo(gender: gender, age: age) + } + + public func putUserCategory( + interestCategoriesToAdd: [Int], + interestCategoriesToDelete: [Int], + interestCategoriesToKeep: [Int] + ) -> Completable { + return repository.putUserCategory( + interestCategoriesToAdd: interestCategoriesToAdd, + interestCategoriesToDelete: interestCategoriesToDelete, + interestCategoriesToKeep: interestCategoriesToKeep + ) + } + + public func putUserProfile(profileImageUrl: String?, nickname: String?, email: String?, instagramId: String?, intro: String?) -> Completable { + return repository.putUserProfile(profileImageUrl: profileImageUrl, nickname: nickname, email: email, instagramId: instagramId, intro: intro) + } + + public func getMyCommentedPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + return repository.getMyCommentedPopUp(page: page, size: size, sort: sort) + } + + public func getBlockUserList(page: Int32?, size: Int32?, sort: String?) -> Observable { + return repository.getBlockUserList(page: page, size: size, sort: sort) + } + + public func getNoticeList() -> Observable { + return repository.getNoticeList() + } + + public func getNoticeDetail(noticeID: Int64) -> Observable { + return repository.getNoticeDetail(noticeID: noticeID) + } + + public func getRecentPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + return repository.getRecentPopUp(page: page, size: size, sort: sort) + } + + public func getBookmarkPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { + return repository.getBookmarkPopUp(page: page, size: size, sort: sort) + } +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStore.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStore.swift new file mode 100644 index 00000000..e57f78fc --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStore.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct AdminStore { + public init(id: Int64, name: String, categoryName: String, mainImageUrl: String) { + self.id = id + self.name = name + self.categoryName = categoryName + self.mainImageUrl = mainImageUrl + } + + public let id: Int64 + public let name: String + public let categoryName: String + public let mainImageUrl: String +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStoreDetail.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStoreDetail.swift new file mode 100644 index 00000000..fe4a5a2e --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/AdminStoreDetail.swift @@ -0,0 +1,51 @@ +import Foundation + +public struct AdminStoreDetail { + public init(id: Int64, name: String, categoryId: Int, categoryName: String, description: String, address: String, startDate: String, endDate: String, createUserId: String, createDateTime: String, mainImageUrl: String, bannerYn: Bool, images: [StoreImage], latitude: Double, longitude: Double, markerTitle: String, markerSnippet: String) { + self.id = id + self.name = name + self.categoryId = categoryId + self.categoryName = categoryName + self.description = description + self.address = address + self.startDate = startDate + self.endDate = endDate + self.createUserId = createUserId + self.createDateTime = createDateTime + self.mainImageUrl = mainImageUrl + self.bannerYn = bannerYn + self.images = images + self.latitude = latitude + self.longitude = longitude + self.markerTitle = markerTitle + self.markerSnippet = markerSnippet + } + + public let id: Int64 + public let name: String + public let categoryId: Int + public let categoryName: String + public let description: String + public let address: String + public let startDate: String + public let endDate: String + public let createUserId: String + public let createDateTime: String + public let mainImageUrl: String + public let bannerYn: Bool + public let images: [StoreImage] + public let latitude: Double + public let longitude: Double + public let markerTitle: String + public let markerSnippet: String + + public struct StoreImage { + public init(id: Int64, imageUrl: String) { + self.id = id + self.imageUrl = imageUrl + } + + public let id: Int64 + public let imageUrl: String + } +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/Params/AdminParams.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/Params/AdminParams.swift new file mode 100644 index 00000000..8e5f46fe --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AdminResponse/Params/AdminParams.swift @@ -0,0 +1,83 @@ +import Foundation + +public struct CreateStoreParams { + public init(name: String, categoryId: Int, desc: String, address: String, startDate: String, endDate: String, mainImageUrl: String, imageUrlList: [String?], latitude: Double, longitude: Double, markerTitle: String, markerSnippet: String, startDateBeforeEndDate: Bool) { + self.name = name + self.categoryId = categoryId + self.desc = desc + self.address = address + self.startDate = startDate + self.endDate = endDate + self.mainImageUrl = mainImageUrl + self.imageUrlList = imageUrlList + self.latitude = latitude + self.longitude = longitude + self.markerTitle = markerTitle + self.markerSnippet = markerSnippet + self.startDateBeforeEndDate = startDateBeforeEndDate + } + + public let name: String + public let categoryId: Int + public let desc: String + public let address: String + public let startDate: String + public let endDate: String + public let mainImageUrl: String + public let imageUrlList: [String?] + public let latitude: Double + public let longitude: Double + public let markerTitle: String + public let markerSnippet: String + public let startDateBeforeEndDate: Bool +} + +public struct UpdateStoreParams { + public init(id: Int64, name: String, categoryId: Int, desc: String, address: String, startDate: String, endDate: String, mainImageUrl: String, imageUrlList: [String?], imagesToDelete: [Int64], latitude: Double, longitude: Double, markerTitle: String, markerSnippet: String, startDateBeforeEndDate: Bool) { + self.id = id + self.name = name + self.categoryId = categoryId + self.desc = desc + self.address = address + self.startDate = startDate + self.endDate = endDate + self.mainImageUrl = mainImageUrl + self.imageUrlList = imageUrlList + self.imagesToDelete = imagesToDelete + self.latitude = latitude + self.longitude = longitude + self.markerTitle = markerTitle + self.markerSnippet = markerSnippet + self.startDateBeforeEndDate = startDateBeforeEndDate + } + + public let id: Int64 + public let name: String + public let categoryId: Int + public let desc: String + public let address: String + public let startDate: String + public let endDate: String + public let mainImageUrl: String + public let imageUrlList: [String?] + public let imagesToDelete: [Int64] + public let latitude: Double + public let longitude: Double + public let markerTitle: String + public let markerSnippet: String + public let startDateBeforeEndDate: Bool +} + +public struct CreateNoticeParams { + public let title: String + public let content: String + public let imageUrlList: [String] +} + +public struct UpdateNoticeParams { + public let id: Int64 + public let title: String + public let content: String + public let imageUrlList: [String] + public let imagesToDelete: [Int64] +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/AuthServiceResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/AuthServiceResponse.swift new file mode 100644 index 00000000..5150eb84 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/AuthServiceResponse.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct AuthServiceResponse: Encodable { + public init(idToken: String? = nil, authorizationCode: String? = nil, kakaoUserId: Int64? = nil, kakaoAccessToken: String? = nil) { + self.idToken = idToken + self.authorizationCode = authorizationCode + self.kakaoUserId = kakaoUserId + self.kakaoAccessToken = kakaoAccessToken + } + + public var idToken: String? + public var authorizationCode: String? + public var kakaoUserId: Int64? + public var kakaoAccessToken: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/CategoryResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/CategoryResponse.swift new file mode 100644 index 00000000..84810945 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/CategoryResponse.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct CategoryResponse { + public init(categoryId: Int, category: String) { + self.categoryId = categoryId + self.category = category + } + + public let categoryId: Int + public let category: String +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/LoginResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/LoginResponse.swift new file mode 100644 index 00000000..94e6f175 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/LoginResponse.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct LoginResponse { + public init(userId: String, grantType: String, accessToken: String, refreshToken: String, accessTokenExpiresAt: String, refreshTokenExpiresAt: String, socialType: String, isRegisteredUser: Bool) { + self.userId = userId + self.grantType = grantType + self.accessToken = accessToken + self.refreshToken = refreshToken + self.accessTokenExpiresAt = accessTokenExpiresAt + self.refreshTokenExpiresAt = refreshTokenExpiresAt + self.socialType = socialType + self.isRegisteredUser = isRegisteredUser + } + + public var userId: String + var grantType: String + public var accessToken: String + public var refreshToken: String + var accessTokenExpiresAt: String + var refreshTokenExpiresAt: String + public var socialType: String + public var isRegisteredUser: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/PostTokenReissueResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/PostTokenReissueResponse.swift new file mode 100644 index 00000000..39c7724d --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/AuthResponse/PostTokenReissueResponse.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PostTokenReissueResponse { + public init(accessToken: String? = nil, refreshToken: String? = nil, accessTokenExpiresAt: String? = nil, refreshTokenExpiresAt: String? = nil) { + self.accessToken = accessToken + self.refreshToken = refreshToken + self.accessTokenExpiresAt = accessTokenExpiresAt + self.refreshTokenExpiresAt = refreshTokenExpiresAt + } + + public var accessToken: String? + public var refreshToken: String? + public var accessTokenExpiresAt: String? + public var refreshTokenExpiresAt: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/BannerPopUpStore.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/BannerPopUpStore.swift new file mode 100644 index 00000000..2c5efa15 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/BannerPopUpStore.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct BannerPopUpStore { + public init(id: Int64, name: String, mainImageUrl: String) { + self.id = id + self.name = name + self.mainImageUrl = mainImageUrl + } + + public var id: Int64 + public var name: String + public var mainImageUrl: String +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/GetHomeInfoResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/GetHomeInfoResponse.swift new file mode 100644 index 00000000..b3ee4e93 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/HomeResponse/GetHomeInfoResponse.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct GetHomeInfoResponse { + public init(bannerPopUpStoreList: [BannerPopUpStore], nickname: String? = nil, customPopUpStoreList: [PopUpStoreResponse], customPopUpStoreTotalPages: Int32, customPopUpStoreTotalElements: Int64, popularPopUpStoreList: [PopUpStoreResponse], popularPopUpStoreTotalPages: Int32, popularPopUpStoreTotalElements: Int64, newPopUpStoreList: [PopUpStoreResponse], newPopUpStoreTotalPages: Int32, newPopUpStoreTotalElements: Int64, loginYn: Bool) { + self.bannerPopUpStoreList = bannerPopUpStoreList + self.nickname = nickname + self.customPopUpStoreList = customPopUpStoreList + self.customPopUpStoreTotalPages = customPopUpStoreTotalPages + self.customPopUpStoreTotalElements = customPopUpStoreTotalElements + self.popularPopUpStoreList = popularPopUpStoreList + self.popularPopUpStoreTotalPages = popularPopUpStoreTotalPages + self.popularPopUpStoreTotalElements = popularPopUpStoreTotalElements + self.newPopUpStoreList = newPopUpStoreList + self.newPopUpStoreTotalPages = newPopUpStoreTotalPages + self.newPopUpStoreTotalElements = newPopUpStoreTotalElements + self.loginYn = loginYn + } + + public var bannerPopUpStoreList: [BannerPopUpStore] + public var nickname: String? + public var customPopUpStoreList: [PopUpStoreResponse] + public var customPopUpStoreTotalPages: Int32 + var customPopUpStoreTotalElements: Int64 + public var popularPopUpStoreList: [PopUpStoreResponse] + public var popularPopUpStoreTotalPages: Int32 + var popularPopUpStoreTotalElements: Int64 + public var newPopUpStoreList: [PopUpStoreResponse] + public var newPopUpStoreTotalPages: Int32 + var newPopUpStoreTotalElements: Int64 + public var loginYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/MapResponse/MapPopUpStore.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/MapResponse/MapPopUpStore.swift new file mode 100644 index 00000000..16fcecb3 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/MapResponse/MapPopUpStore.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct MapPopUpStore: Equatable { + public init(id: Int64, category: String, name: String, address: String, startDate: String, endDate: String, latitude: Double, longitude: Double, markerId: Int64, markerTitle: String, markerSnippet: String, mainImageUrl: String?) { + self.id = id + self.category = category + self.name = name + self.address = address + self.startDate = startDate + self.endDate = endDate + self.latitude = latitude + self.longitude = longitude + self.markerId = markerId + self.markerTitle = markerTitle + self.markerSnippet = markerSnippet + self.mainImageUrl = mainImageUrl + } + + public let id: Int64 + public let category: String + public let name: String + public let address: String + public let startDate: String + public let endDate: String + public let latitude: Double + public let longitude: Double + public let markerId: Int64 + public let markerTitle: String + public let markerSnippet: String + public let mainImageUrl: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpCommentResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpCommentResponse.swift new file mode 100644 index 00000000..ae460a5a --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpCommentResponse.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct GetPopUpCommentResponse { + public init(commentList: [GetPopUpDetailCommentResponse]) { + self.commentList = commentList + } + + public let commentList: [GetPopUpDetailCommentResponse] +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDetailResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDetailResponse.swift new file mode 100644 index 00000000..fcb8137c --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDetailResponse.swift @@ -0,0 +1,89 @@ +import Foundation + +public struct GetPopUpDetailResponse { + public init(name: String?, desc: String?, startDate: String?, endDate: String?, startTime: String?, endTime: String?, address: String?, commentCount: Int64, bookmarkYn: Bool, loginYn: Bool, hasCommented: Bool, mainImageUrl: String?, imageList: [GetPopUpDetailImageResponse], commentList: [GetPopUpDetailCommentResponse], similarPopUpStoreList: [GetPopUpDetailSimilarResponse]) { + self.name = name + self.desc = desc + self.startDate = startDate + self.endDate = endDate + self.startTime = startTime + self.endTime = endTime + self.address = address + self.commentCount = commentCount + self.bookmarkYn = bookmarkYn + self.loginYn = loginYn + self.hasCommented = hasCommented + self.mainImageUrl = mainImageUrl + self.imageList = imageList + self.commentList = commentList + self.similarPopUpStoreList = similarPopUpStoreList + } + + public let name: String? + public let desc: String? + public let startDate: String? + public let endDate: String? + public let startTime: String? + public let endTime: String? + public let address: String? + public let commentCount: Int64 + public let bookmarkYn: Bool + public let loginYn: Bool + public let hasCommented: Bool + public let mainImageUrl: String? + public let imageList: [GetPopUpDetailImageResponse] + public let commentList: [GetPopUpDetailCommentResponse] + public let similarPopUpStoreList: [GetPopUpDetailSimilarResponse] +} + +public struct GetPopUpDetailImageResponse { + public init(id: Int64, imageUrl: String?) { + self.id = id + self.imageUrl = imageUrl + } + + public let id: Int64 + public let imageUrl: String? +} + +public struct GetPopUpDetailCommentResponse { + public init(commentId: Int64, creator: String?, nickname: String?, instagramId: String?, profileImageUrl: String?, content: String?, likeYn: Bool, likeCount: Int64, myCommentYn: Bool, createDateTime: String?, commentImageList: [GetPopUpDetailImageResponse]) { + self.commentId = commentId + self.creator = creator + self.nickname = nickname + self.instagramId = instagramId + self.profileImageUrl = profileImageUrl + self.content = content + self.likeYn = likeYn + self.likeCount = likeCount + self.myCommentYn = myCommentYn + self.createDateTime = createDateTime + self.commentImageList = commentImageList + } + + public let commentId: Int64 + public let creator: String? + public let nickname: String? + public let instagramId: String? + public let profileImageUrl: String? + public let content: String? + public let likeYn: Bool + public let likeCount: Int64 + public let myCommentYn: Bool + public let createDateTime: String? + public let commentImageList: [GetPopUpDetailImageResponse] +} + +public struct GetPopUpDetailSimilarResponse { + public init(id: Int64, name: String?, mainImageUrl: String?, endDate: String?) { + self.id = id + self.name = name + self.mainImageUrl = mainImageUrl + self.endDate = endDate + } + + public let id: Int64 + public let name: String? + public let mainImageUrl: String? + public let endDate: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDirectionResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDirectionResponse.swift new file mode 100644 index 00000000..60157404 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetPopUpDirectionResponse.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct GetPopUpDirectionResponse { + public init(id: Int64, categoryName: String, name: String, address: String, startDate: String, endDate: String, latitude: Double, longitude: Double, markerId: Int64, markerTitle: String, markerSnippet: String) { + self.id = id + self.categoryName = categoryName + self.name = name + self.address = address + self.startDate = startDate + self.endDate = endDate + self.latitude = latitude + self.longitude = longitude + self.markerId = markerId + self.markerTitle = markerTitle + self.markerSnippet = markerSnippet + } + + public let id: Int64 + public let categoryName: String + public let name: String + public let address: String + public let startDate: String + public let endDate: String + public let latitude: Double + public let longitude: Double + public let markerId: Int64 + public let markerTitle: String + public let markerSnippet: String +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchBottomPopUpListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchBottomPopUpListResponse.swift new file mode 100644 index 00000000..acaab501 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchBottomPopUpListResponse.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct GetSearchBottomPopUpListResponse { + public init(popUpStoreList: [PopUpStoreResponse], loginYn: Bool, totalPages: Int32, totalElements: Int64) { + self.popUpStoreList = popUpStoreList + self.loginYn = loginYn + self.totalPages = totalPages + self.totalElements = totalElements + } + + public var popUpStoreList: [PopUpStoreResponse] + public var loginYn: Bool + public var totalPages: Int32 + public var totalElements: Int64 +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchPopUpListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchPopUpListResponse.swift new file mode 100644 index 00000000..93c13fa5 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/GetSearchPopUpListResponse.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct GetSearchPopUpListResponse { + public init(popUpStoreList: [PopUpStoreResponse], loginYn: Bool) { + self.popUpStoreList = popUpStoreList + self.loginYn = loginYn + } + + public var popUpStoreList: [PopUpStoreResponse] + public var loginYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/PopUpStoreResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/PopUpStoreResponse.swift new file mode 100644 index 00000000..5c487da6 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/PopUpResponse/PopUpStoreResponse.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct PopUpStoreResponse { + public init(id: Int64, category: String?, name: String?, address: String?, mainImageUrl: String?, startDate: String?, endDate: String?, bookmarkYn: Bool) { + self.id = id + self.category = category + self.name = name + self.address = address + self.mainImageUrl = mainImageUrl + self.startDate = startDate + self.endDate = endDate + self.bookmarkYn = bookmarkYn + } + + public let id: Int64 + public let category: String? + public let name: String? + public let address: String? + public let mainImageUrl: String? + public let startDate: String? + public let endDate: String? + public let bookmarkYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/SearchResponse/KeywordBasePopupStoreListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/SearchResponse/KeywordBasePopupStoreListResponse.swift new file mode 100644 index 00000000..b1c60718 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/SearchResponse/KeywordBasePopupStoreListResponse.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct KeywordBasePopupStoreListResponse { + public init(popupStoreList: [PopUpStoreResponse], loginYn: Bool) { + self.popupStoreList = popupStoreList + self.loginYn = loginYn + } + + public var popupStoreList: [PopUpStoreResponse] + public var loginYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetBlockUserListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetBlockUserListResponse.swift new file mode 100644 index 00000000..a63aaf7f --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetBlockUserListResponse.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct GetBlockUserListResponse { + public init(blockedUserInfoList: [GetBlockUserListDataResponse], totalPages: Int32, totalElements: Int32) { + self.blockedUserInfoList = blockedUserInfoList + self.totalPages = totalPages + self.totalElements = totalElements + } + + public var blockedUserInfoList: [GetBlockUserListDataResponse] + public var totalPages: Int32 + public var totalElements: Int32 +} + +public struct GetBlockUserListDataResponse { + public init(userId: String? = nil, profileImageUrl: String? = nil, nickname: String? = nil, instagramId: String? = nil) { + self.userId = userId + self.profileImageUrl = profileImageUrl + self.nickname = nickname + self.instagramId = instagramId + } + + public var userId: String? + public var profileImageUrl: String? + public var nickname: String? + public var instagramId: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyCommentResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyCommentResponse.swift new file mode 100644 index 00000000..0f1ecacf --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyCommentResponse.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct GetMyCommentedPopUpResponse { + public init(popUpInfoList: [GetMyCommentedPopUpDataResponse]) { + self.popUpInfoList = popUpInfoList + } + + public var popUpInfoList: [GetMyCommentedPopUpDataResponse] +} + +public struct GetMyCommentedPopUpDataResponse { + public init(popUpStoreId: Int64, popUpStoreName: String? = nil, desc: String? = nil, mainImageUrl: String? = nil, startDate: String? = nil, endDate: String? = nil, address: String? = nil, closedYn: Bool) { + self.popUpStoreId = popUpStoreId + self.popUpStoreName = popUpStoreName + self.desc = desc + self.mainImageUrl = mainImageUrl + self.startDate = startDate + self.endDate = endDate + self.address = address + self.closedYn = closedYn + } + + public var popUpStoreId: Int64 + public var popUpStoreName: String? + public var desc: String? + public var mainImageUrl: String? + public var startDate: String? + public var endDate: String? + public var address: String? + public var closedYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyPageResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyPageResponse.swift new file mode 100644 index 00000000..47bfb54b --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyPageResponse.swift @@ -0,0 +1,33 @@ +import Foundation + +public struct GetMyPageResponse { + public init(nickname: String? = nil, profileImageUrl: String? = nil, intro: String? = nil, instagramId: String? = nil, loginYn: Bool, adminYn: Bool, myCommentedPopUpList: [GetMyPagePopUpResponse]) { + self.nickname = nickname + self.profileImageUrl = profileImageUrl + self.intro = intro + self.instagramId = instagramId + self.loginYn = loginYn + self.adminYn = adminYn + self.myCommentedPopUpList = myCommentedPopUpList + } + + public var nickname: String? + public var profileImageUrl: String? + public var intro: String? + public var instagramId: String? + public var loginYn: Bool + public var adminYn: Bool + public var myCommentedPopUpList: [GetMyPagePopUpResponse] +} + +public struct GetMyPagePopUpResponse { + public init(popUpStoreId: Int64, popUpStoreName: String? = nil, mainImageUrl: String? = nil) { + self.popUpStoreId = popUpStoreId + self.popUpStoreName = popUpStoreName + self.mainImageUrl = mainImageUrl + } + + public var popUpStoreId: Int64 + public var popUpStoreName: String? + public var mainImageUrl: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyProfileResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyProfileResponse.swift new file mode 100644 index 00000000..671433c8 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetMyProfileResponse.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct GetMyProfileResponse { + public init(profileImageUrl: String? = nil, nickname: String? = nil, email: String? = nil, instagramId: String? = nil, intro: String? = nil, gender: String? = nil, age: Int32, interestCategoryList: [CategoryResponse]) { + self.profileImageUrl = profileImageUrl + self.nickname = nickname + self.email = email + self.instagramId = instagramId + self.intro = intro + self.gender = gender + self.age = age + self.interestCategoryList = interestCategoryList + } + + public var profileImageUrl: String? + public var nickname: String? + public var email: String? + public var instagramId: String? + public var intro: String? + public var gender: String? + public var age: Int32 + public var interestCategoryList: [CategoryResponse] +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeDetailResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeDetailResponse.swift new file mode 100644 index 00000000..a1e20371 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeDetailResponse.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct GetNoticeDetailResponse { + public init(id: Int64, title: String? = nil, content: String? = nil, createDateTime: String? = nil) { + self.id = id + self.title = title + self.content = content + self.createDateTime = createDateTime + } + + var id: Int64 + public var title: String? + public var content: String? + public var createDateTime: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeListResponse.swift new file mode 100644 index 00000000..fe085bfc --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetNoticeListResponse.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct GetNoticeListResponse { + public init(noticeInfoList: [GetNoticeListDataResponse]) { + self.noticeInfoList = noticeInfoList + } + + public var noticeInfoList: [GetNoticeListDataResponse] +} + +public struct GetNoticeListDataResponse { + public init(id: Int64, title: String? = nil, createdDateTime: String? = nil) { + self.id = id + self.title = title + self.createdDateTime = createdDateTime + } + + public var id: Int64 + public var title: String? + public var createdDateTime: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetOtherUserCommentedPopUpListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetOtherUserCommentedPopUpListResponse.swift new file mode 100644 index 00000000..65c2f463 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetOtherUserCommentedPopUpListResponse.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct GetOtherUserCommentedPopUpListResponse { + public init(popUpInfoList: [GetOtherUserCommentedPopUpResponse]) { + self.popUpInfoList = popUpInfoList + } + + public var popUpInfoList: [GetOtherUserCommentedPopUpResponse] +} + +public struct GetOtherUserCommentedPopUpResponse { + public init(popUpStoreId: Int64, popUpStoreName: String? = nil, desc: String? = nil, mainImageUrl: String? = nil, startDate: String? = nil, endDate: String? = nil, address: String? = nil, closedYn: Bool) { + self.popUpStoreId = popUpStoreId + self.popUpStoreName = popUpStoreName + self.desc = desc + self.mainImageUrl = mainImageUrl + self.startDate = startDate + self.endDate = endDate + self.address = address + self.closedYn = closedYn + } + + public var popUpStoreId: Int64 + public var popUpStoreName: String? + public var desc: String? + public var mainImageUrl: String? + public var startDate: String? + public var endDate: String? + public var address: String? + public var closedYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetRecentPopUpResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetRecentPopUpResponse.swift new file mode 100644 index 00000000..0e84a1f6 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetRecentPopUpResponse.swift @@ -0,0 +1,35 @@ +import Foundation + +public struct GetRecentPopUpResponse { + public init(popUpInfoList: [GetRecentPopUpDataResponse], totalPages: Int32, totalElements: Int32) { + self.popUpInfoList = popUpInfoList + self.totalPages = totalPages + self.totalElements = totalElements + } + + public var popUpInfoList: [GetRecentPopUpDataResponse] + public var totalPages: Int32 + public var totalElements: Int32 +} + +public struct GetRecentPopUpDataResponse { + public init(popUpStoreId: Int64, popUpStoreName: String? = nil, desc: String? = nil, mainImageUrl: String? = nil, startDate: String? = nil, endDate: String? = nil, address: String? = nil, closeYn: Bool) { + self.popUpStoreId = popUpStoreId + self.popUpStoreName = popUpStoreName + self.desc = desc + self.mainImageUrl = mainImageUrl + self.startDate = startDate + self.endDate = endDate + self.address = address + self.closeYn = closeYn + } + + public var popUpStoreId: Int64 + public var popUpStoreName: String? + public var desc: String? + public var mainImageUrl: String? + public var startDate: String? + public var endDate: String? + public var address: String? + public var closeYn: Bool +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetWithdrawlListResponse.swift b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetWithdrawlListResponse.swift new file mode 100644 index 00000000..830001e0 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Entity/UserResponse/GetWithdrawlListResponse.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct GetWithdrawlListResponse { + public init(withDrawlSurveyList: [GetWithdrawlListDataResponse]) { + self.withDrawlSurveyList = withDrawlSurveyList + } + + public var withDrawlSurveyList: [GetWithdrawlListDataResponse] +} + +public struct GetWithdrawlListDataResponse { + public init(id: Int64, survey: String? = nil) { + self.id = id + self.survey = survey + } + + public var id: Int64 + public var survey: String? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/AdminRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AdminRepository.swift new file mode 100644 index 00000000..4a9f2972 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AdminRepository.swift @@ -0,0 +1,18 @@ +import Foundation + +import RxSwift + +public protocol AdminRepository { + func fetchStoreList(query: String?, page: Int, size: Int) -> Observable<[AdminStore]> + func fetchStoreDetail(id: Int64) -> Observable + + func createStore(params: CreateStoreParams) -> Completable + + func updateStore(params: UpdateStoreParams) -> Completable + + func deleteStore(id: Int64) -> Completable + + func createNotice(params: CreateNoticeParams) -> Completable + func updateNotice(params: UpdateNoticeParams) -> Completable + func deleteNotice(id: Int64) -> Completable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/AppleLoginRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AppleLoginRepository.swift new file mode 100644 index 00000000..6ba5676b --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AppleLoginRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol AppleLoginRepository { + func fetchUserCredential() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/AuthAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AuthAPIRepository.swift new file mode 100644 index 00000000..33154753 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/AuthAPIRepository.swift @@ -0,0 +1,8 @@ +import Foundation + +import RxSwift + +public protocol AuthAPIRepository { + func tryLogIn(userCredential: Encodable, socialType: String) -> Observable + func postTokenReissue() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/CategoryRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/CategoryRepository.swift new file mode 100644 index 00000000..1df92582 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/CategoryRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol CategoryRepository { + func fetchCategoryList() -> Observable<[CategoryResponse]> +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/CommentAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/CommentAPIRepository.swift new file mode 100644 index 00000000..64fc0fbd --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/CommentAPIRepository.swift @@ -0,0 +1,24 @@ +import Foundation + +import RxSwift + +public protocol CommentAPIRepository { + func postCommentAdd( + popUpStoreId: Int64, + content: String?, + commentType: String?, + imageUrlList: [String?] + ) -> Completable + + func deleteComment( + popUpStoreId: Int64, + commentId: Int64 + ) -> Completable + + func editComment( + popUpStoreId: Int64, + commentId: Int64, + content: String?, + imageUrlList: [String?]? + ) -> Completable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/HomeAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/HomeAPIRepository.swift new file mode 100644 index 00000000..d417f03a --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/HomeAPIRepository.swift @@ -0,0 +1,10 @@ +import Foundation + +import RxSwift + +public protocol HomeAPIRepository { + func fetchHome(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchCustomPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchNewPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchPopularPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/KakaoLoginRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/KakaoLoginRepository.swift new file mode 100644 index 00000000..4be6e2ca --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/KakaoLoginRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol KakaoLoginRepository { + func fetchUserCredential() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapDirectionRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapDirectionRepository.swift new file mode 100644 index 00000000..ba17d266 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapDirectionRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol MapDirectionRepository { + func getPopUpDirection(popUpStoreId: Int64) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapRepository.swift new file mode 100644 index 00000000..cf86192f --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/MapRepository.swift @@ -0,0 +1,20 @@ +import Foundation + +import RxSwift + +public protocol MapRepository { + func fetchStoresInBounds( + northEastLat: Double, + northEastLon: Double, + southWestLat: Double, + southWestLon: Double, + categories: [Int] + ) -> Observable<[MapPopUpStore]> + + func searchStores( + query: String, + categories: [Int] + ) -> Observable<[MapPopUpStore]> + + func fetchCategories() -> Observable<[CategoryResponse]> +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/PopUpAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/PopUpAPIRepository.swift new file mode 100644 index 00000000..b6c000ec --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/PopUpAPIRepository.swift @@ -0,0 +1,44 @@ +import Foundation + +import RxSwift + +public protocol PopUpAPIRepository { + func postBookmarkPopUp(popUpStoreId: Int64) -> Completable + + func getClosePopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable + + func getOpenPopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable + + func getSearchPopUpList( + categories: String?, + page: Int32?, + size: Int32?, + sort: String?, + query: String?, + sortCode: String? + ) -> Observable + + func getPopUpDetail(commentType: String?, popUpStoreId: Int64, viewCountYn: Bool?) -> Observable + + func getPopUpComment( + commentType: String?, + page: Int32?, + size: Int32?, + sort: String?, + popUpStoreId: Int64 + ) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/PreSignedRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/PreSignedRepository.swift new file mode 100644 index 00000000..483becff --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/PreSignedRepository.swift @@ -0,0 +1,9 @@ +import UIKit + +import RxSwift + +public protocol PreSignedRepository { + func tryUpload(presignedURLRequest: [(filePath: String, image: UIImage)]) -> Single + func tryDelete(objectKeyList: [String]) -> Completable + func fullImageURL(from filePath: String) -> URL? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/SearchAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/SearchAPIRepository.swift new file mode 100644 index 00000000..4d428540 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/SearchAPIRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol SearchAPIRepository { + func fetchSearchResult(by query: String) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/SignUpRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/SignUpRepository.swift new file mode 100644 index 00000000..5aca1507 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/SignUpRepository.swift @@ -0,0 +1,17 @@ +import Foundation + +import RxSwift + +public protocol SignUpRepository { + func checkNickName(nickName: String) -> Observable + func fetchCategoryList() -> Observable<[CategoryResponse]> + func trySignUp( + nickName: String, + gender: String, + age: Int32, + socialEmail: String, + socialType: String, + interests: [Int], + appleAuthorizationCode: String? + ) -> Completable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/Repository/UserAPIRepository.swift b/Poppool/DomainLayer/Domain/DomainInterface/Repository/UserAPIRepository.swift new file mode 100644 index 00000000..ba4f8e6c --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/Repository/UserAPIRepository.swift @@ -0,0 +1,49 @@ +import Foundation + +import RxSwift + +public protocol UserAPIRepository { + func postBookmarkPopUp(popUpStoreId: Int64) -> Completable + func deleteBookmarkPopUp(popUpStoreId: Int64) -> Completable + func postCommentLike(commentId: Int64) -> Completable + func deleteCommentLike(commentId: Int64) -> Completable + func postUserBlock(blockedUserId: String?) -> Completable + func deleteUserBlock(blockedUserId: String?) -> Completable + + func getOtherUserCommentList( + commenterId: String?, + commentType: String?, + page: Int32?, + size: Int32?, + sort: String? + ) -> Observable + + func getMyPage() -> Observable + func postLogout() -> Completable + func getWithdrawlList() -> Observable + func postWithdrawl(list: [(Int64, String?)]) -> Completable + + func getMyProfile() -> Observable + func putUserTailoredInfo(gender: String?, age: Int32) -> Completable + + func putUserCategory( + interestCategoriesToAdd: [Int], + interestCategoriesToDelete: [Int], + interestCategoriesToKeep: [Int] + ) -> Completable + + func putUserProfile( + profileImageUrl: String?, + nickname: String?, + email: String?, + instagramId: String?, + intro: String? + ) -> Completable + + func getMyCommentedPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func getBlockUserList(page: Int32?, size: Int32?, sort: String?) -> Observable + func getNoticeList() -> Observable + func getNoticeDetail(noticeID: Int64) -> Observable + func getRecentPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func getBookmarkPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AdminUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AdminUseCase.swift new file mode 100644 index 00000000..5e083d73 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AdminUseCase.swift @@ -0,0 +1,19 @@ +import Foundation + +import RxSwift + +public protocol AdminUseCase { + + func fetchStoreList(query: String?, page: Int, size: Int) -> Observable<[AdminStore]> + func fetchStoreDetail(id: Int64) -> Observable + + func createStore(params: CreateStoreParams) -> Completable + + func updateStore(params: UpdateStoreParams) -> Completable + + func deleteStore(id: Int64) -> Completable + + func createNotice(params: CreateNoticeParams) -> Completable + func updateNotice(params: UpdateNoticeParams) -> Completable + func deleteNotice(id: Int64) -> Completable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AppleLoginUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AppleLoginUseCase.swift new file mode 100644 index 00000000..8841534c --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AppleLoginUseCase.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol AppleLoginUseCase { + func fetchUserCredential() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AuthAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AuthAPIUseCase.swift new file mode 100644 index 00000000..44ce761c --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/AuthAPIUseCase.swift @@ -0,0 +1,8 @@ +import Foundation + +import RxSwift + +public protocol AuthAPIUseCase { + func postTryLogin(userCredential: Encodable, socialType: String) -> Observable + func postTokenReissue() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/CommentAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/CommentAPIUseCase.swift new file mode 100644 index 00000000..5114f0d3 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/CommentAPIUseCase.swift @@ -0,0 +1,9 @@ +import Foundation + +import RxSwift + +public protocol CommentAPIUseCase { + func postCommentAdd(popUpStoreId: Int64, content: String?, commentType: String?, imageUrlList: [String?]) -> Completable + func deleteComment(popUpStoreId: Int64, commentId: Int64) -> Completable + func editComment(popUpStoreId: Int64, commentId: Int64, content: String?, imageUrlList: [String?]?) -> Completable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/FetchCategoryListUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/FetchCategoryListUseCase.swift new file mode 100644 index 00000000..14e35064 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/FetchCategoryListUseCase.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol FetchCategoryListUseCase { + func execute() -> Observable<[CategoryResponse]> +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/HomeAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/HomeAPIUseCase.swift new file mode 100644 index 00000000..30f6d2f0 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/HomeAPIUseCase.swift @@ -0,0 +1,10 @@ +import Foundation + +import RxSwift + +public protocol HomeAPIUseCase { + func fetchHome(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchCustomPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchNewPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func fetchPopularPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/KakaoLoginUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/KakaoLoginUseCase.swift new file mode 100644 index 00000000..22e4e65a --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/KakaoLoginUseCase.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol KakaoLoginUseCase { + func fetchUserCredential() -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/MapUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/MapUseCase.swift new file mode 100644 index 00000000..a56de0c9 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/MapUseCase.swift @@ -0,0 +1,21 @@ +import Foundation + +import RxSwift + +public protocol MapUseCase { + func fetchCategories() -> Observable<[CategoryResponse]> + func fetchStoresInBounds( + northEastLat: Double, + northEastLon: Double, + southWestLat: Double, + southWestLon: Double, + categories: [Int] + ) -> Observable<[MapPopUpStore]> + + func searchStores( + query: String, + categories: [Int] + ) -> Observable<[MapPopUpStore]> + + func filterStoresByLocation(_ stores: [MapPopUpStore], selectedRegions: [String]) -> [MapPopUpStore] +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PopUpAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PopUpAPIUseCase.swift new file mode 100644 index 00000000..5c34b07b --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PopUpAPIUseCase.swift @@ -0,0 +1,10 @@ +import Foundation + +import RxSwift + +public protocol PopUpAPIUseCase { + func getSearchBottomPopUpList(isOpen: Bool, categories: [Int], page: Int32?, size: Int32, sort: String?) -> Observable + func getSearchPopUpList(query: String?) -> Observable + func getPopUpDetail(commentType: String?, popUpStoredId: Int64, isViewCount: Bool?) -> Observable + func getPopUpComment(commentType: String?, page: Int32?, size: Int32?, sort: String?, popUpStoreId: Int64) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PreSignedUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PreSignedUseCase.swift new file mode 100644 index 00000000..13b92620 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/PreSignedUseCase.swift @@ -0,0 +1,9 @@ +import UIKit + +import RxSwift + +public protocol PreSignedUseCase { + func tryUpload(presignedURLRequest: [(filePath: String, image: UIImage)]) -> Single + func tryDelete(objectKeyList: [String]) -> Completable + func fullImageURL(from filePath: String) -> URL? +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/Search/FetchKeywordBasePopupStoreList.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/Search/FetchKeywordBasePopupStoreList.swift new file mode 100644 index 00000000..0ecfceab --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/Search/FetchKeywordBasePopupStoreList.swift @@ -0,0 +1,7 @@ +import Foundation + +import RxSwift + +public protocol FetchKeywordBasePopupListUseCase { + func execute(keyword: String) -> Observable +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/SignUpAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/SignUpAPIUseCase.swift new file mode 100644 index 00000000..89883291 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/SignUpAPIUseCase.swift @@ -0,0 +1,19 @@ +import Foundation + +import RxSwift + +public protocol SignUpAPIUseCase { + func trySignUp( + nickName: String, + gender: String, + age: Int32, + socialEmail: String, + socialType: String, + interests: [Int], + appleAuthorizationCode: String? + ) -> Completable + + func checkNickName(nickName: String) -> Observable + + func fetchCategoryList() -> Observable<[CategoryResponse]> +} diff --git a/Poppool/DomainLayer/Domain/DomainInterface/UseCase/UserAPIUseCase.swift b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/UserAPIUseCase.swift new file mode 100644 index 00000000..b488b1a1 --- /dev/null +++ b/Poppool/DomainLayer/Domain/DomainInterface/UseCase/UserAPIUseCase.swift @@ -0,0 +1,27 @@ +import Foundation + +import RxSwift + +public protocol UserAPIUseCase { + func postBookmarkPopUp(popUpID: Int64) -> Completable + func deleteBookmarkPopUp(popUpID: Int64) -> Completable + func postCommentLike(commentId: Int64) -> Completable + func deleteCommentLike(commentId: Int64) -> Completable + func postUserBlock(blockedUserId: String?) -> Completable + func deleteUserBlock(blockedUserId: String?) -> Completable + func getOtherUserCommentedPopUpList(commenterId: String?, commentType: String?, page: Int32?, size: Int32?, sort: String?) -> Observable + func getMyPage() -> Observable + func postLogout() -> Completable + func getWithdrawlList() -> Observable + func postWithdrawl(surveyList: [GetWithdrawlListDataResponse]) -> Completable + func getMyProfile() -> Observable + func putUserTailoredInfo(gender: String?, age: Int32) -> Completable + func putUserCategory(interestCategoriesToAdd: [Int], interestCategoriesToDelete: [Int], interestCategoriesToKeep: [Int]) -> Completable + func putUserProfile(profileImageUrl: String?, nickname: String?, email: String?, instagramId: String?, intro: String?) -> Completable + func getMyCommentedPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func getBlockUserList(page: Int32?, size: Int32?, sort: String?) -> Observable + func getNoticeList() -> Observable + func getNoticeDetail(noticeID: Int64) -> Observable + func getRecentPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable + func getBookmarkPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable +} diff --git a/Poppool/Poppool.xcodeproj/project.pbxproj b/Poppool/Poppool.xcodeproj/project.pbxproj index a38282f7..c4430902 100644 --- a/Poppool/Poppool.xcodeproj/project.pbxproj +++ b/Poppool/Poppool.xcodeproj/project.pbxproj @@ -3,3015 +3,158 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 76; objects = { /* Begin PBXBuildFile section */ - 0818988E2D295DC30067BF01 /* MyPageLogoutSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818988D2D295DC30067BF01 /* MyPageLogoutSection.swift */; }; - 081898902D295DC80067BF01 /* MyPageLogoutSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818988F2D295DC80067BF01 /* MyPageLogoutSectionCell.swift */; }; - 081898942D2965C20067BF01 /* ProfileEditController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898932D2965C20067BF01 /* ProfileEditController.swift */; }; - 081898962D2965C90067BF01 /* ProfileEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898952D2965C90067BF01 /* ProfileEditView.swift */; }; - 081898982D2965D20067BF01 /* ProfileEditReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898972D2965D20067BF01 /* ProfileEditReactor.swift */; }; - 0818989C2D2BAA570067BF01 /* WithdrawlCheckModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818989B2D2BAA570067BF01 /* WithdrawlCheckModalView.swift */; }; - 0818989E2D2BAA610067BF01 /* WithdrawlCheckModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818989D2D2BAA610067BF01 /* WithdrawlCheckModalController.swift */; }; - 081898A02D2BAA670067BF01 /* WithdrawlCheckModalReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818989F2D2BAA670067BF01 /* WithdrawlCheckModalReactor.swift */; }; - 081898A32D2CC0110067BF01 /* WithdrawlReasonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898A22D2CC0110067BF01 /* WithdrawlReasonView.swift */; }; - 081898A52D2CC0180067BF01 /* WithdrawlReasonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898A42D2CC0180067BF01 /* WithdrawlReasonController.swift */; }; - 081898A72D2CC01D0067BF01 /* WithdrawlReasonReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898A62D2CC01D0067BF01 /* WithdrawlReasonReactor.swift */; }; - 081898AA2D2CEA2F0067BF01 /* WithdrawlCheckSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898A92D2CEA2F0067BF01 /* WithdrawlCheckSectionCell.swift */; }; - 081898AC2D2CEA940067BF01 /* WithdrawlCheckSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898AB2D2CEA940067BF01 /* WithdrawlCheckSection.swift */; }; - 081898AE2D2CFC230067BF01 /* GetWithdrawlListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898AD2D2CFC230067BF01 /* GetWithdrawlListResponseDTO.swift */; }; - 081898B02D2CFCA40067BF01 /* GetWithdrawlListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898AF2D2CFCA40067BF01 /* GetWithdrawlListResponse.swift */; }; - 081898B32D2D20D70067BF01 /* WithdrawlCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898B22D2D20D70067BF01 /* WithdrawlCompleteView.swift */; }; - 081898B52D2D20E30067BF01 /* WithdrawlCompleteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898B42D2D20E30067BF01 /* WithdrawlCompleteController.swift */; }; - 081898B72D2D23A90067BF01 /* UINavigationController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898B62D2D23A90067BF01 /* UINavigationController+.swift */; }; - 081898BA2D2E5F4C0067BF01 /* MyCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898B92D2E5F4C0067BF01 /* MyCommentView.swift */; }; - 081898BC2D2E5F510067BF01 /* MyCommentReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898BB2D2E5F510067BF01 /* MyCommentReactor.swift */; }; - 081898BE2D2E5F590067BF01 /* MyCommentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898BD2D2E5F590067BF01 /* MyCommentController.swift */; }; - 081898C32D30AE2C0067BF01 /* GetMyProfileResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898C22D30AE2C0067BF01 /* GetMyProfileResponseDTO.swift */; }; - 081898C52D30AEF40067BF01 /* GetMyProfileResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898C42D30AEF40067BF01 /* GetMyProfileResponse.swift */; }; - 081898CA2D30D5BA0067BF01 /* InfoEditModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898C92D30D5BA0067BF01 /* InfoEditModalView.swift */; }; - 081898CC2D30D5BF0067BF01 /* InfoEditModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898CB2D30D5BF0067BF01 /* InfoEditModalController.swift */; }; - 081898CE2D30D5C60067BF01 /* InfoEditModalReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898CD2D30D5C60067BF01 /* InfoEditModalReactor.swift */; }; - 081898D02D30EA900067BF01 /* PutUserTailoredInfoRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898CF2D30EA900067BF01 /* PutUserTailoredInfoRequestDTO.swift */; }; - 081898D22D30F57D0067BF01 /* CategoryEditModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898D12D30F57D0067BF01 /* CategoryEditModalView.swift */; }; - 081898D42D30F5840067BF01 /* CategoryEditModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898D32D30F5840067BF01 /* CategoryEditModalController.swift */; }; - 081898D62D30F58A0067BF01 /* CategoryEditModalReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898D52D30F58A0067BF01 /* CategoryEditModalReactor.swift */; }; - 081898D82D310C160067BF01 /* PutUserCategoryRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898D72D310C160067BF01 /* PutUserCategoryRequestDTO.swift */; }; - 081898DA2D32559B0067BF01 /* PutUserProfileRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898D92D32559B0067BF01 /* PutUserProfileRequestDTO.swift */; }; - 081898DC2D326DC10067BF01 /* IntroState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898DB2D326DC10067BF01 /* IntroState.swift */; }; - 081898E02D338F9C0067BF01 /* ListCountButtonSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898DF2D338F9C0067BF01 /* ListCountButtonSection.swift */; }; - 081898E22D338FA40067BF01 /* ListCountButtonSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898E12D338FA40067BF01 /* ListCountButtonSectionCell.swift */; }; - 081898E42D3391550067BF01 /* GetMyCommentedPopUpResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898E32D3391550067BF01 /* GetMyCommentedPopUpResponseDTO.swift */; }; - 081898E62D3391CB0067BF01 /* GetMyCommentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898E52D3391CB0067BF01 /* GetMyCommentResponse.swift */; }; - 081898E82D3392480067BF01 /* GetMyCommentRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898E72D3392480067BF01 /* GetMyCommentRequestDTO.swift */; }; - 081898EC2D33A3960067BF01 /* MyCommentSortedModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898EB2D33A3960067BF01 /* MyCommentSortedModalView.swift */; }; - 081898EE2D33A39D0067BF01 /* MyCommentSortedModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898ED2D33A39D0067BF01 /* MyCommentSortedModalController.swift */; }; - 081898F02D33A3A30067BF01 /* MyCommentSortedModalReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898EF2D33A3A30067BF01 /* MyCommentSortedModalReactor.swift */; }; - 081898F32D33D6AC0067BF01 /* BlockUserManageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898F22D33D6AC0067BF01 /* BlockUserManageView.swift */; }; - 081898F52D33D6B10067BF01 /* BlockUserManageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898F42D33D6B10067BF01 /* BlockUserManageController.swift */; }; - 081898F72D33D6B70067BF01 /* BlockUserManageReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898F62D33D6B70067BF01 /* BlockUserManageReactor.swift */; }; - 081898FB2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898FA2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift */; }; - 081898FD2D33D9ED0067BF01 /* GetBlockUserListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898FC2D33D9ED0067BF01 /* GetBlockUserListResponse.swift */; }; - 081898FF2D33DA440067BF01 /* GetBlockUserListRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898FE2D33DA440067BF01 /* GetBlockUserListRequestDTO.swift */; }; - 081899022D3407F50067BF01 /* BlockUserListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899012D3407F50067BF01 /* BlockUserListSection.swift */; }; - 081899052D34080B0067BF01 /* BlockUserListSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899042D34080B0067BF01 /* BlockUserListSectionCell.swift */; }; - 081899082D34B35A0067BF01 /* MyPageNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899072D34B35A0067BF01 /* MyPageNoticeView.swift */; }; - 0818990A2D34B3620067BF01 /* MyPageNoticeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899092D34B3620067BF01 /* MyPageNoticeController.swift */; }; - 0818990C2D34B3670067BF01 /* MyPageNoticeReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818990B2D34B3670067BF01 /* MyPageNoticeReactor.swift */; }; - 0818990E2D34B68C0067BF01 /* GetNoticeListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818990D2D34B68C0067BF01 /* GetNoticeListResponseDTO.swift */; }; - 081899102D34B7240067BF01 /* GetNoticeListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818990F2D34B7240067BF01 /* GetNoticeListResponse.swift */; }; - 081899122D34CA9E0067BF01 /* GetNoticeDetailResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899112D34CA9E0067BF01 /* GetNoticeDetailResponseDTO.swift */; }; - 081899142D34CAEA0067BF01 /* GetNoticeDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899132D34CAEA0067BF01 /* GetNoticeDetailResponse.swift */; }; - 081899182D34D63E0067BF01 /* NoticeListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899172D34D63E0067BF01 /* NoticeListSection.swift */; }; - 0818991A2D34D6430067BF01 /* NoticeListSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899192D34D6430067BF01 /* NoticeListSectionCell.swift */; }; - 0818991E2D34DF7D0067BF01 /* MyPageNoticeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818991D2D34DF7D0067BF01 /* MyPageNoticeDetailView.swift */; }; - 081899202D34DF880067BF01 /* MyPageNoticeDetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818991F2D34DF880067BF01 /* MyPageNoticeDetailController.swift */; }; - 081899222D34DF8E0067BF01 /* MyPageNoticeDetailReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899212D34DF8E0067BF01 /* MyPageNoticeDetailReactor.swift */; }; - 081899252D3500B80067BF01 /* FAQView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899242D3500B80067BF01 /* FAQView.swift */; }; - 081899272D3500BF0067BF01 /* FAQController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899262D3500BF0067BF01 /* FAQController.swift */; }; - 081899292D3500C50067BF01 /* FAQReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899282D3500C50067BF01 /* FAQReactor.swift */; }; - 0818992D2D3506240067BF01 /* FAQDropdownSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818992C2D3506240067BF01 /* FAQDropdownSectionCell.swift */; }; - 0818992F2D3506290067BF01 /* FAQDropdownSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818992E2D3506290067BF01 /* FAQDropdownSection.swift */; }; - 081899332D35F1090067BF01 /* MyPageBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899322D35F1090067BF01 /* MyPageBookmarkView.swift */; }; - 081899352D35F10F0067BF01 /* MyPageBookmarkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899342D35F10F0067BF01 /* MyPageBookmarkController.swift */; }; - 081899372D35F1140067BF01 /* MyPageBookmarkReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899362D35F1140067BF01 /* MyPageBookmarkReactor.swift */; }; - 081899392D35F11F0067BF01 /* MyPageRecentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899382D35F11F0067BF01 /* MyPageRecentView.swift */; }; - 0818993B2D35F1250067BF01 /* MyPageRecentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818993A2D35F1250067BF01 /* MyPageRecentController.swift */; }; - 0818993D2D35F12A0067BF01 /* MyPageRecentReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818993C2D35F12A0067BF01 /* MyPageRecentReactor.swift */; }; - 0818993F2D35FBE00067BF01 /* GetRecentPopUpResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818993E2D35FBE00067BF01 /* GetRecentPopUpResponseDTO.swift */; }; - 081899412D35FDA10067BF01 /* GetRecentPopUpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899402D35FDA10067BF01 /* GetRecentPopUpResponse.swift */; }; - 081899452D35FEA10067BF01 /* RecentPopUpSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899442D35FEA10067BF01 /* RecentPopUpSection.swift */; }; - 0818994A2D36322B0067BF01 /* PopUpCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899492D36322B0067BF01 /* PopUpCardSection.swift */; }; - 0818994C2D3632320067BF01 /* PopUpCardSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818994B2D3632320067BF01 /* PopUpCardSectionCell.swift */; }; - 081899502D363E5C0067BF01 /* BookMarkPopUpViewTypeModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0818994F2D363E5C0067BF01 /* BookMarkPopUpViewTypeModalView.swift */; }; - 081899522D363E640067BF01 /* BookMarkPopUpViewTypeModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899512D363E640067BF01 /* BookMarkPopUpViewTypeModalController.swift */; }; - 081899542D363E6A0067BF01 /* BookMarkPopUpViewTypeModalReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081899532D363E6A0067BF01 /* BookMarkPopUpViewTypeModalReactor.swift */; }; - 082197A12D426DCB0054094A /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 082197A02D426DCB0054094A /* Then */; }; - 082197A72D4E3EE00054094A /* CommentMyMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197A62D4E3EE00054094A /* CommentMyMenuView.swift */; }; - 082197A92D4E3EE90054094A /* CommentMyMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197A82D4E3EE90054094A /* CommentMyMenuController.swift */; }; - 082197AB2D4E3EEF0054094A /* CommentMyMenuReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197AA2D4E3EEF0054094A /* CommentMyMenuReactor.swift */; }; - 082197AD2D4E49370054094A /* DeleteCommentRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197AC2D4E49370054094A /* DeleteCommentRequestDTO.swift */; }; - 082197B02D4E4E190054094A /* NormalCommentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197AF2D4E4E190054094A /* NormalCommentEditView.swift */; }; - 082197B22D4E4E200054094A /* NormalCommentEditController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197B12D4E4E200054094A /* NormalCommentEditController.swift */; }; - 082197B42D4E4E280054094A /* NormalCommentEditReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082197B32D4E4E280054094A /* NormalCommentEditReactor.swift */; }; - 083A25822CF361EF0099B58E /* BaseTabmanController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A257F2CF361EF0099B58E /* BaseTabmanController.swift */; }; - 083A25832CF361EF0099B58E /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25802CF361EF0099B58E /* BaseViewController.swift */; }; - 083A258C2CF361F90099B58E /* ControllerConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25842CF361F90099B58E /* ControllerConvention.swift */; }; - 083A258D2CF361F90099B58E /* ConventionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25852CF361F90099B58E /* ConventionCollectionViewCell.swift */; }; - 083A258E2CF361F90099B58E /* ConventionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25862CF361F90099B58E /* ConventionTableViewCell.swift */; }; - 083A258F2CF361F90099B58E /* ReactorConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25872CF361F90099B58E /* ReactorConvention.swift */; }; - 083A25902CF361F90099B58E /* TestDynamicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25882CF361F90099B58E /* TestDynamicCell.swift */; }; - 083A25912CF361F90099B58E /* TestDynamicSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25892CF361F90099B58E /* TestDynamicSection.swift */; }; - 083A25922CF361F90099B58E /* ViewConvention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A258A2CF361F90099B58E /* ViewConvention.swift */; }; - 083A25992CF362090099B58E /* Sectionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25932CF362090099B58E /* Sectionable.swift */; }; - 083A259A2CF362090099B58E /* SectionDecorationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25942CF362090099B58E /* SectionDecorationItem.swift */; }; - 083A259B2CF362090099B58E /* SectionSupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25952CF362090099B58E /* SectionSupplementaryItem.swift */; }; - 083A259C2CF362090099B58E /* InOutputable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25972CF362090099B58E /* InOutputable.swift */; }; - 083A25B22CF362670099B58E /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A12CF362670099B58E /* NetworkError.swift */; }; - 083A25B32CF362670099B58E /* Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A22CF362670099B58E /* Requestable.swift */; }; - 083A25B42CF362670099B58E /* Responsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A32CF362670099B58E /* Responsable.swift */; }; - 083A25B52CF362670099B58E /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A52CF362670099B58E /* Endpoint.swift */; }; - 083A25B62CF362670099B58E /* MultipartEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A62CF362670099B58E /* MultipartEndPoint.swift */; }; - 083A25B72CF362670099B58E /* RequestEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A72CF362670099B58E /* RequestEndpoint.swift */; }; - 083A25B82CF362670099B58E /* IndicatorMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25A92CF362670099B58E /* IndicatorMaker.swift */; }; - 083A25B92CF362670099B58E /* FormDataInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25AB2CF362670099B58E /* FormDataInterceptor.swift */; }; - 083A25BA2CF362670099B58E /* TokenInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25AC2CF362670099B58E /* TokenInterceptor.swift */; }; - 083A25BB2CF362670099B58E /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25AE2CF362670099B58E /* Provider.swift */; }; - 083A25BC2CF362670099B58E /* ProviderImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25AF2CF362670099B58E /* ProviderImpl.swift */; }; - 083A25BF2CF362770099B58E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25BD2CF362770099B58E /* Logger.swift */; }; - 083A25C82CF363C00099B58E /* LoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25C72CF363C00099B58E /* LoginController.swift */; }; - 083A25CA2CF363C60099B58E /* LoginReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25C92CF363C60099B58E /* LoginReactor.swift */; }; - 083A25CC2CF363CB0099B58E /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083A25CB2CF363CB0099B58E /* LoginView.swift */; }; - 083A25D02CF364B70099B58E /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 083A25CF2CF364B70099B58E /* Alamofire */; }; - 083C860B2D073A15003F441C /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C860A2D073A15003F441C /* DetailView.swift */; }; - 083C860D2D073A1C003F441C /* DetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C860C2D073A1C003F441C /* DetailController.swift */; }; - 083C860F2D073A23003F441C /* DetailReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C860E2D073A23003F441C /* DetailReactor.swift */; }; - 083C861C2D087337003F441C /* GetPopUpDetailRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C861B2D087337003F441C /* GetPopUpDetailRequestDTO.swift */; }; - 083C861E2D08737F003F441C /* GetPopUpDetailResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C861D2D08737F003F441C /* GetPopUpDetailResponseDTO.swift */; }; - 083C86202D087445003F441C /* GetPopUpDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C861F2D087445003F441C /* GetPopUpDetailResponse.swift */; }; - 083C86242D087A44003F441C /* DetailTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86232D087A44003F441C /* DetailTitleSection.swift */; }; - 083C86262D087A4E003F441C /* DetailTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86252D087A4E003F441C /* DetailTitleSectionCell.swift */; }; - 083C86292D088080003F441C /* DetailContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86282D088080003F441C /* DetailContentSection.swift */; }; - 083C862B2D08808C003F441C /* DetailContentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C862A2D08808C003F441C /* DetailContentSectionCell.swift */; }; - 083C86362D0C7EF4003F441C /* CommentSelectedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86352D0C7EF4003F441C /* CommentSelectedController.swift */; }; - 083C86382D0C7EFC003F441C /* CommentSelectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86372D0C7EFC003F441C /* CommentSelectedView.swift */; }; - 083C863A2D0C7F0A003F441C /* CommentSelectedReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86392D0C7F0A003F441C /* CommentSelectedReactor.swift */; }; - 083C863D2D0C8BC4003F441C /* NormalCommentAddController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C863C2D0C8BC4003F441C /* NormalCommentAddController.swift */; }; - 083C863F2D0C8BCE003F441C /* NormalCommentAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C863E2D0C8BCE003F441C /* NormalCommentAddView.swift */; }; - 083C86412D0C8BD8003F441C /* NormalCommentAddReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86402D0C8BD8003F441C /* NormalCommentAddReactor.swift */; }; - 083C86452D0DCDE9003F441C /* AddCommentTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86442D0DCDE8003F441C /* AddCommentTitleSectionCell.swift */; }; - 083C86472D0DCDFB003F441C /* AddCommentTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86462D0DCDFB003F441C /* AddCommentTitleSection.swift */; }; - 083C864A2D0DCF96003F441C /* AddCommentDescriptionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86492D0DCF96003F441C /* AddCommentDescriptionSection.swift */; }; - 083C864C2D0DCF9B003F441C /* AddCommentDescriptionSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C864B2D0DCF9B003F441C /* AddCommentDescriptionSectionCell.swift */; }; - 083C864F2D0DD3A6003F441C /* AddCommentImageSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C864E2D0DD3A6003F441C /* AddCommentImageSection.swift */; }; - 083C86512D0DD3AB003F441C /* AddCommentImageSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86502D0DD3AB003F441C /* AddCommentImageSectionCell.swift */; }; - 083C86542D0DD7E9003F441C /* AddCommentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86532D0DD7E9003F441C /* AddCommentSection.swift */; }; - 083C86562D0DD7EE003F441C /* AddCommentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86552D0DD7EE003F441C /* AddCommentSectionCell.swift */; }; - 083C86592D0DEFC3003F441C /* CommentCheckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86582D0DEFC3003F441C /* CommentCheckView.swift */; }; - 083C865B2D0DEFCF003F441C /* CommentCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C865A2D0DEFCF003F441C /* CommentCheckController.swift */; }; - 083C865D2D0DEFD5003F441C /* CommentCheckReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C865C2D0DEFD5003F441C /* CommentCheckReactor.swift */; }; - 083C86602D0EC496003F441C /* InstaCommentAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C865F2D0EC496003F441C /* InstaCommentAddView.swift */; }; - 083C86622D0EC49E003F441C /* InstaCommentAddController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86612D0EC49E003F441C /* InstaCommentAddController.swift */; }; - 083C86642D0EC4A5003F441C /* InstaCommentAddReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86632D0EC4A5003F441C /* InstaCommentAddReactor.swift */; }; - 083C86692D0ECB47003F441C /* InstaGuideSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86682D0ECB47003F441C /* InstaGuideSection.swift */; }; - 083C866B2D0ECB4F003F441C /* InstaGuideSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C866A2D0ECB4F003F441C /* InstaGuideSectionCell.swift */; }; - 083C866E2D0ECB87003F441C /* InstaGuideChildSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C866D2D0ECB87003F441C /* InstaGuideChildSection.swift */; }; - 083C86702D0ECB8E003F441C /* InstaGuideChildSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C866F2D0ECB8E003F441C /* InstaGuideChildSectionCell.swift */; }; - 083C86732D0EE2B1003F441C /* CommentAPIEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86722D0EE2B1003F441C /* CommentAPIEndPoint.swift */; }; - 083C86762D0EE2CF003F441C /* PostCommentRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86752D0EE2CF003F441C /* PostCommentRequestDTO.swift */; }; - 083C86782D0EE382003F441C /* CommentAPIRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86772D0EE382003F441C /* CommentAPIRepository.swift */; }; - 083C867A2D0EE3BB003F441C /* CommentAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C86792D0EE3BB003F441C /* CommentAPIUseCaseImpl.swift */; }; - 0841BA802CF9F34100049E31 /* ImageBannerChildSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA7F2CF9F34100049E31 /* ImageBannerChildSectionCell.swift */; }; - 0841BA822CF9F5DF00049E31 /* PreSignedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA812CF9F5DF00049E31 /* PreSignedService.swift */; }; - 0841BA872CF9F62400049E31 /* PreSignedURLDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA842CF9F62300049E31 /* PreSignedURLDTO.swift */; }; - 0841BA882CF9F62400049E31 /* PresignedURLRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA852CF9F62300049E31 /* PresignedURLRequestDTO.swift */; }; - 0841BA892CF9F62400049E31 /* PreSignedURLResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA862CF9F62300049E31 /* PreSignedURLResponseDTO.swift */; }; - 0841BA8C2CF9F67100049E31 /* PreSignedAPIEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA8B2CF9F67100049E31 /* PreSignedAPIEndPoint.swift */; }; - 0841BA8E2CF9F8A100049E31 /* ImageBannerChildSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BA8D2CF9F8A100049E31 /* ImageBannerChildSection.swift */; }; - 0841BAA32CFA31A300049E31 /* SpacingSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAA22CFA31A300049E31 /* SpacingSection.swift */; }; - 0841BAA52CFA31A900049E31 /* SpacingSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAA42CFA31A900049E31 /* SpacingSectionCell.swift */; }; - 0841BAA82CFA354500049E31 /* HomeTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAA72CFA354500049E31 /* HomeTitleSection.swift */; }; - 0841BAAA2CFA354C00049E31 /* HomeTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAA92CFA354C00049E31 /* HomeTitleSectionCell.swift */; }; - 0841BAAC2CFA35F300049E31 /* UILabel+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAAB2CFA35F300049E31 /* UILabel+.swift */; }; - 0841BAAF2CFA38EA00049E31 /* HomeCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAAE2CFA38EA00049E31 /* HomeCardSection.swift */; }; - 0841BAB12CFA38F500049E31 /* HomeCardSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAB02CFA38F500049E31 /* HomeCardSectionCell.swift */; }; - 0841BAB42CFABED700049E31 /* HomePopularCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAB32CFABED700049E31 /* HomePopularCardSection.swift */; }; - 0841BAB62CFABEDC00049E31 /* HomePopularCardSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAB52CFABEDC00049E31 /* HomePopularCardSectionCell.swift */; }; - 0841BAB82CFAC41300049E31 /* SectionBackGroundDecorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAB72CFAC41300049E31 /* SectionBackGroundDecorationView.swift */; }; - 0841BABA2CFAE5BE00049E31 /* HomeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAB92CFAE5BE00049E31 /* HomeHeaderView.swift */; }; - 0841BABC2CFB59E200049E31 /* String?+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BABB2CFB59E200049E31 /* String?+.swift */; }; - 0841BABE2CFB5AA600049E31 /* Date?+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BABD2CFB5AA600049E31 /* Date?+.swift */; }; - 0841BAC32CFB600800049E31 /* TabbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0841BAC22CFB600800049E31 /* TabbarController.swift */; }; - 086DD8C82CFDEA9200B97D3B /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8C72CFDEA9200B97D3B /* UIView+.swift */; }; - 086DD8CC2CFDFEA800B97D3B /* HomeListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8CB2CFDFEA800B97D3B /* HomeListController.swift */; }; - 086DD8CE2CFDFEB000B97D3B /* HomeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8CD2CFDFEB000B97D3B /* HomeListView.swift */; }; - 086DD8D02CFDFEB900B97D3B /* HomeListReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8CF2CFDFEB900B97D3B /* HomeListReactor.swift */; }; - 086DD8D32CFDFF1500B97D3B /* HomePopUpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8D22CFDFF1500B97D3B /* HomePopUpType.swift */; }; - 086DD8D62CFF182100B97D3B /* UserAPIEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8D52CFF182100B97D3B /* UserAPIEndPoint.swift */; }; - 086DD8D82CFF185200B97D3B /* PostBookmarkPopUpRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8D72CFF185200B97D3B /* PostBookmarkPopUpRequestDTO.swift */; }; - 086DD8DA2CFF194700B97D3B /* UserAPIRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8D92CFF194700B97D3B /* UserAPIRepositoryImpl.swift */; }; - 086DD8DE2CFF19C400B97D3B /* UserAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8DD2CFF19C400B97D3B /* UserAPIUseCaseImpl.swift */; }; - 086DD8E02CFF2C3700B97D3B /* UIImageView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8DF2CFF2C3700B97D3B /* UIImageView+.swift */; }; - 086DD8E32CFF356300B97D3B /* HomeCardGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD8E22CFF356300B97D3B /* HomeCardGridSection.swift */; }; - 086DD92A2D0086AA00B97D3B /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD9292D0086AA00B97D3B /* SearchController.swift */; }; - 086DD92C2D0086B100B97D3B /* SearchReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD92B2D0086B100B97D3B /* SearchReactor.swift */; }; - 086DD92E2D0086B900B97D3B /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD92D2D0086B900B97D3B /* SearchView.swift */; }; - 086DD9302D0090E900B97D3B /* UITextField+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD92F2D0090E900B97D3B /* UITextField+.swift */; }; - 086DD9342D00962500B97D3B /* SearchTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD9332D00962500B97D3B /* SearchTitleSectionCell.swift */; }; - 086DD9362D00963900B97D3B /* SearchTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD9352D00963900B97D3B /* SearchTitleSection.swift */; }; - 086DD93B2D009A1C00B97D3B /* CancelableTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD93A2D009A1C00B97D3B /* CancelableTagSection.swift */; }; - 086DD93D2D009A2600B97D3B /* CancelableTagSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD93C2D009A2600B97D3B /* CancelableTagSectionCell.swift */; }; - 086DD9402D01EEEB00B97D3B /* SearchCountTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD93F2D01EEEB00B97D3B /* SearchCountTitleSection.swift */; }; - 086DD9422D01EEF700B97D3B /* SearchCountTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086DD9412D01EEF700B97D3B /* SearchCountTitleSectionCell.swift */; }; - 086F89C32D1E347700CA4FC9 /* CommentUserInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89C22D1E347700CA4FC9 /* CommentUserInfoView.swift */; }; - 086F89C52D1E347E00CA4FC9 /* CommentUserInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89C42D1E347E00CA4FC9 /* CommentUserInfoController.swift */; }; - 086F89C72D1E348400CA4FC9 /* CommentUserInfoReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89C62D1E348400CA4FC9 /* CommentUserInfoReactor.swift */; }; - 086F89CA2D1E42A700CA4FC9 /* CommentUserBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89C92D1E42A700CA4FC9 /* CommentUserBlockView.swift */; }; - 086F89CC2D1E42B000CA4FC9 /* CommentUserBlockController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89CB2D1E42B000CA4FC9 /* CommentUserBlockController.swift */; }; - 086F89CE2D1E42B500CA4FC9 /* CommentUserBlockReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89CD2D1E42B500CA4FC9 /* CommentUserBlockReactor.swift */; }; - 086F89D02D1E60A100CA4FC9 /* PostUserBlockRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89CF2D1E60A100CA4FC9 /* PostUserBlockRequestDTO.swift */; }; - 086F89D32D1E6DA600CA4FC9 /* OtherUserCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89D22D1E6DA600CA4FC9 /* OtherUserCommentView.swift */; }; - 086F89D52D1E6DB100CA4FC9 /* OtherUserCommentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89D42D1E6DB100CA4FC9 /* OtherUserCommentController.swift */; }; - 086F89D72D1E6DB700CA4FC9 /* OtherUserCommentReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89D62D1E6DB700CA4FC9 /* OtherUserCommentReactor.swift */; }; - 086F89D92D1E79E200CA4FC9 /* GetOtherUserCommentListRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89D82D1E79E200CA4FC9 /* GetOtherUserCommentListRequestDTO.swift */; }; - 086F89DB2D1E7A6C00CA4FC9 /* GetOtherUserCommentedPopUpListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89DA2D1E7A6C00CA4FC9 /* GetOtherUserCommentedPopUpListResponseDTO.swift */; }; - 086F89E02D1E7CC700CA4FC9 /* GetOtherUserCommentedPopUpListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89DF2D1E7CC700CA4FC9 /* GetOtherUserCommentedPopUpListResponse.swift */; }; - 086F89E42D1FE91300CA4FC9 /* OtherUserCommentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89E32D1FE91300CA4FC9 /* OtherUserCommentSection.swift */; }; - 086F89E62D1FE91800CA4FC9 /* OtherUserCommentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89E52D1FE91800CA4FC9 /* OtherUserCommentSectionCell.swift */; }; - 086F89EA2D2009E300CA4FC9 /* SubLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89E92D2009E300CA4FC9 /* SubLoginView.swift */; }; - 086F89EC2D2009EB00CA4FC9 /* SubLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89EB2D2009EB00CA4FC9 /* SubLoginController.swift */; }; - 086F89EE2D2009F100CA4FC9 /* SubLoginReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89ED2D2009F100CA4FC9 /* SubLoginReactor.swift */; }; - 086F89F12D2269D800CA4FC9 /* MyPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89F02D2269D800CA4FC9 /* MyPageView.swift */; }; - 086F89F32D2269DE00CA4FC9 /* MyPageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89F22D2269DE00CA4FC9 /* MyPageController.swift */; }; - 086F89F52D2269E300CA4FC9 /* MyPageReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89F42D2269E300CA4FC9 /* MyPageReactor.swift */; }; - 086F89F72D226DF600CA4FC9 /* GetMyPageResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89F62D226DF600CA4FC9 /* GetMyPageResponseDTO.swift */; }; - 086F89F92D226EEB00CA4FC9 /* GetMyPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F89F82D226EEB00CA4FC9 /* GetMyPageResponse.swift */; }; - 086F8A052D23CB3300CA4FC9 /* MyPageProfileSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A042D23CB3300CA4FC9 /* MyPageProfileSection.swift */; }; - 086F8A072D23CB3800CA4FC9 /* MyPageProfileSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A062D23CB3800CA4FC9 /* MyPageProfileSectionCell.swift */; }; - 086F8A0A2D2621EE00CA4FC9 /* MyPageMyCommentTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A092D2621EE00CA4FC9 /* MyPageMyCommentTitleSection.swift */; }; - 086F8A0C2D2621F400CA4FC9 /* MyPageMyCommentTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A0B2D2621F400CA4FC9 /* MyPageMyCommentTitleSectionCell.swift */; }; - 086F8A0F2D26297900CA4FC9 /* MyPageCommentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A0E2D26297900CA4FC9 /* MyPageCommentSection.swift */; }; - 086F8A112D26297D00CA4FC9 /* MyPageCommentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A102D26297D00CA4FC9 /* MyPageCommentSectionCell.swift */; }; - 086F8A182D265C5F00CA4FC9 /* MyPageListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A172D265C5F00CA4FC9 /* MyPageListSection.swift */; }; - 086F8A1A2D265C6300CA4FC9 /* MyPageListSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086F8A192D265C6300CA4FC9 /* MyPageListSectionCell.swift */; }; - 088DE2442D104EE70030FA9E /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 088DE2432D104EE70030FA9E /* SwiftSoup */; }; - 088DE2472D12DB5C0030FA9E /* GoogleMaps in Frameworks */ = {isa = PBXBuildFile; productRef = 088DE2462D12DB5C0030FA9E /* GoogleMaps */; }; - 088DE24A2D12F3360030FA9E /* DetailInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2492D12F3360030FA9E /* DetailInfoSection.swift */; }; - 088DE24C2D12F33B0030FA9E /* DetailInfoSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE24B2D12F33B0030FA9E /* DetailInfoSectionCell.swift */; }; - 088DE24F2D13019A0030FA9E /* DetailCommentTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE24E2D13019A0030FA9E /* DetailCommentTitleSection.swift */; }; - 088DE2512D13019E0030FA9E /* DetailCommentTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2502D13019E0030FA9E /* DetailCommentTitleSectionCell.swift */; }; - 088DE2542D144A7E0030FA9E /* DetailCommentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2532D144A7E0030FA9E /* DetailCommentSection.swift */; }; - 088DE2562D144A830030FA9E /* DetailCommentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2552D144A830030FA9E /* DetailCommentSectionCell.swift */; }; - 088DE2582D144B0F0030FA9E /* DetailCommentProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2572D144B0F0030FA9E /* DetailCommentProfileView.swift */; }; - 088DE25A2D1458620030FA9E /* DetailCommentImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE2592D1458620030FA9E /* DetailCommentImageCell.swift */; }; - 088DE25D2D145E3A0030FA9E /* DetailSimilarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE25C2D145E3A0030FA9E /* DetailSimilarSection.swift */; }; - 088DE25F2D145E3F0030FA9E /* DetailSimilarSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088DE25E2D145E3F0030FA9E /* DetailSimilarSectionCell.swift */; }; - 089952422D031E650022AEF9 /* SearchSortedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952412D031E650022AEF9 /* SearchSortedController.swift */; }; - 089952442D031E6D0022AEF9 /* SearchSortedReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952432D031E6D0022AEF9 /* SearchSortedReactor.swift */; }; - 089952462D031E740022AEF9 /* SearchSortedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952452D031E740022AEF9 /* SearchSortedView.swift */; }; - 089952492D033A1C0022AEF9 /* PopUpAPIEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952482D033A1C0022AEF9 /* PopUpAPIEndPoint.swift */; }; - 0899524B2D033A9C0022AEF9 /* GetClosePopUpListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899524A2D033A9C0022AEF9 /* GetClosePopUpListResponseDTO.swift */; }; - 0899524D2D033AA70022AEF9 /* GetOpenPopUpListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899524C2D033AA70022AEF9 /* GetOpenPopUpListResponseDTO.swift */; }; - 0899524F2D033B5A0022AEF9 /* GetSearchPopUpListRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899524E2D033B5A0022AEF9 /* GetSearchPopUpListRequestDTO.swift */; }; - 089952512D033C410022AEF9 /* GetSearchBottomPopUpListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952502D033C410022AEF9 /* GetSearchBottomPopUpListResponse.swift */; }; - 089952532D033C940022AEF9 /* PopUpAPIRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952522D033C940022AEF9 /* PopUpAPIRepositoryImpl.swift */; }; - 089952552D033D480022AEF9 /* PopUpAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952542D033D480022AEF9 /* PopUpAPIUseCaseImpl.swift */; }; - 089952582D0347AC0022AEF9 /* SearchCategoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952572D0347AC0022AEF9 /* SearchCategoryController.swift */; }; - 0899525A2D0347B40022AEF9 /* SearchCategoryReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952592D0347B40022AEF9 /* SearchCategoryReactor.swift */; }; - 0899525C2D0347BD0022AEF9 /* SearchCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899525B2D0347BD0022AEF9 /* SearchCategoryView.swift */; }; - 089952602D0366C40022AEF9 /* SearchMainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899525F2D0366C40022AEF9 /* SearchMainController.swift */; }; - 089952622D0366D30022AEF9 /* SearchMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952612D0366D30022AEF9 /* SearchMainView.swift */; }; - 089952642D0366DA0022AEF9 /* SearchMainReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952632D0366DA0022AEF9 /* SearchMainReactor.swift */; }; - 089952662D046CCD0022AEF9 /* SearchResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952652D046CCD0022AEF9 /* SearchResultController.swift */; }; - 089952682D046CD80022AEF9 /* SearchResultReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952672D046CD80022AEF9 /* SearchResultReactor.swift */; }; - 0899526A2D046CDE0022AEF9 /* SearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952692D046CDE0022AEF9 /* SearchResultView.swift */; }; - 0899526C2D0473EC0022AEF9 /* GetSearchPopUpListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899526B2D0473EC0022AEF9 /* GetSearchPopUpListResponseDTO.swift */; }; - 0899526E2D0474340022AEF9 /* GetSearchPopUpListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899526D2D0474340022AEF9 /* GetSearchPopUpListResponse.swift */; }; - 089952732D0475E90022AEF9 /* SearchResultCountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952722D0475E90022AEF9 /* SearchResultCountSection.swift */; }; - 089952752D0475F20022AEF9 /* SearchResultCountSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089952742D0475F20022AEF9 /* SearchResultCountSectionCell.swift */; }; - 08A2E46C2D15BC5000102313 /* CommentLikeRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E46B2D15BC5000102313 /* CommentLikeRequestDTO.swift */; }; - 08A2E4792D1B06A300102313 /* ImageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4782D1B06A300102313 /* ImageDetailView.swift */; }; - 08A2E47B2D1B06AA00102313 /* ImageDetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E47A2D1B06AA00102313 /* ImageDetailController.swift */; }; - 08A2E47D2D1B06B000102313 /* ImageDetailReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E47C2D1B06B000102313 /* ImageDetailReactor.swift */; }; - 08A2E4802D1BCDE300102313 /* CommentDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E47F2D1BCDE300102313 /* CommentDetailView.swift */; }; - 08A2E4822D1BCDEA00102313 /* CommentDetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4812D1BCDEA00102313 /* CommentDetailController.swift */; }; - 08A2E4842D1BCDEF00102313 /* CommentDetailReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4832D1BCDEF00102313 /* CommentDetailReactor.swift */; }; - 08A2E4862D1BD85C00102313 /* CommentDetailImageSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4852D1BD85C00102313 /* CommentDetailImageSection.swift */; }; - 08A2E48A2D1BDA8400102313 /* CommentDetailContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4892D1BDA8400102313 /* CommentDetailContentSection.swift */; }; - 08A2E48C2D1BDA8A00102313 /* CommentDetailContentSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E48B2D1BDA8A00102313 /* CommentDetailContentSectionCell.swift */; }; - 08A2E48F2D1BF6E500102313 /* CommentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E48E2D1BF6E500102313 /* CommentListView.swift */; }; - 08A2E4912D1BF6EA00102313 /* CommentListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4902D1BF6EA00102313 /* CommentListController.swift */; }; - 08A2E4932D1BF6EF00102313 /* CommentListReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4922D1BF6EF00102313 /* CommentListReactor.swift */; }; - 08A2E4952D1C078300102313 /* GetPopUpCommentRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4942D1C078300102313 /* GetPopUpCommentRequestDTO.swift */; }; - 08A2E4972D1C07F500102313 /* GetPopUpCommentResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4962D1C07F500102313 /* GetPopUpCommentResponseDTO.swift */; }; - 08A2E4992D1C08D600102313 /* GetPopUpCommentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E4982D1C08D600102313 /* GetPopUpCommentResponse.swift */; }; - 08A2E49D2D1C416800102313 /* CommentListTitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E49C2D1C416800102313 /* CommentListTitleSection.swift */; }; - 08A2E49F2D1C417000102313 /* CommentListTitleSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2E49E2D1C417000102313 /* CommentListTitleSectionCell.swift */; }; - 08B191372CF366680057BC04 /* UICollectionViewCell+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191342CF366670057BC04 /* UICollectionViewCell+.swift */; }; - 08B191382CF366680057BC04 /* UICollectionReusableView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191352CF366670057BC04 /* UICollectionReusableView+.swift */; }; - 08B191392CF366680057BC04 /* UITableViewCell+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191362CF366670057BC04 /* UITableViewCell+.swift */; }; - 08B1913B2CF366A00057BC04 /* UIApplication+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1913A2CF366A00057BC04 /* UIApplication+.swift */; }; - 08B1913F2CF367FA0057BC04 /* UIColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1913E2CF367FA0057BC04 /* UIColor+.swift */; }; - 08B191412CF367FF0057BC04 /* UIFont+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191402CF367FF0057BC04 /* UIFont+.swift */; }; - 08B1914B2CF41D690057BC04 /* GothicA1-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191422CF41D680057BC04 /* GothicA1-Bold.ttf */; }; - 08B1914C2CF41D690057BC04 /* GothicA1-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191432CF41D680057BC04 /* GothicA1-Light.ttf */; }; - 08B1914D2CF41D690057BC04 /* GothicA1-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191442CF41D680057BC04 /* GothicA1-Medium.ttf */; }; - 08B1914E2CF41D690057BC04 /* GothicA1-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191452CF41D680057BC04 /* GothicA1-Regular.ttf */; }; - 08B1914F2CF41D690057BC04 /* Poppins-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191462CF41D680057BC04 /* Poppins-Bold.ttf */; }; - 08B191502CF41D690057BC04 /* Poppins-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191472CF41D680057BC04 /* Poppins-Light.ttf */; }; - 08B191512CF41D690057BC04 /* Poppins-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191482CF41D680057BC04 /* Poppins-Medium.ttf */; }; - 08B191522CF41D690057BC04 /* Poppins-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 08B191492CF41D680057BC04 /* Poppins-Regular.ttf */; }; - 08B191552CF41D6F0057BC04 /* PP_loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 08B191532CF41D6F0057BC04 /* PP_loading.json */; }; - 08B191562CF41D6F0057BC04 /* PP_splash.json in Resources */ = {isa = PBXBuildFile; fileRef = 08B191542CF41D6F0057BC04 /* PP_splash.json */; }; - 08B191592CF41E610057BC04 /* SignUpMainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191582CF41E610057BC04 /* SignUpMainController.swift */; }; - 08B1915B2CF41E690057BC04 /* SignUpMainReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1915A2CF41E690057BC04 /* SignUpMainReactor.swift */; }; - 08B1915D2CF41E6F0057BC04 /* SignUpMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1915C2CF41E6F0057BC04 /* SignUpMainView.swift */; }; - 08B191612CF430E70057BC04 /* PPProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191602CF430E70057BC04 /* PPProgressView.swift */; }; - 08B191632CF430F30057BC04 /* PPProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191622CF430F30057BC04 /* PPProgressIndicator.swift */; }; - 08B191672CF432220057BC04 /* PPCancelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191662CF432220057BC04 /* PPCancelHeaderView.swift */; }; - 08B1916A2CF434B80057BC04 /* SignUpStep1Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191692CF434B80057BC04 /* SignUpStep1Controller.swift */; }; - 08B1916C2CF434C30057BC04 /* SignUpStep1View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1916B2CF434C30057BC04 /* SignUpStep1View.swift */; }; - 08B1916E2CF434CF0057BC04 /* SignUpStep1Reactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1916D2CF434CF0057BC04 /* SignUpStep1Reactor.swift */; }; - 08B191712CF4398D0057BC04 /* PPLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191702CF4398D0057BC04 /* PPLabel.swift */; }; - 08B191742CF43DF40057BC04 /* SignUpCheckBoxButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191732CF43DF40057BC04 /* SignUpCheckBoxButton.swift */; }; - 08B191762CF440C40057BC04 /* PPButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191752CF440C40057BC04 /* PPButton.swift */; }; - 08B191782CF442230057BC04 /* UIImage+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191772CF442230057BC04 /* UIImage+.swift */; }; - 08B1917A2CF452B30057BC04 /* SignUpTermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191792CF452B30057BC04 /* SignUpTermsView.swift */; }; - 08B1917D2CF46DE30057BC04 /* TermsDetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1917C2CF46DE30057BC04 /* TermsDetailController.swift */; }; - 08B1917F2CF46DF20057BC04 /* TermsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1917E2CF46DF20057BC04 /* TermsDetailView.swift */; }; - 08B191822CF48A7B0057BC04 /* SignUpStep2Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191812CF48A7B0057BC04 /* SignUpStep2Controller.swift */; }; - 08B191842CF48A820057BC04 /* SignUpStep2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191832CF48A820057BC04 /* SignUpStep2View.swift */; }; - 08B191862CF48A8B0057BC04 /* SignUpStep2Reactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191852CF48A8B0057BC04 /* SignUpStep2Reactor.swift */; }; - 08B191882CF48FAE0057BC04 /* NickNameState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191872CF48FAE0057BC04 /* NickNameState.swift */; }; - 08B1918D2CF49FF70057BC04 /* SignUpStep3View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1918C2CF49FF70057BC04 /* SignUpStep3View.swift */; }; - 08B1918F2CF4A0020057BC04 /* SignUpStep3Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1918E2CF4A0020057BC04 /* SignUpStep3Controller.swift */; }; - 08B191912CF4A00E0057BC04 /* SignUpStep3Reactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191902CF4A00E0057BC04 /* SignUpStep3Reactor.swift */; }; - 08B191942CF4A0F00057BC04 /* SignUpStep4View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191932CF4A0F00057BC04 /* SignUpStep4View.swift */; }; - 08B191962CF4A0FA0057BC04 /* SignUpStep4Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191952CF4A0FA0057BC04 /* SignUpStep4Controller.swift */; }; - 08B191982CF4A1010057BC04 /* SignUpStep4Reactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191972CF4A1010057BC04 /* SignUpStep4Reactor.swift */; }; - 08B1919C2CF4A77C0057BC04 /* TagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1919B2CF4A77C0057BC04 /* TagSection.swift */; }; - 08B1919E2CF4A7830057BC04 /* TagSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1919D2CF4A7830057BC04 /* TagSectionCell.swift */; }; - 08B191A02CF4AA0E0057BC04 /* Reactive+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B1919F2CF4AA0E0057BC04 /* Reactive+.swift */; }; - 08B191A22CF4AE890057BC04 /* ToastMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191A12CF4AE890057BC04 /* ToastMaker.swift */; }; - 08B191A42CF5A7030057BC04 /* PPSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191A32CF5A7030057BC04 /* PPSegmentedControl.swift */; }; - 08B191A72CF5A9430057BC04 /* AgeSelectedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191A62CF5A9430057BC04 /* AgeSelectedButton.swift */; }; - 08B191AC2CF5BF9D0057BC04 /* AgeSelectedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191AB2CF5BF9D0057BC04 /* AgeSelectedController.swift */; }; - 08B191AE2CF5BFA60057BC04 /* AgeSelectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191AD2CF5BFA60057BC04 /* AgeSelectedView.swift */; }; - 08B191B02CF5BFAE0057BC04 /* AgeSelectedReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191AF2CF5BFAE0057BC04 /* AgeSelectedReactor.swift */; }; - 08B191B22CF5C0A60057BC04 /* PPPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191B12CF5C0A60057BC04 /* PPPicker.swift */; }; - 08B191B42CF609260057BC04 /* KakaoLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191B32CF609260057BC04 /* KakaoLoginService.swift */; }; - 08B191B62CF6092B0057BC04 /* AppleLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191B52CF6092B0057BC04 /* AppleLoginService.swift */; }; - 08B191B82CF6092F0057BC04 /* AuthServiceable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191B72CF6092F0057BC04 /* AuthServiceable.swift */; }; - 08B191BA2CF609AE0057BC04 /* RxKakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 08B191B92CF609AE0057BC04 /* RxKakaoSDK */; }; - 08B191BC2CF609AE0057BC04 /* RxKakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 08B191BB2CF609AE0057BC04 /* RxKakaoSDKAuth */; }; - 08B191BE2CF609AE0057BC04 /* RxKakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 08B191BD2CF609AE0057BC04 /* RxKakaoSDKUser */; }; - 08B191C22CF615CA0057BC04 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191C12CF615CA0057BC04 /* Secrets.swift */; }; - 08CBEA032D38989E00248007 /* PostTokenReissueResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA022D38989E00248007 /* PostTokenReissueResponseDTO.swift */; }; - 08CBEA062D38991600248007 /* PostTokenReissueResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA052D38991600248007 /* PostTokenReissueResponse.swift */; }; - 08CBEA0B2D38DBD600248007 /* LastLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA0A2D38DBD600248007 /* LastLoginView.swift */; }; - 08CBEA0D2D38ED0D00248007 /* CountButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA0C2D38ED0D00248007 /* CountButtonView.swift */; }; - 08CBEA3A2D3FABE100248007 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA392D3FABE100248007 /* ToastView.swift */; }; - 08CBEA3C2D3FABED00248007 /* BookMarkToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA3B2D3FABED00248007 /* BookMarkToastView.swift */; }; - 08CBEA3E2D3FF6A100248007 /* PopUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBEA3D2D3FF6A100248007 /* PopUpCardView.swift */; }; - 08DC61F32CF75037002A2F44 /* KeyChainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC61F22CF75037002A2F44 /* KeyChainService.swift */; }; - 08DC61F52CF765B5002A2F44 /* UserDefaultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC61F42CF765B5002A2F44 /* UserDefaultService.swift */; }; - 08DC61F82CF76843002A2F44 /* SignUpCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC61F72CF76843002A2F44 /* SignUpCompleteView.swift */; }; - 08DC61FA2CF7684F002A2F44 /* SignUpCompleteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC61F92CF7684F002A2F44 /* SignUpCompleteController.swift */; }; - 08DC61FC2CF76862002A2F44 /* SignUpCompleteReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC61FB2CF76862002A2F44 /* SignUpCompleteReactor.swift */; }; - 08DC62032CF8AC06002A2F44 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC62022CF8AC06002A2F44 /* HomeView.swift */; }; - 08DC62052CF8AC0E002A2F44 /* HomeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC62042CF8AC0E002A2F44 /* HomeController.swift */; }; - 08DC62072CF8AC14002A2F44 /* HomeReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC62062CF8AC14002A2F44 /* HomeReactor.swift */; }; - 08DC620B2CF8AE0F002A2F44 /* ImageBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC620A2CF8AE0F002A2F44 /* ImageBannerSection.swift */; }; - 08DC620D2CF8AE16002A2F44 /* ImageBannerSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC620C2CF8AE16002A2F44 /* ImageBannerSectionCell.swift */; }; - 08DC62112CF8B446002A2F44 /* SortedRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC62102CF8B446002A2F44 /* SortedRequestDTO.swift */; }; - 08DC62132CF8B833002A2F44 /* Optional+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DC62122CF8B833002A2F44 /* Optional+.swift */; }; - 08DE8A0D2D5236840049BCAC /* PutCommentRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A0C2D5236840049BCAC /* PutCommentRequestDTO.swift */; }; - 08DE8A102D5255110049BCAC /* DetailEmptyCommetSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A0F2D5255110049BCAC /* DetailEmptyCommetSection.swift */; }; - 08DE8A122D5255180049BCAC /* DetailEmptyCommetSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A112D5255180049BCAC /* DetailEmptyCommetSectionCell.swift */; }; - 08DE8A182D525BA20049BCAC /* Terms.plist in Resources */ = {isa = PBXBuildFile; fileRef = 08DE8A172D525BA20049BCAC /* Terms.plist */; }; - 08DE8A1B2D5261DE0049BCAC /* MyPageTermsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A1A2D5261DE0049BCAC /* MyPageTermsController.swift */; }; - 08DE8A1D2D5261E70049BCAC /* MyPageTermsReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A1C2D5261E70049BCAC /* MyPageTermsReactor.swift */; }; - 08DE8A3F2D54DCC40049BCAC /* MyCommentedPopUpGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A3E2D54DCC40049BCAC /* MyCommentedPopUpGridSection.swift */; }; - 08DE8A412D54DCCA0049BCAC /* MyCommentedPopUpGridSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE8A402D54DCCA0049BCAC /* MyCommentedPopUpGridSectionCell.swift */; }; - 4E5825672D1951DF00EE83EF /* FloatingPanel in Frameworks */ = {isa = PBXBuildFile; productRef = 4E5825662D1951DF00EE83EF /* FloatingPanel */; }; - 4E643FC12D738D7F0046AF29 /* PopUpStoreRegisterReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E643FC02D738D7F0046AF29 /* PopUpStoreRegisterReactor.swift */; }; - 4E643FC32D738D930046AF29 /* PopUpStoreRegisterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E643FC22D738D930046AF29 /* PopUpStoreRegisterView.swift */; }; - 4E685ECE2D12CEB6001EF91C /* BalloonBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EAA2D12CEB6001EF91C /* BalloonBackgroundView.swift */; }; - 4E685ECF2D12CEB6001EF91C /* BalloonChipCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EAB2D12CEB6001EF91C /* BalloonChipCell.swift */; }; - 4E685ED12D12CEB6001EF91C /* FilterBottomSheetReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EAD2D12CEB6001EF91C /* FilterBottomSheetReactor.swift */; }; - 4E685ED22D12CEB6001EF91C /* FilterBottomSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EAE2D12CEB6001EF91C /* FilterBottomSheetView.swift */; }; - 4E685ED32D12CEB6001EF91C /* FilterBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EAF2D12CEB6001EF91C /* FilterBottomSheetViewController.swift */; }; - 4E685ED42D12CEB6001EF91C /* FilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EB02D12CEB6001EF91C /* FilterCell.swift */; }; - 4E685ED52D12CEB6001EF91C /* FilterChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EB12D12CEB6001EF91C /* FilterChip.swift */; }; - 4E685ED62D12CEB6001EF91C /* FilterChipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EB22D12CEB6001EF91C /* FilterChipsView.swift */; }; - 4E685ED92D12CEB6001EF91C /* MapRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EB62D12CEB6001EF91C /* MapRepository.swift */; }; - 4E685EDA2D12CEB6001EF91C /* MapUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EB82D12CEB6001EF91C /* MapUseCase.swift */; }; - 4E685EDB2D12CEB6001EF91C /* MapAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EBA2D12CEB6001EF91C /* MapAPIEndpoint.swift */; }; - 4E685EDD2D12CEB6001EF91C /* MapPopUpStoreDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EBC2D12CEB6001EF91C /* MapPopUpStoreDTO.swift */; }; - 4E685EDE2D12CEB6001EF91C /* MapPopupCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EBE2D12CEB6001EF91C /* MapPopupCarouselView.swift */; }; - 4E685EDF2D12CEB6001EF91C /* PopupCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EBF2D12CEB6001EF91C /* PopupCardCell.swift */; }; - 4E685EE02D12CEB6001EF91C /* StoreListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC12D12CEB6001EF91C /* StoreListCell.swift */; }; - 4E685EE12D12CEB6001EF91C /* StoreListReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC22D12CEB6001EF91C /* StoreListReactor.swift */; }; - 4E685EE22D12CEB6001EF91C /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC32D12CEB6001EF91C /* StoreListView.swift */; }; - 4E685EE32D12CEB6001EF91C /* StoreListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC42D12CEB6001EF91C /* StoreListViewController.swift */; }; - 4E685EE42D12CEB6001EF91C /* MapFilterChips.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC62D12CEB6001EF91C /* MapFilterChips.swift */; }; - 4E685EE52D12CEB6001EF91C /* MapMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC72D12CEB6001EF91C /* MapMarker.swift */; }; - 4E685EE62D12CEB6001EF91C /* MapReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC82D12CEB6001EF91C /* MapReactor.swift */; }; - 4E685EE72D12CEB6001EF91C /* MapSearchInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EC92D12CEB6001EF91C /* MapSearchInput.swift */; }; - 4E685EE92D12CEB6001EF91C /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685ECB2D12CEB6001EF91C /* MapView.swift */; }; - 4E685EEA2D12CEB6001EF91C /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685ECC2D12CEB6001EF91C /* MapViewController.swift */; }; - 4E6A06702D42A96100B2A658 /* FullScreenMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6A066F2D42A96100B2A658 /* FullScreenMapViewController.swift */; }; - 4E6C07062D4B6E56008A962A /* RegionDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C07052D4B6E56008A962A /* RegionDefinitions.swift */; }; - 4E6C07082D4B6E74008A962A /* ClusteringModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C07072D4B6E74008A962A /* ClusteringModels.swift */; }; - 4E6C070A2D4B6E81008A962A /* ClusteringManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C07092D4B6E81008A962A /* ClusteringManager.swift */; }; - 4E6CA4852D34D6ED0034D09A /* AdminBottomSheetReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6CA4842D34D6ED0034D09A /* AdminBottomSheetReactor.swift */; }; - 4E755B1D2D2B9AD300ADFB21 /* GetAdminPopUpStoreListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B1C2D2B9AD300ADFB21 /* GetAdminPopUpStoreListResponseDTO.swift */; }; - 4E755B1F2D2B9AE500ADFB21 /* AdminAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B1E2D2B9AE500ADFB21 /* AdminAPIEndpoint.swift */; }; - 4E755B212D2B9BAB00ADFB21 /* AdminResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B202D2B9BAB00ADFB21 /* AdminResponseDTO.swift */; }; - 4E755B232D2B9C5D00ADFB21 /* AdminViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B222D2B9C5D00ADFB21 /* AdminViewController.swift */; }; - 4E755B252D2B9C6C00ADFB21 /* AdminView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B242D2B9C6C00ADFB21 /* AdminView.swift */; }; - 4E755B292D2BA65A00ADFB21 /* AdminReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B282D2BA65A00ADFB21 /* AdminReactor.swift */; }; - 4E755B2B2D2BA76E00ADFB21 /* AdminUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B2A2D2BA76E00ADFB21 /* AdminUseCase.swift */; }; - 4E7823A82D2E84E800AC5110 /* AdminRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B2E2D2BA7FB00ADFB21 /* AdminRepository.swift */; }; - 4E7823A92D2E84FB00AC5110 /* AdminStoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B262D2B9C7C00ADFB21 /* AdminStoreCell.swift */; }; - 4E78706E2D37CB1900465FC9 /* ProfileEditListButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081898BF2D2FBD130067BF01 /* ProfileEditListButton.swift */; }; - 4E78706F2D37CB2200465FC9 /* PopUpStoreRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9C12802D2BE0A6006744D6 /* PopUpStoreRegisterViewController.swift */; }; - 4E8AA29D2D59A2340029DF75 /* MarkerTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8AA29C2D59A2340029DF75 /* MarkerTooltipView.swift */; }; - 4E9790C52D40E13500210499 /* MapGuideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9790C42D40E13500210499 /* MapGuideViewController.swift */; }; - 4E9A465E2D50B2DB0010578A /* AdminStoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E755B262D2B9C7C00ADFB21 /* AdminStoreCell.swift */; }; - 4E9A46602D55D1270010578A /* MapUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A465F2D55D1270010578A /* MapUtilities.swift */; }; - 4E9C12782D2BC7A0006744D6 /* AdminBottomSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9C12772D2BC7A0006744D6 /* AdminBottomSheetView.swift */; }; - 4E9C127A2D2BC811006744D6 /* AdminBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9C12792D2BC811006744D6 /* AdminBottomSheetViewController.swift */; }; - 4EA2C93D2D424D3300F4D97C /* MapDirectionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA2C93C2D424D3300F4D97C /* MapDirectionRepository.swift */; }; - 4EA2C93F2D424D7400F4D97C /* MapDirectionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA2C93E2D424D7400F4D97C /* MapDirectionUseCase.swift */; }; - 4EA2C9412D424D8400F4D97C /* GetPopUpDirectionResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA2C9402D424D8400F4D97C /* GetPopUpDirectionResponseDTO.swift */; }; - 4EA2C9432D424DF900F4D97C /* FindDirectionEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA2C9422D424DF900F4D97C /* FindDirectionEndPoint.swift */; }; - 4EA9989A2D21C2FC009DC30B /* StoreListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA998992D21C2FC009DC30B /* StoreListSection.swift */; }; - 4EA9989D2D21C404009DC30B /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA9989C2D21C404009DC30B /* RxDataSources */; }; - 4EAB809D2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAB809C2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift */; }; - 4EAB809F2D3F8EF50041AF30 /* ViewportBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAB809E2D3F8EF50041AF30 /* ViewportBounds.swift */; }; - 4EDDEFB42D2D285900CFAFA5 /* DateTimePickerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDDEFB32D2D285900CFAFA5 /* DateTimePickerManager.swift */; }; - 4EDE57012D5E6A5F0014D924 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685ECC2D12CEB6001EF91C /* MapViewController.swift */; }; - 4EDE57032D5E70650014D924 /* LocationPermissionBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDE57022D5E70650014D924 /* LocationPermissionBottomSheet.swift */; }; - 4EE5A3D32D40E4A600A2469A /* MapGuideReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE5A3D22D40E4A600A2469A /* MapGuideReactor.swift */; }; - 4EEA1D8F2D352012003E7DE9 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEA1D8E2D352012003E7DE9 /* ImageCell.swift */; }; - 4EEA1D912D352027003E7DE9 /* ExtendedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEA1D902D352027003E7DE9 /* ExtendedImage.swift */; }; - 4EECA3942D56770B00A07CCA /* MapPopUpStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E685EBB2D12CEB6001EF91C /* MapPopUpStore.swift */; }; - 4EED9BAC2D22730400B288E7 /* FilterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED9BAB2D22730400B288E7 /* FilterType.swift */; }; - BD226D512CF6DB290038C984 /* PPReturnHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD226D502CF6DB290038C984 /* PPReturnHeaderView.swift */; }; - BD9103612CF6149D00BBCCAE /* AuthAPIEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91034E2CF6149D00BBCCAE /* AuthAPIEndPoint.swift */; }; - BD9103622CF6149D00BBCCAE /* LoginResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91034F2CF6149D00BBCCAE /* LoginResponseDTO.swift */; }; - BD9103632CF6149D00BBCCAE /* BannerPopUpStoreDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103512CF6149D00BBCCAE /* BannerPopUpStoreDTO.swift */; }; - BD9103642CF6149D00BBCCAE /* GetHomeInfoResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103522CF6149D00BBCCAE /* GetHomeInfoResponseDTO.swift */; }; - BD9103652CF6149D00BBCCAE /* HomeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103532CF6149D00BBCCAE /* HomeAPIEndpoint.swift */; }; - BD9103662CF6149D00BBCCAE /* PopUpStoreResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103542CF6149D00BBCCAE /* PopUpStoreResponseDTO.swift */; }; - BD9103682CF6149D00BBCCAE /* AuthAPIRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103572CF6149D00BBCCAE /* AuthAPIRepositoryImpl.swift */; }; - BD9103692CF6149D00BBCCAE /* HomeAPIRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103582CF6149D00BBCCAE /* HomeAPIRepository.swift */; }; - BD91036A2CF6149D00BBCCAE /* SignUpRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103592CF6149D00BBCCAE /* SignUpRepositoryImpl.swift */; }; - BD91036B2CF6149D00BBCCAE /* CheckNickNameRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91035B2CF6149D00BBCCAE /* CheckNickNameRequestDTO.swift */; }; - BD91036C2CF6149D00BBCCAE /* GetCategoryListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91035C2CF6149D00BBCCAE /* GetCategoryListResponseDTO.swift */; }; - BD91036D2CF6149D00BBCCAE /* SignUpAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91035D2CF6149D00BBCCAE /* SignUpAPIEndpoint.swift */; }; - BD91036E2CF6149D00BBCCAE /* SignUpRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91035E2CF6149D00BBCCAE /* SignUpRequestDTO.swift */; }; - BD9103812CF614A900BBCCAE /* HomeAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103702CF614A900BBCCAE /* HomeAPIUseCaseImpl.swift */; }; - BD9103832CF614A900BBCCAE /* SignUpAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103722CF614A900BBCCAE /* SignUpAPIUseCaseImpl.swift */; }; - BD9103852CF614A900BBCCAE /* AuthAPIUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103742CF614A900BBCCAE /* AuthAPIUseCaseImpl.swift */; }; - BD9103862CF614A900BBCCAE /* BannerPopUpStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103762CF614A900BBCCAE /* BannerPopUpStore.swift */; }; - BD9103872CF614A900BBCCAE /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103772CF614A900BBCCAE /* Category.swift */; }; - BD9103882CF614A900BBCCAE /* GetHomeInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103782CF614A900BBCCAE /* GetHomeInfoResponse.swift */; }; - BD9103892CF614A900BBCCAE /* PopUpStoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103792CF614A900BBCCAE /* PopUpStoreResponse.swift */; }; - BD91038A2CF614A900BBCCAE /* LoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91037A2CF614A900BBCCAE /* LoginResponse.swift */; }; - BD91038B2CF614A900BBCCAE /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91037C2CF614A900BBCCAE /* AuthRepository.swift */; }; - BD9103922CF6166800BBCCAE /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD91038E2CF6166800BBCCAE /* SplashView.swift */; }; - BD9103932CF6166800BBCCAE /* SplashController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9103902CF6166800BBCCAE /* SplashController.swift */; }; - BDCA41C12CF35AC0005EECF6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41C02CF35AC0005EECF6 /* AppDelegate.swift */; }; - BDCA41C32CF35AC0005EECF6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41C22CF35AC0005EECF6 /* SceneDelegate.swift */; }; - BDCA41C52CF35AC0005EECF6 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41C42CF35AC0005EECF6 /* TestViewController.swift */; }; - BDCA41CA2CF35AC1005EECF6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BDCA41C92CF35AC1005EECF6 /* Assets.xcassets */; }; - BDCA41CD2CF35AC1005EECF6 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = BDCA41CC2CF35AC1005EECF6 /* Base */; }; - BDCA41D82CF35AC1005EECF6 /* PoppoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41D72CF35AC1005EECF6 /* PoppoolTests.swift */; }; - BDCA41E22CF35AC1005EECF6 /* PoppoolUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41E12CF35AC1005EECF6 /* PoppoolUITests.swift */; }; - BDCA41E42CF35AC1005EECF6 /* PoppoolUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCA41E32CF35AC1005EECF6 /* PoppoolUITestsLaunchTests.swift */; }; - BDCA41F22CF35D0D005EECF6 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA41F12CF35D0D005EECF6 /* SnapKit */; }; - BDCA41F52CF35D33005EECF6 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA41F42CF35D33005EECF6 /* Kingfisher */; }; - BDCA41F82CF35D9A005EECF6 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA41F72CF35D9A005EECF6 /* RxSwift */; }; - BDCA41FE2CF35EE7005EECF6 /* ReactorKit in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA41FD2CF35EE7005EECF6 /* ReactorKit */; }; - BDCA42012CF35EFE005EECF6 /* RxKeyboard in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA42002CF35EFE005EECF6 /* RxKeyboard */; }; - BDCA42042CF35F76005EECF6 /* PanModal in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA42032CF35F76005EECF6 /* PanModal */; }; - BDCA42072CF35FA6005EECF6 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA42062CF35FA6005EECF6 /* Tabman */; }; - BDCA420A2CF35FB1005EECF6 /* Pageboy in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA42092CF35FB1005EECF6 /* Pageboy */; }; - BDCA420D2CF35FD2005EECF6 /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA420C2CF35FD2005EECF6 /* RxGesture */; }; - BDCA42102CF35FF5005EECF6 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA420F2CF35FF5005EECF6 /* Lottie */; }; + 0522C1D92DB67C5900B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1D82DB67C5900B141FF /* RxSwift */; }; + 0522C3C62DB67D7800B141FF /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C3C52DB67D7800B141FF /* RxGesture */; }; + 0543C5CD2DF86C740070BB93 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6072DB53A4900508FFD /* Data.framework */; }; + 0543C5CE2DF86C740070BB93 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6072DB53A4900508FFD /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5D02DF86C790070BB93 /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05CFFC422DCC83290051129F /* DesignSystem.framework */; }; + 0543C5D12DF86C790070BB93 /* DesignSystem.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05CFFC422DCC83290051129F /* DesignSystem.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5D22DF86C7B0070BB93 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6082DB53A4900508FFD /* Domain.framework */; }; + 0543C5D32DF86C7B0070BB93 /* Domain.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6082DB53A4900508FFD /* Domain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5D42DF86C7C0070BB93 /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05BDD3D52DB66E1700C1E192 /* DomainInterface.framework */; }; + 0543C5D52DF86C7C0070BB93 /* DomainInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05BDD3D52DB66E1700C1E192 /* DomainInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5D62DF86C7E0070BB93 /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6092DB53A4900508FFD /* Infrastructure.framework */; }; + 0543C5D72DF86C7E0070BB93 /* Infrastructure.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D6092DB53A4900508FFD /* Infrastructure.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5D82DF86C7F0070BB93 /* Presentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D60A2DB53A4900508FFD /* Presentation.framework */; }; + 0543C5D92DF86C7F0070BB93 /* Presentation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D60A2DB53A4900508FFD /* Presentation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5DA2DF86C800070BB93 /* PresentationInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C5E2DCE04CE0093825D /* PresentationInterface.framework */; }; + 0543C5DB2DF86C800070BB93 /* PresentationInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C5E2DCE04CE0093825D /* PresentationInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5DC2DF86C810070BB93 /* SearchFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C582DCDFAC20093825D /* SearchFeature.framework */; }; + 0543C5DD2DF86C810070BB93 /* SearchFeature.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C582DCDFAC20093825D /* SearchFeature.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0543C5DE2DF86C830070BB93 /* SearchFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C592DCDFAC20093825D /* SearchFeatureInterface.framework */; }; + 0543C5DF2DF86C830070BB93 /* SearchFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C592DCDFAC20093825D /* SearchFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05734C632DCE04FA0093825D /* Pageboy in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C622DCE04FA0093825D /* Pageboy */; }; + 05734C662DCE05070093825D /* PanModal in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C652DCE05070093825D /* PanModal */; }; + 05734C682DCE05240093825D /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C672DCE05240093825D /* SnapKit */; }; + 05734C6B2DCE05550093825D /* ReactorKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C6A2DCE05550093825D /* ReactorKit */; }; + 05734C6E2DCE05680093825D /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C6D2DCE05680093825D /* Tabman */; }; + 05734C712DCE059D0093825D /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C702DCE059D0093825D /* Then */; }; + 05BBA73E2DB75DA60047A061 /* KakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 05BBA73D2DB75DA60047A061 /* KakaoSDKUser */; }; + 4E15142A2D99480200DFD08F /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 4E1514292D99480200DFD08F /* NMapsMap */; }; + 4E15142E2D994A3A00DFD08F /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */; }; + 4EE360FD2D91876300D2441D /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE360FC2D91876300D2441D /* NMapsMap */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - BDCA41D42CF35AC1005EECF6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BDCA41B52CF35AC0005EECF6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BDCA41BC2CF35AC0005EECF6; - remoteInfo = Poppool; - }; - BDCA41DE2CF35AC1005EECF6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BDCA41B52CF35AC0005EECF6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BDCA41BC2CF35AC0005EECF6; - remoteInfo = Poppool; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 0818988D2D295DC30067BF01 /* MyPageLogoutSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageLogoutSection.swift; sourceTree = ""; }; - 0818988F2D295DC80067BF01 /* MyPageLogoutSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageLogoutSectionCell.swift; sourceTree = ""; }; - 081898932D2965C20067BF01 /* ProfileEditController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditController.swift; sourceTree = ""; }; - 081898952D2965C90067BF01 /* ProfileEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditView.swift; sourceTree = ""; }; - 081898972D2965D20067BF01 /* ProfileEditReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditReactor.swift; sourceTree = ""; }; - 0818989B2D2BAA570067BF01 /* WithdrawlCheckModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCheckModalView.swift; sourceTree = ""; }; - 0818989D2D2BAA610067BF01 /* WithdrawlCheckModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCheckModalController.swift; sourceTree = ""; }; - 0818989F2D2BAA670067BF01 /* WithdrawlCheckModalReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCheckModalReactor.swift; sourceTree = ""; }; - 081898A22D2CC0110067BF01 /* WithdrawlReasonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlReasonView.swift; sourceTree = ""; }; - 081898A42D2CC0180067BF01 /* WithdrawlReasonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlReasonController.swift; sourceTree = ""; }; - 081898A62D2CC01D0067BF01 /* WithdrawlReasonReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlReasonReactor.swift; sourceTree = ""; }; - 081898A92D2CEA2F0067BF01 /* WithdrawlCheckSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCheckSectionCell.swift; sourceTree = ""; }; - 081898AB2D2CEA940067BF01 /* WithdrawlCheckSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCheckSection.swift; sourceTree = ""; }; - 081898AD2D2CFC230067BF01 /* GetWithdrawlListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetWithdrawlListResponseDTO.swift; sourceTree = ""; }; - 081898AF2D2CFCA40067BF01 /* GetWithdrawlListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetWithdrawlListResponse.swift; sourceTree = ""; }; - 081898B22D2D20D70067BF01 /* WithdrawlCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCompleteView.swift; sourceTree = ""; }; - 081898B42D2D20E30067BF01 /* WithdrawlCompleteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawlCompleteController.swift; sourceTree = ""; }; - 081898B62D2D23A90067BF01 /* UINavigationController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+.swift"; sourceTree = ""; }; - 081898B92D2E5F4C0067BF01 /* MyCommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentView.swift; sourceTree = ""; }; - 081898BB2D2E5F510067BF01 /* MyCommentReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentReactor.swift; sourceTree = ""; }; - 081898BD2D2E5F590067BF01 /* MyCommentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentController.swift; sourceTree = ""; }; - 081898BF2D2FBD130067BF01 /* ProfileEditListButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditListButton.swift; sourceTree = ""; }; - 081898C22D30AE2C0067BF01 /* GetMyProfileResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyProfileResponseDTO.swift; sourceTree = ""; }; - 081898C42D30AEF40067BF01 /* GetMyProfileResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyProfileResponse.swift; sourceTree = ""; }; - 081898C92D30D5BA0067BF01 /* InfoEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoEditModalView.swift; sourceTree = ""; }; - 081898CB2D30D5BF0067BF01 /* InfoEditModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoEditModalController.swift; sourceTree = ""; }; - 081898CD2D30D5C60067BF01 /* InfoEditModalReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoEditModalReactor.swift; sourceTree = ""; }; - 081898CF2D30EA900067BF01 /* PutUserTailoredInfoRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PutUserTailoredInfoRequestDTO.swift; sourceTree = ""; }; - 081898D12D30F57D0067BF01 /* CategoryEditModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEditModalView.swift; sourceTree = ""; }; - 081898D32D30F5840067BF01 /* CategoryEditModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEditModalController.swift; sourceTree = ""; }; - 081898D52D30F58A0067BF01 /* CategoryEditModalReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEditModalReactor.swift; sourceTree = ""; }; - 081898D72D310C160067BF01 /* PutUserCategoryRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PutUserCategoryRequestDTO.swift; sourceTree = ""; }; - 081898D92D32559B0067BF01 /* PutUserProfileRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PutUserProfileRequestDTO.swift; sourceTree = ""; }; - 081898DB2D326DC10067BF01 /* IntroState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroState.swift; sourceTree = ""; }; - 081898DF2D338F9C0067BF01 /* ListCountButtonSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCountButtonSection.swift; sourceTree = ""; }; - 081898E12D338FA40067BF01 /* ListCountButtonSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCountButtonSectionCell.swift; sourceTree = ""; }; - 081898E32D3391550067BF01 /* GetMyCommentedPopUpResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyCommentedPopUpResponseDTO.swift; sourceTree = ""; }; - 081898E52D3391CB0067BF01 /* GetMyCommentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyCommentResponse.swift; sourceTree = ""; }; - 081898E72D3392480067BF01 /* GetMyCommentRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyCommentRequestDTO.swift; sourceTree = ""; }; - 081898EB2D33A3960067BF01 /* MyCommentSortedModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentSortedModalView.swift; sourceTree = ""; }; - 081898ED2D33A39D0067BF01 /* MyCommentSortedModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentSortedModalController.swift; sourceTree = ""; }; - 081898EF2D33A3A30067BF01 /* MyCommentSortedModalReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentSortedModalReactor.swift; sourceTree = ""; }; - 081898F22D33D6AC0067BF01 /* BlockUserManageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserManageView.swift; sourceTree = ""; }; - 081898F42D33D6B10067BF01 /* BlockUserManageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserManageController.swift; sourceTree = ""; }; - 081898F62D33D6B70067BF01 /* BlockUserManageReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserManageReactor.swift; sourceTree = ""; }; - 081898FA2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBlockUserListResponseDTO.swift; sourceTree = ""; }; - 081898FC2D33D9ED0067BF01 /* GetBlockUserListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBlockUserListResponse.swift; sourceTree = ""; }; - 081898FE2D33DA440067BF01 /* GetBlockUserListRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBlockUserListRequestDTO.swift; sourceTree = ""; }; - 081899012D3407F50067BF01 /* BlockUserListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserListSection.swift; sourceTree = ""; }; - 081899042D34080B0067BF01 /* BlockUserListSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserListSectionCell.swift; sourceTree = ""; }; - 081899072D34B35A0067BF01 /* MyPageNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeView.swift; sourceTree = ""; }; - 081899092D34B3620067BF01 /* MyPageNoticeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeController.swift; sourceTree = ""; }; - 0818990B2D34B3670067BF01 /* MyPageNoticeReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeReactor.swift; sourceTree = ""; }; - 0818990D2D34B68C0067BF01 /* GetNoticeListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNoticeListResponseDTO.swift; sourceTree = ""; }; - 0818990F2D34B7240067BF01 /* GetNoticeListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNoticeListResponse.swift; sourceTree = ""; }; - 081899112D34CA9E0067BF01 /* GetNoticeDetailResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNoticeDetailResponseDTO.swift; sourceTree = ""; }; - 081899132D34CAEA0067BF01 /* GetNoticeDetailResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNoticeDetailResponse.swift; sourceTree = ""; }; - 081899172D34D63E0067BF01 /* NoticeListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeListSection.swift; sourceTree = ""; }; - 081899192D34D6430067BF01 /* NoticeListSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeListSectionCell.swift; sourceTree = ""; }; - 0818991D2D34DF7D0067BF01 /* MyPageNoticeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeDetailView.swift; sourceTree = ""; }; - 0818991F2D34DF880067BF01 /* MyPageNoticeDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeDetailController.swift; sourceTree = ""; }; - 081899212D34DF8E0067BF01 /* MyPageNoticeDetailReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageNoticeDetailReactor.swift; sourceTree = ""; }; - 081899242D3500B80067BF01 /* FAQView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAQView.swift; sourceTree = ""; }; - 081899262D3500BF0067BF01 /* FAQController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAQController.swift; sourceTree = ""; }; - 081899282D3500C50067BF01 /* FAQReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAQReactor.swift; sourceTree = ""; }; - 0818992C2D3506240067BF01 /* FAQDropdownSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAQDropdownSectionCell.swift; sourceTree = ""; }; - 0818992E2D3506290067BF01 /* FAQDropdownSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAQDropdownSection.swift; sourceTree = ""; }; - 081899322D35F1090067BF01 /* MyPageBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageBookmarkView.swift; sourceTree = ""; }; - 081899342D35F10F0067BF01 /* MyPageBookmarkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageBookmarkController.swift; sourceTree = ""; }; - 081899362D35F1140067BF01 /* MyPageBookmarkReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageBookmarkReactor.swift; sourceTree = ""; }; - 081899382D35F11F0067BF01 /* MyPageRecentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageRecentView.swift; sourceTree = ""; }; - 0818993A2D35F1250067BF01 /* MyPageRecentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageRecentController.swift; sourceTree = ""; }; - 0818993C2D35F12A0067BF01 /* MyPageRecentReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageRecentReactor.swift; sourceTree = ""; }; - 0818993E2D35FBE00067BF01 /* GetRecentPopUpResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRecentPopUpResponseDTO.swift; sourceTree = ""; }; - 081899402D35FDA10067BF01 /* GetRecentPopUpResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRecentPopUpResponse.swift; sourceTree = ""; }; - 081899442D35FEA10067BF01 /* RecentPopUpSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPopUpSection.swift; sourceTree = ""; }; - 081899492D36322B0067BF01 /* PopUpCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpCardSection.swift; sourceTree = ""; }; - 0818994B2D3632320067BF01 /* PopUpCardSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpCardSectionCell.swift; sourceTree = ""; }; - 0818994F2D363E5C0067BF01 /* BookMarkPopUpViewTypeModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMarkPopUpViewTypeModalView.swift; sourceTree = ""; }; - 081899512D363E640067BF01 /* BookMarkPopUpViewTypeModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMarkPopUpViewTypeModalController.swift; sourceTree = ""; }; - 081899532D363E6A0067BF01 /* BookMarkPopUpViewTypeModalReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMarkPopUpViewTypeModalReactor.swift; sourceTree = ""; }; - 082197A62D4E3EE00054094A /* CommentMyMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMyMenuView.swift; sourceTree = ""; }; - 082197A82D4E3EE90054094A /* CommentMyMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMyMenuController.swift; sourceTree = ""; }; - 082197AA2D4E3EEF0054094A /* CommentMyMenuReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentMyMenuReactor.swift; sourceTree = ""; }; - 082197AC2D4E49370054094A /* DeleteCommentRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCommentRequestDTO.swift; sourceTree = ""; }; - 082197AF2D4E4E190054094A /* NormalCommentEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentEditView.swift; sourceTree = ""; }; - 082197B12D4E4E200054094A /* NormalCommentEditController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentEditController.swift; sourceTree = ""; }; - 082197B32D4E4E280054094A /* NormalCommentEditReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentEditReactor.swift; sourceTree = ""; }; - 083A257F2CF361EF0099B58E /* BaseTabmanController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTabmanController.swift; sourceTree = ""; }; - 083A25802CF361EF0099B58E /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; - 083A25842CF361F90099B58E /* ControllerConvention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerConvention.swift; sourceTree = ""; }; - 083A25852CF361F90099B58E /* ConventionCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConventionCollectionViewCell.swift; sourceTree = ""; }; - 083A25862CF361F90099B58E /* ConventionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConventionTableViewCell.swift; sourceTree = ""; }; - 083A25872CF361F90099B58E /* ReactorConvention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactorConvention.swift; sourceTree = ""; }; - 083A25882CF361F90099B58E /* TestDynamicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDynamicCell.swift; sourceTree = ""; }; - 083A25892CF361F90099B58E /* TestDynamicSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDynamicSection.swift; sourceTree = ""; }; - 083A258A2CF361F90099B58E /* ViewConvention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewConvention.swift; sourceTree = ""; }; - 083A25932CF362090099B58E /* Sectionable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sectionable.swift; sourceTree = ""; }; - 083A25942CF362090099B58E /* SectionDecorationItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionDecorationItem.swift; sourceTree = ""; }; - 083A25952CF362090099B58E /* SectionSupplementaryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionSupplementaryItem.swift; sourceTree = ""; }; - 083A25972CF362090099B58E /* InOutputable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InOutputable.swift; sourceTree = ""; }; - 083A25A12CF362670099B58E /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; - 083A25A22CF362670099B58E /* Requestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Requestable.swift; sourceTree = ""; }; - 083A25A32CF362670099B58E /* Responsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Responsable.swift; sourceTree = ""; }; - 083A25A52CF362670099B58E /* Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; - 083A25A62CF362670099B58E /* MultipartEndPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartEndPoint.swift; sourceTree = ""; }; - 083A25A72CF362670099B58E /* RequestEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestEndpoint.swift; sourceTree = ""; }; - 083A25A92CF362670099B58E /* IndicatorMaker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorMaker.swift; sourceTree = ""; }; - 083A25AB2CF362670099B58E /* FormDataInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormDataInterceptor.swift; sourceTree = ""; }; - 083A25AC2CF362670099B58E /* TokenInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInterceptor.swift; sourceTree = ""; }; - 083A25AE2CF362670099B58E /* Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; }; - 083A25AF2CF362670099B58E /* ProviderImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderImpl.swift; sourceTree = ""; }; - 083A25BD2CF362770099B58E /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - 083A25C72CF363C00099B58E /* LoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginController.swift; sourceTree = ""; }; - 083A25C92CF363C60099B58E /* LoginReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginReactor.swift; sourceTree = ""; }; - 083A25CB2CF363CB0099B58E /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; - 083C860A2D073A15003F441C /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; - 083C860C2D073A1C003F441C /* DetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailController.swift; sourceTree = ""; }; - 083C860E2D073A23003F441C /* DetailReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailReactor.swift; sourceTree = ""; }; - 083C861B2D087337003F441C /* GetPopUpDetailRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpDetailRequestDTO.swift; sourceTree = ""; }; - 083C861D2D08737F003F441C /* GetPopUpDetailResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpDetailResponseDTO.swift; sourceTree = ""; }; - 083C861F2D087445003F441C /* GetPopUpDetailResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpDetailResponse.swift; sourceTree = ""; }; - 083C86232D087A44003F441C /* DetailTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitleSection.swift; sourceTree = ""; }; - 083C86252D087A4E003F441C /* DetailTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitleSectionCell.swift; sourceTree = ""; }; - 083C86282D088080003F441C /* DetailContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContentSection.swift; sourceTree = ""; }; - 083C862A2D08808C003F441C /* DetailContentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContentSectionCell.swift; sourceTree = ""; }; - 083C86352D0C7EF4003F441C /* CommentSelectedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentSelectedController.swift; sourceTree = ""; }; - 083C86372D0C7EFC003F441C /* CommentSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentSelectedView.swift; sourceTree = ""; }; - 083C86392D0C7F0A003F441C /* CommentSelectedReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentSelectedReactor.swift; sourceTree = ""; }; - 083C863C2D0C8BC4003F441C /* NormalCommentAddController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentAddController.swift; sourceTree = ""; }; - 083C863E2D0C8BCE003F441C /* NormalCommentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentAddView.swift; sourceTree = ""; }; - 083C86402D0C8BD8003F441C /* NormalCommentAddReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalCommentAddReactor.swift; sourceTree = ""; }; - 083C86442D0DCDE8003F441C /* AddCommentTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentTitleSectionCell.swift; sourceTree = ""; }; - 083C86462D0DCDFB003F441C /* AddCommentTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentTitleSection.swift; sourceTree = ""; }; - 083C86492D0DCF96003F441C /* AddCommentDescriptionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentDescriptionSection.swift; sourceTree = ""; }; - 083C864B2D0DCF9B003F441C /* AddCommentDescriptionSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentDescriptionSectionCell.swift; sourceTree = ""; }; - 083C864E2D0DD3A6003F441C /* AddCommentImageSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentImageSection.swift; sourceTree = ""; }; - 083C86502D0DD3AB003F441C /* AddCommentImageSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentImageSectionCell.swift; sourceTree = ""; }; - 083C86532D0DD7E9003F441C /* AddCommentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentSection.swift; sourceTree = ""; }; - 083C86552D0DD7EE003F441C /* AddCommentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCommentSectionCell.swift; sourceTree = ""; }; - 083C86582D0DEFC3003F441C /* CommentCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCheckView.swift; sourceTree = ""; }; - 083C865A2D0DEFCF003F441C /* CommentCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCheckController.swift; sourceTree = ""; }; - 083C865C2D0DEFD5003F441C /* CommentCheckReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCheckReactor.swift; sourceTree = ""; }; - 083C865F2D0EC496003F441C /* InstaCommentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaCommentAddView.swift; sourceTree = ""; }; - 083C86612D0EC49E003F441C /* InstaCommentAddController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaCommentAddController.swift; sourceTree = ""; }; - 083C86632D0EC4A5003F441C /* InstaCommentAddReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaCommentAddReactor.swift; sourceTree = ""; }; - 083C86682D0ECB47003F441C /* InstaGuideSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaGuideSection.swift; sourceTree = ""; }; - 083C866A2D0ECB4F003F441C /* InstaGuideSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaGuideSectionCell.swift; sourceTree = ""; }; - 083C866D2D0ECB87003F441C /* InstaGuideChildSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaGuideChildSection.swift; sourceTree = ""; }; - 083C866F2D0ECB8E003F441C /* InstaGuideChildSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstaGuideChildSectionCell.swift; sourceTree = ""; }; - 083C86722D0EE2B1003F441C /* CommentAPIEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentAPIEndPoint.swift; sourceTree = ""; }; - 083C86752D0EE2CF003F441C /* PostCommentRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCommentRequestDTO.swift; sourceTree = ""; }; - 083C86772D0EE382003F441C /* CommentAPIRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentAPIRepository.swift; sourceTree = ""; }; - 083C86792D0EE3BB003F441C /* CommentAPIUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentAPIUseCaseImpl.swift; sourceTree = ""; }; - 0841BA7F2CF9F34100049E31 /* ImageBannerChildSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerChildSectionCell.swift; sourceTree = ""; }; - 0841BA812CF9F5DF00049E31 /* PreSignedService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreSignedService.swift; sourceTree = ""; }; - 0841BA842CF9F62300049E31 /* PreSignedURLDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreSignedURLDTO.swift; sourceTree = ""; }; - 0841BA852CF9F62300049E31 /* PresignedURLRequestDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresignedURLRequestDTO.swift; sourceTree = ""; }; - 0841BA862CF9F62300049E31 /* PreSignedURLResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreSignedURLResponseDTO.swift; sourceTree = ""; }; - 0841BA8B2CF9F67100049E31 /* PreSignedAPIEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreSignedAPIEndPoint.swift; sourceTree = ""; }; - 0841BA8D2CF9F8A100049E31 /* ImageBannerChildSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerChildSection.swift; sourceTree = ""; }; - 0841BAA22CFA31A300049E31 /* SpacingSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingSection.swift; sourceTree = ""; }; - 0841BAA42CFA31A900049E31 /* SpacingSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingSectionCell.swift; sourceTree = ""; }; - 0841BAA72CFA354500049E31 /* HomeTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTitleSection.swift; sourceTree = ""; }; - 0841BAA92CFA354C00049E31 /* HomeTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTitleSectionCell.swift; sourceTree = ""; }; - 0841BAAB2CFA35F300049E31 /* UILabel+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+.swift"; sourceTree = ""; }; - 0841BAAE2CFA38EA00049E31 /* HomeCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCardSection.swift; sourceTree = ""; }; - 0841BAB02CFA38F500049E31 /* HomeCardSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCardSectionCell.swift; sourceTree = ""; }; - 0841BAB32CFABED700049E31 /* HomePopularCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePopularCardSection.swift; sourceTree = ""; }; - 0841BAB52CFABEDC00049E31 /* HomePopularCardSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePopularCardSectionCell.swift; sourceTree = ""; }; - 0841BAB72CFAC41300049E31 /* SectionBackGroundDecorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionBackGroundDecorationView.swift; sourceTree = ""; }; - 0841BAB92CFAE5BE00049E31 /* HomeHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeHeaderView.swift; sourceTree = ""; }; - 0841BABB2CFB59E200049E31 /* String?+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String?+.swift"; sourceTree = ""; }; - 0841BABD2CFB5AA600049E31 /* Date?+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date?+.swift"; sourceTree = ""; }; - 0841BAC22CFB600800049E31 /* TabbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbarController.swift; sourceTree = ""; }; - 086DD8C72CFDEA9200B97D3B /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; - 086DD8CB2CFDFEA800B97D3B /* HomeListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeListController.swift; sourceTree = ""; }; - 086DD8CD2CFDFEB000B97D3B /* HomeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeListView.swift; sourceTree = ""; }; - 086DD8CF2CFDFEB900B97D3B /* HomeListReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeListReactor.swift; sourceTree = ""; }; - 086DD8D22CFDFF1500B97D3B /* HomePopUpType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePopUpType.swift; sourceTree = ""; }; - 086DD8D52CFF182100B97D3B /* UserAPIEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPIEndPoint.swift; sourceTree = ""; }; - 086DD8D72CFF185200B97D3B /* PostBookmarkPopUpRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBookmarkPopUpRequestDTO.swift; sourceTree = ""; }; - 086DD8D92CFF194700B97D3B /* UserAPIRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPIRepositoryImpl.swift; sourceTree = ""; }; - 086DD8DD2CFF19C400B97D3B /* UserAPIUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPIUseCaseImpl.swift; sourceTree = ""; }; - 086DD8DF2CFF2C3700B97D3B /* UIImageView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+.swift"; sourceTree = ""; }; - 086DD8E22CFF356300B97D3B /* HomeCardGridSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCardGridSection.swift; sourceTree = ""; }; - 086DD9292D0086AA00B97D3B /* SearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = ""; }; - 086DD92B2D0086B100B97D3B /* SearchReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchReactor.swift; sourceTree = ""; }; - 086DD92D2D0086B900B97D3B /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - 086DD92F2D0090E900B97D3B /* UITextField+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+.swift"; sourceTree = ""; }; - 086DD9332D00962500B97D3B /* SearchTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTitleSectionCell.swift; sourceTree = ""; }; - 086DD9352D00963900B97D3B /* SearchTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTitleSection.swift; sourceTree = ""; }; - 086DD93A2D009A1C00B97D3B /* CancelableTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableTagSection.swift; sourceTree = ""; }; - 086DD93C2D009A2600B97D3B /* CancelableTagSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableTagSectionCell.swift; sourceTree = ""; }; - 086DD93F2D01EEEB00B97D3B /* SearchCountTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCountTitleSection.swift; sourceTree = ""; }; - 086DD9412D01EEF700B97D3B /* SearchCountTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCountTitleSectionCell.swift; sourceTree = ""; }; - 086F89C22D1E347700CA4FC9 /* CommentUserInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserInfoView.swift; sourceTree = ""; }; - 086F89C42D1E347E00CA4FC9 /* CommentUserInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserInfoController.swift; sourceTree = ""; }; - 086F89C62D1E348400CA4FC9 /* CommentUserInfoReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserInfoReactor.swift; sourceTree = ""; }; - 086F89C92D1E42A700CA4FC9 /* CommentUserBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserBlockView.swift; sourceTree = ""; }; - 086F89CB2D1E42B000CA4FC9 /* CommentUserBlockController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserBlockController.swift; sourceTree = ""; }; - 086F89CD2D1E42B500CA4FC9 /* CommentUserBlockReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentUserBlockReactor.swift; sourceTree = ""; }; - 086F89CF2D1E60A100CA4FC9 /* PostUserBlockRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostUserBlockRequestDTO.swift; sourceTree = ""; }; - 086F89D22D1E6DA600CA4FC9 /* OtherUserCommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserCommentView.swift; sourceTree = ""; }; - 086F89D42D1E6DB100CA4FC9 /* OtherUserCommentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserCommentController.swift; sourceTree = ""; }; - 086F89D62D1E6DB700CA4FC9 /* OtherUserCommentReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserCommentReactor.swift; sourceTree = ""; }; - 086F89D82D1E79E200CA4FC9 /* GetOtherUserCommentListRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOtherUserCommentListRequestDTO.swift; sourceTree = ""; }; - 086F89DA2D1E7A6C00CA4FC9 /* GetOtherUserCommentedPopUpListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOtherUserCommentedPopUpListResponseDTO.swift; sourceTree = ""; }; - 086F89DF2D1E7CC700CA4FC9 /* GetOtherUserCommentedPopUpListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOtherUserCommentedPopUpListResponse.swift; sourceTree = ""; }; - 086F89E32D1FE91300CA4FC9 /* OtherUserCommentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserCommentSection.swift; sourceTree = ""; }; - 086F89E52D1FE91800CA4FC9 /* OtherUserCommentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserCommentSectionCell.swift; sourceTree = ""; }; - 086F89E92D2009E300CA4FC9 /* SubLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubLoginView.swift; sourceTree = ""; }; - 086F89EB2D2009EB00CA4FC9 /* SubLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubLoginController.swift; sourceTree = ""; }; - 086F89ED2D2009F100CA4FC9 /* SubLoginReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubLoginReactor.swift; sourceTree = ""; }; - 086F89F02D2269D800CA4FC9 /* MyPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageView.swift; sourceTree = ""; }; - 086F89F22D2269DE00CA4FC9 /* MyPageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageController.swift; sourceTree = ""; }; - 086F89F42D2269E300CA4FC9 /* MyPageReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageReactor.swift; sourceTree = ""; }; - 086F89F62D226DF600CA4FC9 /* GetMyPageResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyPageResponseDTO.swift; sourceTree = ""; }; - 086F89F82D226EEB00CA4FC9 /* GetMyPageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMyPageResponse.swift; sourceTree = ""; }; - 086F8A042D23CB3300CA4FC9 /* MyPageProfileSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageProfileSection.swift; sourceTree = ""; }; - 086F8A062D23CB3800CA4FC9 /* MyPageProfileSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageProfileSectionCell.swift; sourceTree = ""; }; - 086F8A092D2621EE00CA4FC9 /* MyPageMyCommentTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageMyCommentTitleSection.swift; sourceTree = ""; }; - 086F8A0B2D2621F400CA4FC9 /* MyPageMyCommentTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageMyCommentTitleSectionCell.swift; sourceTree = ""; }; - 086F8A0E2D26297900CA4FC9 /* MyPageCommentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageCommentSection.swift; sourceTree = ""; }; - 086F8A102D26297D00CA4FC9 /* MyPageCommentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageCommentSectionCell.swift; sourceTree = ""; }; - 086F8A172D265C5F00CA4FC9 /* MyPageListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageListSection.swift; sourceTree = ""; }; - 086F8A192D265C6300CA4FC9 /* MyPageListSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageListSectionCell.swift; sourceTree = ""; }; - 088DE2492D12F3360030FA9E /* DetailInfoSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailInfoSection.swift; sourceTree = ""; }; - 088DE24B2D12F33B0030FA9E /* DetailInfoSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailInfoSectionCell.swift; sourceTree = ""; }; - 088DE24E2D13019A0030FA9E /* DetailCommentTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentTitleSection.swift; sourceTree = ""; }; - 088DE2502D13019E0030FA9E /* DetailCommentTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentTitleSectionCell.swift; sourceTree = ""; }; - 088DE2532D144A7E0030FA9E /* DetailCommentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentSection.swift; sourceTree = ""; }; - 088DE2552D144A830030FA9E /* DetailCommentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentSectionCell.swift; sourceTree = ""; }; - 088DE2572D144B0F0030FA9E /* DetailCommentProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentProfileView.swift; sourceTree = ""; }; - 088DE2592D1458620030FA9E /* DetailCommentImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCommentImageCell.swift; sourceTree = ""; }; - 088DE25C2D145E3A0030FA9E /* DetailSimilarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSimilarSection.swift; sourceTree = ""; }; - 088DE25E2D145E3F0030FA9E /* DetailSimilarSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSimilarSectionCell.swift; sourceTree = ""; }; - 089952412D031E650022AEF9 /* SearchSortedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSortedController.swift; sourceTree = ""; }; - 089952432D031E6D0022AEF9 /* SearchSortedReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSortedReactor.swift; sourceTree = ""; }; - 089952452D031E740022AEF9 /* SearchSortedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSortedView.swift; sourceTree = ""; }; - 089952482D033A1C0022AEF9 /* PopUpAPIEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpAPIEndPoint.swift; sourceTree = ""; }; - 0899524A2D033A9C0022AEF9 /* GetClosePopUpListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetClosePopUpListResponseDTO.swift; sourceTree = ""; }; - 0899524C2D033AA70022AEF9 /* GetOpenPopUpListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOpenPopUpListResponseDTO.swift; sourceTree = ""; }; - 0899524E2D033B5A0022AEF9 /* GetSearchPopUpListRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSearchPopUpListRequestDTO.swift; sourceTree = ""; }; - 089952502D033C410022AEF9 /* GetSearchBottomPopUpListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSearchBottomPopUpListResponse.swift; sourceTree = ""; }; - 089952522D033C940022AEF9 /* PopUpAPIRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpAPIRepositoryImpl.swift; sourceTree = ""; }; - 089952542D033D480022AEF9 /* PopUpAPIUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpAPIUseCaseImpl.swift; sourceTree = ""; }; - 089952572D0347AC0022AEF9 /* SearchCategoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCategoryController.swift; sourceTree = ""; }; - 089952592D0347B40022AEF9 /* SearchCategoryReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCategoryReactor.swift; sourceTree = ""; }; - 0899525B2D0347BD0022AEF9 /* SearchCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCategoryView.swift; sourceTree = ""; }; - 0899525F2D0366C40022AEF9 /* SearchMainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMainController.swift; sourceTree = ""; }; - 089952612D0366D30022AEF9 /* SearchMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMainView.swift; sourceTree = ""; }; - 089952632D0366DA0022AEF9 /* SearchMainReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMainReactor.swift; sourceTree = ""; }; - 089952652D046CCD0022AEF9 /* SearchResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultController.swift; sourceTree = ""; }; - 089952672D046CD80022AEF9 /* SearchResultReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultReactor.swift; sourceTree = ""; }; - 089952692D046CDE0022AEF9 /* SearchResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultView.swift; sourceTree = ""; }; - 0899526B2D0473EC0022AEF9 /* GetSearchPopUpListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSearchPopUpListResponseDTO.swift; sourceTree = ""; }; - 0899526D2D0474340022AEF9 /* GetSearchPopUpListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSearchPopUpListResponse.swift; sourceTree = ""; }; - 089952722D0475E90022AEF9 /* SearchResultCountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCountSection.swift; sourceTree = ""; }; - 089952742D0475F20022AEF9 /* SearchResultCountSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCountSectionCell.swift; sourceTree = ""; }; - 08A2E46B2D15BC5000102313 /* CommentLikeRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentLikeRequestDTO.swift; sourceTree = ""; }; - 08A2E4782D1B06A300102313 /* ImageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailView.swift; sourceTree = ""; }; - 08A2E47A2D1B06AA00102313 /* ImageDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailController.swift; sourceTree = ""; }; - 08A2E47C2D1B06B000102313 /* ImageDetailReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailReactor.swift; sourceTree = ""; }; - 08A2E47F2D1BCDE300102313 /* CommentDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailView.swift; sourceTree = ""; }; - 08A2E4812D1BCDEA00102313 /* CommentDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailController.swift; sourceTree = ""; }; - 08A2E4832D1BCDEF00102313 /* CommentDetailReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailReactor.swift; sourceTree = ""; }; - 08A2E4852D1BD85C00102313 /* CommentDetailImageSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailImageSection.swift; sourceTree = ""; }; - 08A2E4892D1BDA8400102313 /* CommentDetailContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailContentSection.swift; sourceTree = ""; }; - 08A2E48B2D1BDA8A00102313 /* CommentDetailContentSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailContentSectionCell.swift; sourceTree = ""; }; - 08A2E48E2D1BF6E500102313 /* CommentListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentListView.swift; sourceTree = ""; }; - 08A2E4902D1BF6EA00102313 /* CommentListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentListController.swift; sourceTree = ""; }; - 08A2E4922D1BF6EF00102313 /* CommentListReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentListReactor.swift; sourceTree = ""; }; - 08A2E4942D1C078300102313 /* GetPopUpCommentRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpCommentRequestDTO.swift; sourceTree = ""; }; - 08A2E4962D1C07F500102313 /* GetPopUpCommentResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpCommentResponseDTO.swift; sourceTree = ""; }; - 08A2E4982D1C08D600102313 /* GetPopUpCommentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpCommentResponse.swift; sourceTree = ""; }; - 08A2E49C2D1C416800102313 /* CommentListTitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentListTitleSection.swift; sourceTree = ""; }; - 08A2E49E2D1C417000102313 /* CommentListTitleSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentListTitleSectionCell.swift; sourceTree = ""; }; - 08B191342CF366670057BC04 /* UICollectionViewCell+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionViewCell+.swift"; sourceTree = ""; }; - 08B191352CF366670057BC04 /* UICollectionReusableView+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionReusableView+.swift"; sourceTree = ""; }; - 08B191362CF366670057BC04 /* UITableViewCell+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+.swift"; sourceTree = ""; }; - 08B1913A2CF366A00057BC04 /* UIApplication+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+.swift"; sourceTree = ""; }; - 08B1913E2CF367FA0057BC04 /* UIColor+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+.swift"; sourceTree = ""; }; - 08B191402CF367FF0057BC04 /* UIFont+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+.swift"; sourceTree = ""; }; - 08B191422CF41D680057BC04 /* GothicA1-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "GothicA1-Bold.ttf"; sourceTree = ""; }; - 08B191432CF41D680057BC04 /* GothicA1-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "GothicA1-Light.ttf"; sourceTree = ""; }; - 08B191442CF41D680057BC04 /* GothicA1-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "GothicA1-Medium.ttf"; sourceTree = ""; }; - 08B191452CF41D680057BC04 /* GothicA1-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "GothicA1-Regular.ttf"; sourceTree = ""; }; - 08B191462CF41D680057BC04 /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = ""; }; - 08B191472CF41D680057BC04 /* Poppins-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Light.ttf"; sourceTree = ""; }; - 08B191482CF41D680057BC04 /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = ""; }; - 08B191492CF41D680057BC04 /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = ""; }; - 08B191532CF41D6F0057BC04 /* PP_loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PP_loading.json; sourceTree = ""; }; - 08B191542CF41D6F0057BC04 /* PP_splash.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PP_splash.json; sourceTree = ""; }; - 08B191582CF41E610057BC04 /* SignUpMainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpMainController.swift; sourceTree = ""; }; - 08B1915A2CF41E690057BC04 /* SignUpMainReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpMainReactor.swift; sourceTree = ""; }; - 08B1915C2CF41E6F0057BC04 /* SignUpMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpMainView.swift; sourceTree = ""; }; - 08B191602CF430E70057BC04 /* PPProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPProgressView.swift; sourceTree = ""; }; - 08B191622CF430F30057BC04 /* PPProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPProgressIndicator.swift; sourceTree = ""; }; - 08B191662CF432220057BC04 /* PPCancelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPCancelHeaderView.swift; sourceTree = ""; }; - 08B191692CF434B80057BC04 /* SignUpStep1Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep1Controller.swift; sourceTree = ""; }; - 08B1916B2CF434C30057BC04 /* SignUpStep1View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep1View.swift; sourceTree = ""; }; - 08B1916D2CF434CF0057BC04 /* SignUpStep1Reactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep1Reactor.swift; sourceTree = ""; }; - 08B191702CF4398D0057BC04 /* PPLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPLabel.swift; sourceTree = ""; }; - 08B191732CF43DF40057BC04 /* SignUpCheckBoxButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpCheckBoxButton.swift; sourceTree = ""; }; - 08B191752CF440C40057BC04 /* PPButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPButton.swift; sourceTree = ""; }; - 08B191772CF442230057BC04 /* UIImage+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+.swift"; sourceTree = ""; }; - 08B191792CF452B30057BC04 /* SignUpTermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpTermsView.swift; sourceTree = ""; }; - 08B1917C2CF46DE30057BC04 /* TermsDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsDetailController.swift; sourceTree = ""; }; - 08B1917E2CF46DF20057BC04 /* TermsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsDetailView.swift; sourceTree = ""; }; - 08B191812CF48A7B0057BC04 /* SignUpStep2Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep2Controller.swift; sourceTree = ""; }; - 08B191832CF48A820057BC04 /* SignUpStep2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep2View.swift; sourceTree = ""; }; - 08B191852CF48A8B0057BC04 /* SignUpStep2Reactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep2Reactor.swift; sourceTree = ""; }; - 08B191872CF48FAE0057BC04 /* NickNameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NickNameState.swift; sourceTree = ""; }; - 08B1918C2CF49FF70057BC04 /* SignUpStep3View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep3View.swift; sourceTree = ""; }; - 08B1918E2CF4A0020057BC04 /* SignUpStep3Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep3Controller.swift; sourceTree = ""; }; - 08B191902CF4A00E0057BC04 /* SignUpStep3Reactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep3Reactor.swift; sourceTree = ""; }; - 08B191932CF4A0F00057BC04 /* SignUpStep4View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep4View.swift; sourceTree = ""; }; - 08B191952CF4A0FA0057BC04 /* SignUpStep4Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep4Controller.swift; sourceTree = ""; }; - 08B191972CF4A1010057BC04 /* SignUpStep4Reactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpStep4Reactor.swift; sourceTree = ""; }; - 08B1919B2CF4A77C0057BC04 /* TagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSection.swift; sourceTree = ""; }; - 08B1919D2CF4A7830057BC04 /* TagSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSectionCell.swift; sourceTree = ""; }; - 08B1919F2CF4AA0E0057BC04 /* Reactive+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Reactive+.swift"; sourceTree = ""; }; - 08B191A12CF4AE890057BC04 /* ToastMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastMaker.swift; sourceTree = ""; }; - 08B191A32CF5A7030057BC04 /* PPSegmentedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPSegmentedControl.swift; sourceTree = ""; }; - 08B191A62CF5A9430057BC04 /* AgeSelectedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeSelectedButton.swift; sourceTree = ""; }; - 08B191AB2CF5BF9D0057BC04 /* AgeSelectedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeSelectedController.swift; sourceTree = ""; }; - 08B191AD2CF5BFA60057BC04 /* AgeSelectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeSelectedView.swift; sourceTree = ""; }; - 08B191AF2CF5BFAE0057BC04 /* AgeSelectedReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeSelectedReactor.swift; sourceTree = ""; }; - 08B191B12CF5C0A60057BC04 /* PPPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPicker.swift; sourceTree = ""; }; - 08B191B32CF609260057BC04 /* KakaoLoginService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KakaoLoginService.swift; sourceTree = ""; }; - 08B191B52CF6092B0057BC04 /* AppleLoginService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleLoginService.swift; sourceTree = ""; }; - 08B191B72CF6092F0057BC04 /* AuthServiceable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthServiceable.swift; sourceTree = ""; }; - 08B191C12CF615CA0057BC04 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; - 08CBEA022D38989E00248007 /* PostTokenReissueResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTokenReissueResponseDTO.swift; sourceTree = ""; }; - 08CBEA052D38991600248007 /* PostTokenReissueResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTokenReissueResponse.swift; sourceTree = ""; }; - 08CBEA0A2D38DBD600248007 /* LastLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastLoginView.swift; sourceTree = ""; }; - 08CBEA0C2D38ED0D00248007 /* CountButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountButtonView.swift; sourceTree = ""; }; - 08CBEA392D3FABE100248007 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; - 08CBEA3B2D3FABED00248007 /* BookMarkToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMarkToastView.swift; sourceTree = ""; }; - 08CBEA3D2D3FF6A100248007 /* PopUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpCardView.swift; sourceTree = ""; }; - 08DC61F22CF75037002A2F44 /* KeyChainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyChainService.swift; sourceTree = ""; }; - 08DC61F42CF765B5002A2F44 /* UserDefaultService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultService.swift; sourceTree = ""; }; - 08DC61F72CF76843002A2F44 /* SignUpCompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpCompleteView.swift; sourceTree = ""; }; - 08DC61F92CF7684F002A2F44 /* SignUpCompleteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpCompleteController.swift; sourceTree = ""; }; - 08DC61FB2CF76862002A2F44 /* SignUpCompleteReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpCompleteReactor.swift; sourceTree = ""; }; - 08DC62022CF8AC06002A2F44 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 08DC62042CF8AC0E002A2F44 /* HomeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeController.swift; sourceTree = ""; }; - 08DC62062CF8AC14002A2F44 /* HomeReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReactor.swift; sourceTree = ""; }; - 08DC620A2CF8AE0F002A2F44 /* ImageBannerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerSection.swift; sourceTree = ""; }; - 08DC620C2CF8AE16002A2F44 /* ImageBannerSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerSectionCell.swift; sourceTree = ""; }; - 08DC62102CF8B446002A2F44 /* SortedRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortedRequestDTO.swift; sourceTree = ""; }; - 08DC62122CF8B833002A2F44 /* Optional+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+.swift"; sourceTree = ""; }; - 08DE8A0C2D5236840049BCAC /* PutCommentRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PutCommentRequestDTO.swift; sourceTree = ""; }; - 08DE8A0F2D5255110049BCAC /* DetailEmptyCommetSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailEmptyCommetSection.swift; sourceTree = ""; }; - 08DE8A112D5255180049BCAC /* DetailEmptyCommetSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailEmptyCommetSectionCell.swift; sourceTree = ""; }; - 08DE8A162D525A9B0049BCAC /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/LaunchScreen.strings; sourceTree = ""; }; - 08DE8A172D525BA20049BCAC /* Terms.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Terms.plist; sourceTree = ""; }; - 08DE8A1A2D5261DE0049BCAC /* MyPageTermsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageTermsController.swift; sourceTree = ""; }; - 08DE8A1C2D5261E70049BCAC /* MyPageTermsReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageTermsReactor.swift; sourceTree = ""; }; - 08DE8A3E2D54DCC40049BCAC /* MyCommentedPopUpGridSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentedPopUpGridSection.swift; sourceTree = ""; }; - 08DE8A402D54DCCA0049BCAC /* MyCommentedPopUpGridSectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentedPopUpGridSectionCell.swift; sourceTree = ""; }; - 4E643FC02D738D7F0046AF29 /* PopUpStoreRegisterReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpStoreRegisterReactor.swift; sourceTree = ""; }; - 4E643FC22D738D930046AF29 /* PopUpStoreRegisterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpStoreRegisterView.swift; sourceTree = ""; }; - 4E685EAA2D12CEB6001EF91C /* BalloonBackgroundView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalloonBackgroundView.swift; sourceTree = ""; }; - 4E685EAB2D12CEB6001EF91C /* BalloonChipCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalloonChipCell.swift; sourceTree = ""; }; - 4E685EAD2D12CEB6001EF91C /* FilterBottomSheetReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterBottomSheetReactor.swift; sourceTree = ""; }; - 4E685EAE2D12CEB6001EF91C /* FilterBottomSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterBottomSheetView.swift; sourceTree = ""; }; - 4E685EAF2D12CEB6001EF91C /* FilterBottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterBottomSheetViewController.swift; sourceTree = ""; }; - 4E685EB02D12CEB6001EF91C /* FilterCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterCell.swift; sourceTree = ""; }; - 4E685EB12D12CEB6001EF91C /* FilterChip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterChip.swift; sourceTree = ""; }; - 4E685EB22D12CEB6001EF91C /* FilterChipsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterChipsView.swift; sourceTree = ""; }; - 4E685EB62D12CEB6001EF91C /* MapRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapRepository.swift; sourceTree = ""; }; - 4E685EB82D12CEB6001EF91C /* MapUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapUseCase.swift; sourceTree = ""; }; - 4E685EBA2D12CEB6001EF91C /* MapAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapAPIEndpoint.swift; sourceTree = ""; }; - 4E685EBB2D12CEB6001EF91C /* MapPopUpStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapPopUpStore.swift; sourceTree = ""; }; - 4E685EBC2D12CEB6001EF91C /* MapPopUpStoreDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapPopUpStoreDTO.swift; sourceTree = ""; }; - 4E685EBE2D12CEB6001EF91C /* MapPopupCarouselView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapPopupCarouselView.swift; sourceTree = ""; }; - 4E685EBF2D12CEB6001EF91C /* PopupCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupCardCell.swift; sourceTree = ""; }; - 4E685EC12D12CEB6001EF91C /* StoreListCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreListCell.swift; sourceTree = ""; }; - 4E685EC22D12CEB6001EF91C /* StoreListReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreListReactor.swift; sourceTree = ""; }; - 4E685EC32D12CEB6001EF91C /* StoreListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = ""; }; - 4E685EC42D12CEB6001EF91C /* StoreListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreListViewController.swift; sourceTree = ""; }; - 4E685EC62D12CEB6001EF91C /* MapFilterChips.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapFilterChips.swift; sourceTree = ""; }; - 4E685EC72D12CEB6001EF91C /* MapMarker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapMarker.swift; sourceTree = ""; }; - 4E685EC82D12CEB6001EF91C /* MapReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapReactor.swift; sourceTree = ""; }; - 4E685EC92D12CEB6001EF91C /* MapSearchInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapSearchInput.swift; sourceTree = ""; }; - 4E685ECB2D12CEB6001EF91C /* MapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; - 4E685ECC2D12CEB6001EF91C /* MapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; - 4E6A066F2D42A96100B2A658 /* FullScreenMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMapViewController.swift; sourceTree = ""; }; - 4E6C07052D4B6E56008A962A /* RegionDefinitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionDefinitions.swift; sourceTree = ""; }; - 4E6C07072D4B6E74008A962A /* ClusteringModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClusteringModels.swift; sourceTree = ""; }; - 4E6C07092D4B6E81008A962A /* ClusteringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClusteringManager.swift; sourceTree = ""; }; - 4E6CA4842D34D6ED0034D09A /* AdminBottomSheetReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminBottomSheetReactor.swift; sourceTree = ""; }; - 4E755B1C2D2B9AD300ADFB21 /* GetAdminPopUpStoreListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAdminPopUpStoreListResponseDTO.swift; sourceTree = ""; }; - 4E755B1E2D2B9AE500ADFB21 /* AdminAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminAPIEndpoint.swift; sourceTree = ""; }; - 4E755B202D2B9BAB00ADFB21 /* AdminResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminResponseDTO.swift; sourceTree = ""; }; - 4E755B222D2B9C5D00ADFB21 /* AdminViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminViewController.swift; sourceTree = ""; }; - 4E755B242D2B9C6C00ADFB21 /* AdminView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminView.swift; sourceTree = ""; }; - 4E755B262D2B9C7C00ADFB21 /* AdminStoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminStoreCell.swift; sourceTree = ""; }; - 4E755B282D2BA65A00ADFB21 /* AdminReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminReactor.swift; sourceTree = ""; }; - 4E755B2A2D2BA76E00ADFB21 /* AdminUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminUseCase.swift; sourceTree = ""; }; - 4E755B2E2D2BA7FB00ADFB21 /* AdminRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminRepository.swift; sourceTree = ""; }; - 4E8AA29C2D59A2340029DF75 /* MarkerTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerTooltipView.swift; sourceTree = ""; }; - 4E9790C42D40E13500210499 /* MapGuideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGuideViewController.swift; sourceTree = ""; }; - 4E9A465F2D55D1270010578A /* MapUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUtilities.swift; sourceTree = ""; }; - 4E9C12772D2BC7A0006744D6 /* AdminBottomSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminBottomSheetView.swift; sourceTree = ""; }; - 4E9C12792D2BC811006744D6 /* AdminBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminBottomSheetViewController.swift; sourceTree = ""; }; - 4E9C12802D2BE0A6006744D6 /* PopUpStoreRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpStoreRegisterViewController.swift; sourceTree = ""; }; - 4EA2C93C2D424D3300F4D97C /* MapDirectionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDirectionRepository.swift; sourceTree = ""; }; - 4EA2C93E2D424D7400F4D97C /* MapDirectionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDirectionUseCase.swift; sourceTree = ""; }; - 4EA2C9402D424D8400F4D97C /* GetPopUpDirectionResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPopUpDirectionResponseDTO.swift; sourceTree = ""; }; - 4EA2C9422D424DF900F4D97C /* FindDirectionEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindDirectionEndPoint.swift; sourceTree = ""; }; - 4EA998992D21C2FC009DC30B /* StoreListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListSection.swift; sourceTree = ""; }; - 4EAB809C2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GMSMapViewDelegateProxy.swift; sourceTree = ""; }; - 4EAB809E2D3F8EF50041AF30 /* ViewportBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewportBounds.swift; sourceTree = ""; }; - 4EDDEFB32D2D285900CFAFA5 /* DateTimePickerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimePickerManager.swift; sourceTree = ""; }; - 4EDE57022D5E70650014D924 /* LocationPermissionBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionBottomSheet.swift; sourceTree = ""; }; - 4EE5A3D22D40E4A600A2469A /* MapGuideReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGuideReactor.swift; sourceTree = ""; }; - 4EEA1D8E2D352012003E7DE9 /* ImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; - 4EEA1D902D352027003E7DE9 /* ExtendedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedImage.swift; sourceTree = ""; }; - 4EED9BAB2D22730400B288E7 /* FilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterType.swift; sourceTree = ""; }; - BD226D502CF6DB290038C984 /* PPReturnHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPReturnHeaderView.swift; sourceTree = ""; }; - BD91034E2CF6149D00BBCCAE /* AuthAPIEndPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIEndPoint.swift; sourceTree = ""; }; - BD91034F2CF6149D00BBCCAE /* LoginResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginResponseDTO.swift; sourceTree = ""; }; - BD9103512CF6149D00BBCCAE /* BannerPopUpStoreDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannerPopUpStoreDTO.swift; sourceTree = ""; }; - BD9103522CF6149D00BBCCAE /* GetHomeInfoResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetHomeInfoResponseDTO.swift; sourceTree = ""; }; - BD9103532CF6149D00BBCCAE /* HomeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeAPIEndpoint.swift; sourceTree = ""; }; - BD9103542CF6149D00BBCCAE /* PopUpStoreResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopUpStoreResponseDTO.swift; sourceTree = ""; }; - BD9103572CF6149D00BBCCAE /* AuthAPIRepositoryImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIRepositoryImpl.swift; sourceTree = ""; }; - BD9103582CF6149D00BBCCAE /* HomeAPIRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeAPIRepository.swift; sourceTree = ""; }; - BD9103592CF6149D00BBCCAE /* SignUpRepositoryImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpRepositoryImpl.swift; sourceTree = ""; }; - BD91035B2CF6149D00BBCCAE /* CheckNickNameRequestDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckNickNameRequestDTO.swift; sourceTree = ""; }; - BD91035C2CF6149D00BBCCAE /* GetCategoryListResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetCategoryListResponseDTO.swift; sourceTree = ""; }; - BD91035D2CF6149D00BBCCAE /* SignUpAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpAPIEndpoint.swift; sourceTree = ""; }; - BD91035E2CF6149D00BBCCAE /* SignUpRequestDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpRequestDTO.swift; sourceTree = ""; }; - BD9103702CF614A900BBCCAE /* HomeAPIUseCaseImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeAPIUseCaseImpl.swift; sourceTree = ""; }; - BD9103722CF614A900BBCCAE /* SignUpAPIUseCaseImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpAPIUseCaseImpl.swift; sourceTree = ""; }; - BD9103742CF614A900BBCCAE /* AuthAPIUseCaseImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIUseCaseImpl.swift; sourceTree = ""; }; - BD9103762CF614A900BBCCAE /* BannerPopUpStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannerPopUpStore.swift; sourceTree = ""; }; - BD9103772CF614A900BBCCAE /* Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = ""; }; - BD9103782CF614A900BBCCAE /* GetHomeInfoResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetHomeInfoResponse.swift; sourceTree = ""; }; - BD9103792CF614A900BBCCAE /* PopUpStoreResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopUpStoreResponse.swift; sourceTree = ""; }; - BD91037A2CF614A900BBCCAE /* LoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginResponse.swift; sourceTree = ""; }; - BD91037C2CF614A900BBCCAE /* AuthRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthRepository.swift; sourceTree = ""; }; - BD91038E2CF6166800BBCCAE /* SplashView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; - BD9103902CF6166800BBCCAE /* SplashController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashController.swift; sourceTree = ""; }; - BDCA41BD2CF35AC0005EECF6 /* Poppool.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Poppool.app; sourceTree = BUILT_PRODUCTS_DIR; }; - BDCA41C02CF35AC0005EECF6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - BDCA41C22CF35AC0005EECF6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - BDCA41C42CF35AC0005EECF6 /* TestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; }; - BDCA41C92CF35AC1005EECF6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - BDCA41CC2CF35AC1005EECF6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - BDCA41CE2CF35AC1005EECF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - BDCA41D32CF35AC1005EECF6 /* PoppoolTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PoppoolTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - BDCA41D72CF35AC1005EECF6 /* PoppoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppoolTests.swift; sourceTree = ""; }; - BDCA41DD2CF35AC1005EECF6 /* PoppoolUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PoppoolUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - BDCA41E12CF35AC1005EECF6 /* PoppoolUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppoolUITests.swift; sourceTree = ""; }; - BDCA41E32CF35AC1005EECF6 /* PoppoolUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppoolUITestsLaunchTests.swift; sourceTree = ""; }; - BDE30CE02CF87A9700C21E08 /* Poppool.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Poppool.entitlements; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - BDCA41BA2CF35AC0005EECF6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - BDCA41F82CF35D9A005EECF6 /* RxSwift in Frameworks */, - BDCA420D2CF35FD2005EECF6 /* RxGesture in Frameworks */, - BDCA41F52CF35D33005EECF6 /* Kingfisher in Frameworks */, - BDCA42072CF35FA6005EECF6 /* Tabman in Frameworks */, - BDCA42042CF35F76005EECF6 /* PanModal in Frameworks */, - 082197A12D426DCB0054094A /* Then in Frameworks */, - 08B191BA2CF609AE0057BC04 /* RxKakaoSDK in Frameworks */, - 08B191BC2CF609AE0057BC04 /* RxKakaoSDKAuth in Frameworks */, - 083A25D02CF364B70099B58E /* Alamofire in Frameworks */, - 088DE2472D12DB5C0030FA9E /* GoogleMaps in Frameworks */, - BDCA42102CF35FF5005EECF6 /* Lottie in Frameworks */, - BDCA41FE2CF35EE7005EECF6 /* ReactorKit in Frameworks */, - BDCA41F22CF35D0D005EECF6 /* SnapKit in Frameworks */, - BDCA420A2CF35FB1005EECF6 /* Pageboy in Frameworks */, - 08B191BE2CF609AE0057BC04 /* RxKakaoSDKUser in Frameworks */, - 4E5825672D1951DF00EE83EF /* FloatingPanel in Frameworks */, - 4EA9989D2D21C404009DC30B /* RxDataSources in Frameworks */, - BDCA42012CF35EFE005EECF6 /* RxKeyboard in Frameworks */, - 088DE2442D104EE70030FA9E /* SwiftSoup in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BDCA41D02CF35AC1005EECF6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BDCA41DA2CF35AC1005EECF6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 0818988C2D295DAD0067BF01 /* MyPageLogoutSection */ = { - isa = PBXGroup; - children = ( - 0818988D2D295DC30067BF01 /* MyPageLogoutSection.swift */, - 0818988F2D295DC80067BF01 /* MyPageLogoutSectionCell.swift */, - ); - path = MyPageLogoutSection; - sourceTree = ""; - }; - 081898912D2962E30067BF01 /* Main */ = { - isa = PBXGroup; - children = ( - 086F89FA2D23B84900CA4FC9 /* View */, - 086F89F22D2269DE00CA4FC9 /* MyPageController.swift */, - 086F89F42D2269E300CA4FC9 /* MyPageReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 081898922D2965B10067BF01 /* ProfileEdit */ = { - isa = PBXGroup; - children = ( - 081898C82D30D5AC0067BF01 /* CategoryEditModal */, - 081898C72D30D5A10067BF01 /* InfoEditModal */, - 081898C62D30D5940067BF01 /* Main */, - ); - path = ProfileEdit; - sourceTree = ""; - }; - 081898992D2BAA200067BF01 /* Withdrawl */ = { - isa = PBXGroup; - children = ( - 081898B12D2D20C50067BF01 /* Complete */, - 081898A12D2CBFF30067BF01 /* SelectedReason */, - 0818989A2D2BAA400067BF01 /* CheckModal */, - ); - path = Withdrawl; - sourceTree = ""; - }; - 0818989A2D2BAA400067BF01 /* CheckModal */ = { - isa = PBXGroup; - children = ( - 0818989B2D2BAA570067BF01 /* WithdrawlCheckModalView.swift */, - 0818989D2D2BAA610067BF01 /* WithdrawlCheckModalController.swift */, - 0818989F2D2BAA670067BF01 /* WithdrawlCheckModalReactor.swift */, - ); - path = CheckModal; - sourceTree = ""; - }; - 081898A12D2CBFF30067BF01 /* SelectedReason */ = { - isa = PBXGroup; - children = ( - 081898A82D2CEA210067BF01 /* View */, - 081898A42D2CC0180067BF01 /* WithdrawlReasonController.swift */, - 081898A62D2CC01D0067BF01 /* WithdrawlReasonReactor.swift */, - ); - path = SelectedReason; - sourceTree = ""; - }; - 081898A82D2CEA210067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081898A22D2CC0110067BF01 /* WithdrawlReasonView.swift */, - 081898A92D2CEA2F0067BF01 /* WithdrawlCheckSectionCell.swift */, - 081898AB2D2CEA940067BF01 /* WithdrawlCheckSection.swift */, - ); - path = View; - sourceTree = ""; - }; - 081898B12D2D20C50067BF01 /* Complete */ = { - isa = PBXGroup; - children = ( - 081898B22D2D20D70067BF01 /* WithdrawlCompleteView.swift */, - 081898B42D2D20E30067BF01 /* WithdrawlCompleteController.swift */, - ); - path = Complete; - sourceTree = ""; - }; - 081898B82D2E5F420067BF01 /* MyComment */ = { - isa = PBXGroup; - children = ( - 081898EA2D33A3870067BF01 /* SortedModal */, - 081898E92D33A3800067BF01 /* Main */, - ); - path = MyComment; - sourceTree = ""; - }; - 081898C12D2FBD170067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081898952D2965C90067BF01 /* ProfileEditView.swift */, - 081898BF2D2FBD130067BF01 /* ProfileEditListButton.swift */, - ); - path = View; - sourceTree = ""; - }; - 081898C62D30D5940067BF01 /* Main */ = { - isa = PBXGroup; - children = ( - 081898C12D2FBD170067BF01 /* View */, - 081898932D2965C20067BF01 /* ProfileEditController.swift */, - 081898972D2965D20067BF01 /* ProfileEditReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 081898C72D30D5A10067BF01 /* InfoEditModal */ = { - isa = PBXGroup; - children = ( - 081898C92D30D5BA0067BF01 /* InfoEditModalView.swift */, - 081898CB2D30D5BF0067BF01 /* InfoEditModalController.swift */, - 081898CD2D30D5C60067BF01 /* InfoEditModalReactor.swift */, - ); - path = InfoEditModal; - sourceTree = ""; - }; - 081898C82D30D5AC0067BF01 /* CategoryEditModal */ = { - isa = PBXGroup; - children = ( - 081898D12D30F57D0067BF01 /* CategoryEditModalView.swift */, - 081898D32D30F5840067BF01 /* CategoryEditModalController.swift */, - 081898D52D30F58A0067BF01 /* CategoryEditModalReactor.swift */, - ); - path = CategoryEditModal; - sourceTree = ""; - }; - 081898DD2D338E2B0067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 08DE8A3D2D54DCAF0049BCAC /* MyCommentedPopUpGridSection */, - 081898DE2D338F790067BF01 /* ListCountButtonSection */, - 081898B92D2E5F4C0067BF01 /* MyCommentView.swift */, - ); - path = View; - sourceTree = ""; - }; - 081898DE2D338F790067BF01 /* ListCountButtonSection */ = { - isa = PBXGroup; - children = ( - 081898DF2D338F9C0067BF01 /* ListCountButtonSection.swift */, - 081898E12D338FA40067BF01 /* ListCountButtonSectionCell.swift */, - ); - path = ListCountButtonSection; - sourceTree = ""; - }; - 081898E92D33A3800067BF01 /* Main */ = { - isa = PBXGroup; - children = ( - 081898DD2D338E2B0067BF01 /* View */, - 081898BD2D2E5F590067BF01 /* MyCommentController.swift */, - 081898BB2D2E5F510067BF01 /* MyCommentReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 081898EA2D33A3870067BF01 /* SortedModal */ = { - isa = PBXGroup; - children = ( - 081898EB2D33A3960067BF01 /* MyCommentSortedModalView.swift */, - 081898ED2D33A39D0067BF01 /* MyCommentSortedModalController.swift */, - 081898EF2D33A3A30067BF01 /* MyCommentSortedModalReactor.swift */, - ); - path = SortedModal; - sourceTree = ""; - }; - 081898F12D33D6930067BF01 /* Block */ = { - isa = PBXGroup; - children = ( - 081899002D3407DC0067BF01 /* View */, - 081898F42D33D6B10067BF01 /* BlockUserManageController.swift */, - 081898F62D33D6B70067BF01 /* BlockUserManageReactor.swift */, - ); - path = Block; - sourceTree = ""; - }; - 081899002D3407DC0067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081899032D3407F80067BF01 /* BlockUserListSection */, - 081898F22D33D6AC0067BF01 /* BlockUserManageView.swift */, - ); - path = View; - sourceTree = ""; - }; - 081899032D3407F80067BF01 /* BlockUserListSection */ = { - isa = PBXGroup; - children = ( - 081899012D3407F50067BF01 /* BlockUserListSection.swift */, - 081899042D34080B0067BF01 /* BlockUserListSectionCell.swift */, - ); - path = BlockUserListSection; - sourceTree = ""; - }; - 081899062D34B3470067BF01 /* Notice */ = { - isa = PBXGroup; - children = ( - 0818991C2D34DF5B0067BF01 /* Detail */, - 0818991B2D34DF530067BF01 /* List */, - ); - path = Notice; - sourceTree = ""; - }; - 081899152D34D6060067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081899162D34D62F0067BF01 /* NoticeListSection */, - 081899072D34B35A0067BF01 /* MyPageNoticeView.swift */, - ); - path = View; - sourceTree = ""; - }; - 081899162D34D62F0067BF01 /* NoticeListSection */ = { - isa = PBXGroup; - children = ( - 081899172D34D63E0067BF01 /* NoticeListSection.swift */, - 081899192D34D6430067BF01 /* NoticeListSectionCell.swift */, - ); - path = NoticeListSection; - sourceTree = ""; - }; - 0818991B2D34DF530067BF01 /* List */ = { - isa = PBXGroup; - children = ( - 081899152D34D6060067BF01 /* View */, - 081899092D34B3620067BF01 /* MyPageNoticeController.swift */, - 0818990B2D34B3670067BF01 /* MyPageNoticeReactor.swift */, - ); - path = List; - sourceTree = ""; - }; - 0818991C2D34DF5B0067BF01 /* Detail */ = { - isa = PBXGroup; - children = ( - 0818991D2D34DF7D0067BF01 /* MyPageNoticeDetailView.swift */, - 0818991F2D34DF880067BF01 /* MyPageNoticeDetailController.swift */, - 081899212D34DF8E0067BF01 /* MyPageNoticeDetailReactor.swift */, - ); - path = Detail; - sourceTree = ""; - }; - 081899232D3500A90067BF01 /* FAQ */ = { - isa = PBXGroup; - children = ( - 0818992A2D3506100067BF01 /* View */, - 081899262D3500BF0067BF01 /* FAQController.swift */, - 081899282D3500C50067BF01 /* FAQReactor.swift */, - ); - path = FAQ; - sourceTree = ""; - }; - 0818992A2D3506100067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 0818992B2D3506160067BF01 /* FAQDropdownSection */, - 081899242D3500B80067BF01 /* FAQView.swift */, - ); - path = View; - sourceTree = ""; - }; - 0818992B2D3506160067BF01 /* FAQDropdownSection */ = { - isa = PBXGroup; - children = ( - 0818992C2D3506240067BF01 /* FAQDropdownSectionCell.swift */, - 0818992E2D3506290067BF01 /* FAQDropdownSection.swift */, - ); - path = FAQDropdownSection; - sourceTree = ""; - }; - 081899302D35F0C40067BF01 /* Bookmark */ = { - isa = PBXGroup; - children = ( - 0818994E2D363E3D0067BF01 /* ViewTypeModal */, - 0818994D2D363E340067BF01 /* Main */, - ); - path = Bookmark; - sourceTree = ""; - }; - 081899312D35F0D40067BF01 /* Recent */ = { - isa = PBXGroup; - children = ( - 081899422D35FE870067BF01 /* View */, - 0818993A2D35F1250067BF01 /* MyPageRecentController.swift */, - 0818993C2D35F12A0067BF01 /* MyPageRecentReactor.swift */, - ); - path = Recent; - sourceTree = ""; - }; - 081899422D35FE870067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081899432D35FE8D0067BF01 /* RecentPopUpSection */, - 081899382D35F11F0067BF01 /* MyPageRecentView.swift */, - ); - path = View; - sourceTree = ""; - }; - 081899432D35FE8D0067BF01 /* RecentPopUpSection */ = { - isa = PBXGroup; - children = ( - 081899442D35FEA10067BF01 /* RecentPopUpSection.swift */, - ); - path = RecentPopUpSection; - sourceTree = ""; - }; - 081899462D3632030067BF01 /* View */ = { - isa = PBXGroup; - children = ( - 081899472D3632070067BF01 /* PopUpCardSection */, - 08CBEA0C2D38ED0D00248007 /* CountButtonView.swift */, - ); - path = View; - sourceTree = ""; - }; - 081899472D3632070067BF01 /* PopUpCardSection */ = { - isa = PBXGroup; - children = ( - 081899492D36322B0067BF01 /* PopUpCardSection.swift */, - 0818994B2D3632320067BF01 /* PopUpCardSectionCell.swift */, - 08CBEA3D2D3FF6A100248007 /* PopUpCardView.swift */, - ); - path = PopUpCardSection; - sourceTree = ""; - }; - 0818994D2D363E340067BF01 /* Main */ = { - isa = PBXGroup; - children = ( - 081899462D3632030067BF01 /* View */, - 081899322D35F1090067BF01 /* MyPageBookmarkView.swift */, - 081899342D35F10F0067BF01 /* MyPageBookmarkController.swift */, - 081899362D35F1140067BF01 /* MyPageBookmarkReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 0818994E2D363E3D0067BF01 /* ViewTypeModal */ = { - isa = PBXGroup; - children = ( - 0818994F2D363E5C0067BF01 /* BookMarkPopUpViewTypeModalView.swift */, - 081899512D363E640067BF01 /* BookMarkPopUpViewTypeModalController.swift */, - 081899532D363E6A0067BF01 /* BookMarkPopUpViewTypeModalReactor.swift */, - ); - path = ViewTypeModal; - sourceTree = ""; - }; - 082197A42D4E3E9B0054094A /* CommentMyMenu */ = { - isa = PBXGroup; - children = ( - 082197A62D4E3EE00054094A /* CommentMyMenuView.swift */, - 082197A82D4E3EE90054094A /* CommentMyMenuController.swift */, - 082197AA2D4E3EEF0054094A /* CommentMyMenuReactor.swift */, - ); - path = CommentMyMenu; - sourceTree = ""; - }; - 082197A52D4E3EAC0054094A /* CommentInfoMenu */ = { - isa = PBXGroup; - children = ( - 086F89C12D1E345D00CA4FC9 /* CommentUserInfo */, - 082197A42D4E3E9B0054094A /* CommentMyMenu */, - ); - path = CommentInfoMenu; - sourceTree = ""; - }; - 082197AE2D4E4E040054094A /* NormalCommentEdit */ = { - isa = PBXGroup; - children = ( - 082197AF2D4E4E190054094A /* NormalCommentEditView.swift */, - 082197B12D4E4E200054094A /* NormalCommentEditController.swift */, - 082197B32D4E4E280054094A /* NormalCommentEditReactor.swift */, - ); - path = NormalCommentEdit; - sourceTree = ""; - }; - 083A256B2CF361190099B58E /* Application */ = { - isa = PBXGroup; - children = ( - BDCA41C02CF35AC0005EECF6 /* AppDelegate.swift */, - BDCA41C22CF35AC0005EECF6 /* SceneDelegate.swift */, - BDCA41C42CF35AC0005EECF6 /* TestViewController.swift */, - ); - path = Application; - sourceTree = ""; - }; - 083A256C2CF361210099B58E /* Resource */ = { - isa = PBXGroup; - children = ( - 08DE8A132D525A4A0049BCAC /* Strings */, - 08B191C12CF615CA0057BC04 /* Secrets.swift */, - 08B191532CF41D6F0057BC04 /* PP_loading.json */, - 08B191542CF41D6F0057BC04 /* PP_splash.json */, - 08B1914A2CF41D680057BC04 /* Font */, - BDCA41C92CF35AC1005EECF6 /* Assets.xcassets */, - BDCA41CB2CF35AC1005EECF6 /* LaunchScreen.storyboard */, - BDCA41CE2CF35AC1005EECF6 /* Info.plist */, - ); - path = Resource; - sourceTree = ""; - }; - 083A256D2CF3613C0099B58E /* Presentation */ = { - isa = PBXGroup; - children = ( - 4E755B1B2D2B9ABF00ADFB21 /* Admin */, - 4E685ECD2D12CEB6001EF91C /* Map */, - 083A258B2CF361F90099B58E /* Convention */, - 08B1915F2CF430D40057BC04 /* Components */, - 083A25C22CF3635B0099B58E /* Scene */, - 083A259D2CF3620B0099B58E /* Utills */, - 08B191332CF366500057BC04 /* Extension */, - ); - path = Presentation; - sourceTree = ""; - }; - 083A25812CF361EF0099B58E /* Controllers */ = { - isa = PBXGroup; - children = ( - 083A257F2CF361EF0099B58E /* BaseTabmanController.swift */, - 083A25802CF361EF0099B58E /* BaseViewController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 083A258B2CF361F90099B58E /* Convention */ = { - isa = PBXGroup; - children = ( - 083A25842CF361F90099B58E /* ControllerConvention.swift */, - 083A25852CF361F90099B58E /* ConventionCollectionViewCell.swift */, - 083A25862CF361F90099B58E /* ConventionTableViewCell.swift */, - 083A25872CF361F90099B58E /* ReactorConvention.swift */, - 083A25882CF361F90099B58E /* TestDynamicCell.swift */, - 083A25892CF361F90099B58E /* TestDynamicSection.swift */, - 083A258A2CF361F90099B58E /* ViewConvention.swift */, - ); - path = Convention; - sourceTree = ""; - }; - 083A25962CF362090099B58E /* Sectionable */ = { - isa = PBXGroup; - children = ( - 083A25932CF362090099B58E /* Sectionable.swift */, - 083A25942CF362090099B58E /* SectionDecorationItem.swift */, - 083A25952CF362090099B58E /* SectionSupplementaryItem.swift */, - ); - path = Sectionable; - sourceTree = ""; - }; - 083A25982CF362090099B58E /* Interfaces */ = { - isa = PBXGroup; - children = ( - 083A25962CF362090099B58E /* Sectionable */, - 083A25972CF362090099B58E /* InOutputable.swift */, - ); - path = Interfaces; - sourceTree = ""; - }; - 083A259D2CF3620B0099B58E /* Utills */ = { - isa = PBXGroup; - children = ( - 083A25982CF362090099B58E /* Interfaces */, - 083A25812CF361EF0099B58E /* Controllers */, - 08CBEA382D3FABD300248007 /* ToastMaker */, - ); - path = Utills; - sourceTree = ""; - }; - 083A259E2CF362310099B58E /* Domain */ = { - isa = PBXGroup; - children = ( - BD91037B2CF614A900BBCCAE /* Entities */, - BD91037F2CF614A900BBCCAE /* Repository */, - BD9103752CF614A900BBCCAE /* UseCase */, - ); - path = Domain; - sourceTree = ""; - }; - 083A259F2CF362360099B58E /* Data */ = { - isa = PBXGroup; - children = ( - BD91035A2CF6149D00BBCCAE /* Repository */, - BD9103602CF6149D00BBCCAE /* Network */, - ); - path = Data; - sourceTree = ""; - }; - 083A25A02CF3623C0099B58E /* Infrastructure */ = { - isa = PBXGroup; - children = ( - 0841BA832CF9F61500049E31 /* PreSignedService */, - 083A25B12CF362670099B58E /* NetworkLayer */, - 08DC61F42CF765B5002A2F44 /* UserDefaultService.swift */, - 08DC61F22CF75037002A2F44 /* KeyChainService.swift */, - 08B191B72CF6092F0057BC04 /* AuthServiceable.swift */, - 08B191B52CF6092B0057BC04 /* AppleLoginService.swift */, - 08B191B32CF609260057BC04 /* KakaoLoginService.swift */, - 083A25BE2CF362770099B58E /* Logger */, - ); - path = Infrastructure; - sourceTree = ""; - }; - 083A25A42CF362670099B58E /* Common */ = { - isa = PBXGroup; - children = ( - 083A25A12CF362670099B58E /* NetworkError.swift */, - 083A25A22CF362670099B58E /* Requestable.swift */, - 083A25A32CF362670099B58E /* Responsable.swift */, - ); - path = Common; - sourceTree = ""; - }; - 083A25A82CF362670099B58E /* EndPoint */ = { - isa = PBXGroup; - children = ( - 083A25A52CF362670099B58E /* Endpoint.swift */, - 083A25A62CF362670099B58E /* MultipartEndPoint.swift */, - 083A25A72CF362670099B58E /* RequestEndpoint.swift */, - ); - path = EndPoint; - sourceTree = ""; - }; - 083A25AA2CF362670099B58E /* IndicatorMaker */ = { - isa = PBXGroup; - children = ( - 083A25A92CF362670099B58E /* IndicatorMaker.swift */, - ); - path = IndicatorMaker; - sourceTree = ""; - }; - 083A25AD2CF362670099B58E /* Interceptor */ = { - isa = PBXGroup; - children = ( - 083A25AB2CF362670099B58E /* FormDataInterceptor.swift */, - 083A25AC2CF362670099B58E /* TokenInterceptor.swift */, - ); - path = Interceptor; - sourceTree = ""; - }; - 083A25B02CF362670099B58E /* Provider */ = { - isa = PBXGroup; - children = ( - 083A25AE2CF362670099B58E /* Provider.swift */, - 083A25AF2CF362670099B58E /* ProviderImpl.swift */, - ); - path = Provider; - sourceTree = ""; - }; - 083A25B12CF362670099B58E /* NetworkLayer */ = { - isa = PBXGroup; - children = ( - 083A25A42CF362670099B58E /* Common */, - 083A25A82CF362670099B58E /* EndPoint */, - 083A25AA2CF362670099B58E /* IndicatorMaker */, - 083A25AD2CF362670099B58E /* Interceptor */, - 083A25B02CF362670099B58E /* Provider */, - ); - path = NetworkLayer; - sourceTree = ""; - }; - 083A25BE2CF362770099B58E /* Logger */ = { - isa = PBXGroup; - children = ( - 083A25BD2CF362770099B58E /* Logger.swift */, - ); - path = Logger; - sourceTree = ""; - }; - 083A25C22CF3635B0099B58E /* Scene */ = { - isa = PBXGroup; - children = ( - 086F89EF2D2269CD00CA4FC9 /* MyPage */, - 08A2E4772D1B069300102313 /* ImageDetail */, - 0841BABF2CFB5EC700049E31 /* TabbarController */, - 08DC62012CF8ABDA002A2F44 /* Home */, - 086DD9282D00869D00B97D3B /* Search */, - 083A25C42CF363A10099B58E /* SignUp */, - 083A25C32CF363650099B58E /* Login */, - BD9103912CF6166800BBCCAE /* Splash */, - 083C86092D073A08003F441C /* Detail */, - 083C86332D0C7EDB003F441C /* Comment */, - ); - path = Scene; - sourceTree = ""; - }; - 083A25C32CF363650099B58E /* Login */ = { - isa = PBXGroup; - children = ( - 086F89E72D2009CF00CA4FC9 /* Main */, - 086F89E82D2009D700CA4FC9 /* Sub */, - 08CBEA0A2D38DBD600248007 /* LastLoginView.swift */, - ); - path = Login; - sourceTree = ""; - }; - 083A25C42CF363A10099B58E /* SignUp */ = { - isa = PBXGroup; - children = ( - 08B1917B2CF46DD40057BC04 /* TermsDetail */, - 08B191572CF41E550057BC04 /* Main */, - 08B191682CF434A10057BC04 /* Step1 */, - 08B191802CF48A5C0057BC04 /* Step2 */, - 08B191892CF49FDE0057BC04 /* Step3 */, - 08B191922CF4A0E00057BC04 /* Step4 */, - 08DC61F62CF76831002A2F44 /* SignUpComplete */, - ); - path = SignUp; - sourceTree = ""; - }; - 083C86092D073A08003F441C /* Detail */ = { - isa = PBXGroup; - children = ( - 083C86212D087A14003F441C /* View */, - 083C860C2D073A1C003F441C /* DetailController.swift */, - 083C860E2D073A23003F441C /* DetailReactor.swift */, - ); - path = Detail; - sourceTree = ""; - }; - 083C86192D087316003F441C /* RequestDTO */ = { - isa = PBXGroup; - children = ( - 0899524E2D033B5A0022AEF9 /* GetSearchPopUpListRequestDTO.swift */, - 083C861B2D087337003F441C /* GetPopUpDetailRequestDTO.swift */, - 08A2E4942D1C078300102313 /* GetPopUpCommentRequestDTO.swift */, - ); - path = RequestDTO; - sourceTree = ""; - }; - 083C861A2D08731C003F441C /* ResponseDTO */ = { - isa = PBXGroup; - children = ( - 0899526B2D0473EC0022AEF9 /* GetSearchPopUpListResponseDTO.swift */, - 0899524A2D033A9C0022AEF9 /* GetClosePopUpListResponseDTO.swift */, - 0899524C2D033AA70022AEF9 /* GetOpenPopUpListResponseDTO.swift */, - 083C861D2D08737F003F441C /* GetPopUpDetailResponseDTO.swift */, - 08A2E4962D1C07F500102313 /* GetPopUpCommentResponseDTO.swift */, - ); - path = ResponseDTO; - sourceTree = ""; - }; - 083C86212D087A14003F441C /* View */ = { - isa = PBXGroup; - children = ( - 08DE8A0E2D5255000049BCAC /* DetailEmptyCommetSection */, - 088DE25B2D145E1F0030FA9E /* DetailSimilarSection */, - 088DE2522D144A6E0030FA9E /* DetailCommentSection */, - 088DE24D2D13018C0030FA9E /* DetailCommentTitleSection */, - 088DE2482D12F3250030FA9E /* DetailInfoSection */, - 083C86272D088069003F441C /* DetailContentSection */, - 083C86222D087A37003F441C /* DetailTitleSection */, - 083C860A2D073A15003F441C /* DetailView.swift */, - ); - path = View; - sourceTree = ""; - }; - 083C86222D087A37003F441C /* DetailTitleSection */ = { - isa = PBXGroup; - children = ( - 083C86232D087A44003F441C /* DetailTitleSection.swift */, - 083C86252D087A4E003F441C /* DetailTitleSectionCell.swift */, - ); - path = DetailTitleSection; - sourceTree = ""; - }; - 083C86272D088069003F441C /* DetailContentSection */ = { - isa = PBXGroup; - children = ( - 083C86282D088080003F441C /* DetailContentSection.swift */, - 083C862A2D08808C003F441C /* DetailContentSectionCell.swift */, - ); - path = DetailContentSection; - sourceTree = ""; - }; - 083C86332D0C7EDB003F441C /* Comment */ = { - isa = PBXGroup; - children = ( - 082197AE2D4E4E040054094A /* NormalCommentEdit */, - 082197A52D4E3EAC0054094A /* CommentInfoMenu */, - 086F89D12D1E6D9100CA4FC9 /* OtherUserComment */, - 086F89C82D1E429400CA4FC9 /* CommentUserBlock */, - 08A2E48D2D1BF6D900102313 /* CommentList */, - 08A2E47E2D1BCDB800102313 /* CommentDetail */, - 083C86572D0DEFB1003F441C /* CommentCheck */, - 083C86342D0C7EE3003F441C /* CommentSelected */, - 083C863B2D0C8BAF003F441C /* NormalCommentAdd */, - 083C865E2D0EC47B003F441C /* InstaComment */, - ); - path = Comment; - sourceTree = ""; - }; - 083C86342D0C7EE3003F441C /* CommentSelected */ = { - isa = PBXGroup; - children = ( - 083C86372D0C7EFC003F441C /* CommentSelectedView.swift */, - 083C86352D0C7EF4003F441C /* CommentSelectedController.swift */, - 083C86392D0C7F0A003F441C /* CommentSelectedReactor.swift */, - ); - path = CommentSelected; - sourceTree = ""; - }; - 083C863B2D0C8BAF003F441C /* NormalCommentAdd */ = { - isa = PBXGroup; - children = ( - 083C86422D0DCD9B003F441C /* View */, - 083C863C2D0C8BC4003F441C /* NormalCommentAddController.swift */, - 083C86402D0C8BD8003F441C /* NormalCommentAddReactor.swift */, - ); - path = NormalCommentAdd; - sourceTree = ""; - }; - 083C86422D0DCD9B003F441C /* View */ = { - isa = PBXGroup; - children = ( - 083C864D2D0DD317003F441C /* AddCommentImageSection */, - 083C86432D0DCDD5003F441C /* AddCommentTitleSection */, - 083C86482D0DCF7A003F441C /* AddCommentDescriptionSection */, - 083C86522D0DD7D1003F441C /* AddCommentSection */, - 083C863E2D0C8BCE003F441C /* NormalCommentAddView.swift */, - ); - path = View; - sourceTree = ""; - }; - 083C86432D0DCDD5003F441C /* AddCommentTitleSection */ = { - isa = PBXGroup; - children = ( - 083C86442D0DCDE8003F441C /* AddCommentTitleSectionCell.swift */, - 083C86462D0DCDFB003F441C /* AddCommentTitleSection.swift */, - ); - path = AddCommentTitleSection; - sourceTree = ""; - }; - 083C86482D0DCF7A003F441C /* AddCommentDescriptionSection */ = { - isa = PBXGroup; - children = ( - 083C86492D0DCF96003F441C /* AddCommentDescriptionSection.swift */, - 083C864B2D0DCF9B003F441C /* AddCommentDescriptionSectionCell.swift */, - ); - path = AddCommentDescriptionSection; - sourceTree = ""; - }; - 083C864D2D0DD317003F441C /* AddCommentImageSection */ = { - isa = PBXGroup; - children = ( - 083C864E2D0DD3A6003F441C /* AddCommentImageSection.swift */, - 083C86502D0DD3AB003F441C /* AddCommentImageSectionCell.swift */, - ); - path = AddCommentImageSection; - sourceTree = ""; - }; - 083C86522D0DD7D1003F441C /* AddCommentSection */ = { - isa = PBXGroup; - children = ( - 083C86532D0DD7E9003F441C /* AddCommentSection.swift */, - 083C86552D0DD7EE003F441C /* AddCommentSectionCell.swift */, - ); - path = AddCommentSection; - sourceTree = ""; - }; - 083C86572D0DEFB1003F441C /* CommentCheck */ = { - isa = PBXGroup; - children = ( - 083C86582D0DEFC3003F441C /* CommentCheckView.swift */, - 083C865A2D0DEFCF003F441C /* CommentCheckController.swift */, - 083C865C2D0DEFD5003F441C /* CommentCheckReactor.swift */, - ); - path = CommentCheck; - sourceTree = ""; - }; - 083C865E2D0EC47B003F441C /* InstaComment */ = { - isa = PBXGroup; - children = ( - 083C86652D0ECB23003F441C /* View */, - 083C86612D0EC49E003F441C /* InstaCommentAddController.swift */, - 083C86632D0EC4A5003F441C /* InstaCommentAddReactor.swift */, - ); - path = InstaComment; - sourceTree = ""; - }; - 083C86652D0ECB23003F441C /* View */ = { - isa = PBXGroup; - children = ( - 083C86662D0ECB2B003F441C /* InstaGuideSection */, - 083C865F2D0EC496003F441C /* InstaCommentAddView.swift */, - ); - path = View; - sourceTree = ""; - }; - 083C86662D0ECB2B003F441C /* InstaGuideSection */ = { - isa = PBXGroup; - children = ( - 083C866C2D0ECB7B003F441C /* InstaGuideChildSection */, - 083C86672D0ECB3C003F441C /* InstaGuideSection */, - ); - path = InstaGuideSection; - sourceTree = ""; - }; - 083C86672D0ECB3C003F441C /* InstaGuideSection */ = { - isa = PBXGroup; - children = ( - 083C86682D0ECB47003F441C /* InstaGuideSection.swift */, - 083C866A2D0ECB4F003F441C /* InstaGuideSectionCell.swift */, - ); - path = InstaGuideSection; - sourceTree = ""; - }; - 083C866C2D0ECB7B003F441C /* InstaGuideChildSection */ = { - isa = PBXGroup; - children = ( - 083C866D2D0ECB87003F441C /* InstaGuideChildSection.swift */, - 083C866F2D0ECB8E003F441C /* InstaGuideChildSectionCell.swift */, - ); - path = InstaGuideChildSection; - sourceTree = ""; - }; - 083C86712D0EE2A3003F441C /* CommentAPI */ = { - isa = PBXGroup; - children = ( - 083C86722D0EE2B1003F441C /* CommentAPIEndPoint.swift */, - 083C86742D0EE2B8003F441C /* RequestDTO */, - ); - path = CommentAPI; - sourceTree = ""; - }; - 083C86742D0EE2B8003F441C /* RequestDTO */ = { - isa = PBXGroup; - children = ( - 083C86752D0EE2CF003F441C /* PostCommentRequestDTO.swift */, - 082197AC2D4E49370054094A /* DeleteCommentRequestDTO.swift */, - 08DE8A0C2D5236840049BCAC /* PutCommentRequestDTO.swift */, - ); - path = RequestDTO; - sourceTree = ""; - }; - 0841BA832CF9F61500049E31 /* PreSignedService */ = { - isa = PBXGroup; - children = ( - 0841BA842CF9F62300049E31 /* PreSignedURLDTO.swift */, - 0841BA852CF9F62300049E31 /* PresignedURLRequestDTO.swift */, - 0841BA862CF9F62300049E31 /* PreSignedURLResponseDTO.swift */, - 0841BA812CF9F5DF00049E31 /* PreSignedService.swift */, - 0841BA8B2CF9F67100049E31 /* PreSignedAPIEndPoint.swift */, - ); - path = PreSignedService; - sourceTree = ""; - }; - 0841BA9D2CFA085700049E31 /* ImageBannerSection */ = { - isa = PBXGroup; - children = ( - 08DC620A2CF8AE0F002A2F44 /* ImageBannerSection.swift */, - 08DC620C2CF8AE16002A2F44 /* ImageBannerSectionCell.swift */, - ); - path = ImageBannerSection; - sourceTree = ""; - }; - 0841BA9E2CFA085F00049E31 /* ImageBannerChildSection */ = { - isa = PBXGroup; - children = ( - 0841BA8D2CF9F8A100049E31 /* ImageBannerChildSection.swift */, - 0841BA7F2CF9F34100049E31 /* ImageBannerChildSectionCell.swift */, - ); - path = ImageBannerChildSection; - sourceTree = ""; - }; - 0841BAA12CFA319400049E31 /* SpacingSection */ = { - isa = PBXGroup; - children = ( - 0841BAA22CFA31A300049E31 /* SpacingSection.swift */, - 0841BAA42CFA31A900049E31 /* SpacingSectionCell.swift */, - ); - path = SpacingSection; - sourceTree = ""; - }; - 0841BAA62CFA353500049E31 /* HomeTitleSection */ = { - isa = PBXGroup; - children = ( - 0841BAA72CFA354500049E31 /* HomeTitleSection.swift */, - 0841BAA92CFA354C00049E31 /* HomeTitleSectionCell.swift */, - ); - path = HomeTitleSection; - sourceTree = ""; - }; - 0841BAAD2CFA38CB00049E31 /* HomeCardSection */ = { - isa = PBXGroup; - children = ( - 0841BAAE2CFA38EA00049E31 /* HomeCardSection.swift */, - 0841BAB02CFA38F500049E31 /* HomeCardSectionCell.swift */, - ); - path = HomeCardSection; - sourceTree = ""; - }; - 0841BAB22CFABEA900049E31 /* HomePopularCardSection */ = { - isa = PBXGroup; - children = ( - 0841BAB32CFABED700049E31 /* HomePopularCardSection.swift */, - 0841BAB52CFABEDC00049E31 /* HomePopularCardSectionCell.swift */, - ); - path = HomePopularCardSection; - sourceTree = ""; - }; - 0841BABF2CFB5EC700049E31 /* TabbarController */ = { - isa = PBXGroup; - children = ( - 0841BAC22CFB600800049E31 /* TabbarController.swift */, - ); - path = TabbarController; - sourceTree = ""; - }; - 086DD8C92CFDFE9100B97D3B /* Main */ = { - isa = PBXGroup; - children = ( - 08DC62082CF8ADB7002A2F44 /* View */, - 08DC62042CF8AC0E002A2F44 /* HomeController.swift */, - 08DC62062CF8AC14002A2F44 /* HomeReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 086DD8CA2CFDFE9900B97D3B /* List */ = { - isa = PBXGroup; - children = ( - 086DD8E12CFF355200B97D3B /* View */, - 086DD8CB2CFDFEA800B97D3B /* HomeListController.swift */, - 086DD8CF2CFDFEB900B97D3B /* HomeListReactor.swift */, - 086DD8D22CFDFF1500B97D3B /* HomePopUpType.swift */, - ); - path = List; - sourceTree = ""; - }; - 086DD8D42CFF181500B97D3B /* UserAPI */ = { - isa = PBXGroup; - children = ( - 086DD8D52CFF182100B97D3B /* UserAPIEndPoint.swift */, - 086F89DD2D1E7A7A00CA4FC9 /* ResponseDTO */, - 086F89DC2D1E7A7100CA4FC9 /* RequesetDTO */, - ); - path = UserAPI; - sourceTree = ""; - }; - 086DD8E12CFF355200B97D3B /* View */ = { - isa = PBXGroup; - children = ( - 086DD8CD2CFDFEB000B97D3B /* HomeListView.swift */, - 086DD8E22CFF356300B97D3B /* HomeCardGridSection.swift */, - ); - path = View; - sourceTree = ""; - }; - 086DD9282D00869D00B97D3B /* Search */ = { - isa = PBXGroup; - children = ( - 0899523F2D031E470022AEF9 /* Main */, - 0899525E2D0366A20022AEF9 /* AfterSearch */, - 0899525D2D0366950022AEF9 /* BeforeSearch */, - 089952562D03478D0022AEF9 /* CategoryController */, - 089952402D031E500022AEF9 /* SortedController */, - ); - path = Search; - sourceTree = ""; - }; - 086DD9312D0095DA00B97D3B /* View */ = { - isa = PBXGroup; - children = ( - 086DD93E2D01EED200B97D3B /* SearchCountTitleSection */, - 086DD9372D0099FB00B97D3B /* CancelableTagSection */, - 086DD9322D00960700B97D3B /* SearchTitleSection */, - 086DD92D2D0086B900B97D3B /* SearchView.swift */, - ); - path = View; - sourceTree = ""; - }; - 086DD9322D00960700B97D3B /* SearchTitleSection */ = { - isa = PBXGroup; - children = ( - 086DD9332D00962500B97D3B /* SearchTitleSectionCell.swift */, - 086DD9352D00963900B97D3B /* SearchTitleSection.swift */, - ); - path = SearchTitleSection; - sourceTree = ""; - }; - 086DD9372D0099FB00B97D3B /* CancelableTagSection */ = { - isa = PBXGroup; - children = ( - 086DD93A2D009A1C00B97D3B /* CancelableTagSection.swift */, - 086DD93C2D009A2600B97D3B /* CancelableTagSectionCell.swift */, - ); - path = CancelableTagSection; - sourceTree = ""; - }; - 086DD93E2D01EED200B97D3B /* SearchCountTitleSection */ = { - isa = PBXGroup; - children = ( - 086DD93F2D01EEEB00B97D3B /* SearchCountTitleSection.swift */, - 086DD9412D01EEF700B97D3B /* SearchCountTitleSectionCell.swift */, - ); - path = SearchCountTitleSection; - sourceTree = ""; - }; - 086F89C12D1E345D00CA4FC9 /* CommentUserInfo */ = { - isa = PBXGroup; - children = ( - 086F89C22D1E347700CA4FC9 /* CommentUserInfoView.swift */, - 086F89C42D1E347E00CA4FC9 /* CommentUserInfoController.swift */, - 086F89C62D1E348400CA4FC9 /* CommentUserInfoReactor.swift */, - ); - path = CommentUserInfo; - sourceTree = ""; - }; - 086F89C82D1E429400CA4FC9 /* CommentUserBlock */ = { - isa = PBXGroup; - children = ( - 086F89C92D1E42A700CA4FC9 /* CommentUserBlockView.swift */, - 086F89CB2D1E42B000CA4FC9 /* CommentUserBlockController.swift */, - 086F89CD2D1E42B500CA4FC9 /* CommentUserBlockReactor.swift */, - ); - path = CommentUserBlock; - sourceTree = ""; - }; - 086F89D12D1E6D9100CA4FC9 /* OtherUserComment */ = { - isa = PBXGroup; - children = ( - 086F89E12D1FE8F200CA4FC9 /* View */, - 086F89D42D1E6DB100CA4FC9 /* OtherUserCommentController.swift */, - 086F89D62D1E6DB700CA4FC9 /* OtherUserCommentReactor.swift */, - ); - path = OtherUserComment; - sourceTree = ""; - }; - 086F89DC2D1E7A7100CA4FC9 /* RequesetDTO */ = { - isa = PBXGroup; - children = ( - 086DD8D72CFF185200B97D3B /* PostBookmarkPopUpRequestDTO.swift */, - 08A2E46B2D15BC5000102313 /* CommentLikeRequestDTO.swift */, - 086F89CF2D1E60A100CA4FC9 /* PostUserBlockRequestDTO.swift */, - 086F89D82D1E79E200CA4FC9 /* GetOtherUserCommentListRequestDTO.swift */, - 081898CF2D30EA900067BF01 /* PutUserTailoredInfoRequestDTO.swift */, - 081898D72D310C160067BF01 /* PutUserCategoryRequestDTO.swift */, - 081898D92D32559B0067BF01 /* PutUserProfileRequestDTO.swift */, - 081898E72D3392480067BF01 /* GetMyCommentRequestDTO.swift */, - 081898FE2D33DA440067BF01 /* GetBlockUserListRequestDTO.swift */, - ); - path = RequesetDTO; - sourceTree = ""; - }; - 086F89DD2D1E7A7A00CA4FC9 /* ResponseDTO */ = { - isa = PBXGroup; - children = ( - 086F89DA2D1E7A6C00CA4FC9 /* GetOtherUserCommentedPopUpListResponseDTO.swift */, - 081898C22D30AE2C0067BF01 /* GetMyProfileResponseDTO.swift */, - 086F89F62D226DF600CA4FC9 /* GetMyPageResponseDTO.swift */, - 081898AD2D2CFC230067BF01 /* GetWithdrawlListResponseDTO.swift */, - 081898E32D3391550067BF01 /* GetMyCommentedPopUpResponseDTO.swift */, - 081898FA2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift */, - 0818990D2D34B68C0067BF01 /* GetNoticeListResponseDTO.swift */, - 081899112D34CA9E0067BF01 /* GetNoticeDetailResponseDTO.swift */, - 0818993E2D35FBE00067BF01 /* GetRecentPopUpResponseDTO.swift */, - ); - path = ResponseDTO; - sourceTree = ""; - }; - 086F89DE2D1E7CBE00CA4FC9 /* UserAPI */ = { - isa = PBXGroup; - children = ( - 086F89DF2D1E7CC700CA4FC9 /* GetOtherUserCommentedPopUpListResponse.swift */, - 086F89F82D226EEB00CA4FC9 /* GetMyPageResponse.swift */, - 081898AF2D2CFCA40067BF01 /* GetWithdrawlListResponse.swift */, - 081898C42D30AEF40067BF01 /* GetMyProfileResponse.swift */, - 081898E52D3391CB0067BF01 /* GetMyCommentResponse.swift */, - 081898FC2D33D9ED0067BF01 /* GetBlockUserListResponse.swift */, - 0818990F2D34B7240067BF01 /* GetNoticeListResponse.swift */, - 081899132D34CAEA0067BF01 /* GetNoticeDetailResponse.swift */, - 081899402D35FDA10067BF01 /* GetRecentPopUpResponse.swift */, - ); - name = UserAPI; - sourceTree = ""; - }; - 086F89E12D1FE8F200CA4FC9 /* View */ = { - isa = PBXGroup; - children = ( - 086F89E22D1FE8F800CA4FC9 /* OtherUserCommentSection */, - 086F89D22D1E6DA600CA4FC9 /* OtherUserCommentView.swift */, - ); - path = View; - sourceTree = ""; - }; - 086F89E22D1FE8F800CA4FC9 /* OtherUserCommentSection */ = { - isa = PBXGroup; - children = ( - 086F89E32D1FE91300CA4FC9 /* OtherUserCommentSection.swift */, - 086F89E52D1FE91800CA4FC9 /* OtherUserCommentSectionCell.swift */, - ); - path = OtherUserCommentSection; - sourceTree = ""; - }; - 086F89E72D2009CF00CA4FC9 /* Main */ = { - isa = PBXGroup; - children = ( - 083A25CB2CF363CB0099B58E /* LoginView.swift */, - 083A25C72CF363C00099B58E /* LoginController.swift */, - 083A25C92CF363C60099B58E /* LoginReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 086F89E82D2009D700CA4FC9 /* Sub */ = { - isa = PBXGroup; - children = ( - 086F89E92D2009E300CA4FC9 /* SubLoginView.swift */, - 086F89EB2D2009EB00CA4FC9 /* SubLoginController.swift */, - 086F89ED2D2009F100CA4FC9 /* SubLoginReactor.swift */, - ); - path = Sub; - sourceTree = ""; - }; - 086F89EF2D2269CD00CA4FC9 /* MyPage */ = { - isa = PBXGroup; - children = ( - 08DE8A192D5261CD0049BCAC /* Terms */, - 081899312D35F0D40067BF01 /* Recent */, - 081899302D35F0C40067BF01 /* Bookmark */, - 081899232D3500A90067BF01 /* FAQ */, - 081899062D34B3470067BF01 /* Notice */, - 081898F12D33D6930067BF01 /* Block */, - 081898B82D2E5F420067BF01 /* MyComment */, - 081898992D2BAA200067BF01 /* Withdrawl */, - 081898922D2965B10067BF01 /* ProfileEdit */, - 081898912D2962E30067BF01 /* Main */, - ); - path = MyPage; - sourceTree = ""; - }; - 086F89FA2D23B84900CA4FC9 /* View */ = { - isa = PBXGroup; - children = ( - 0818988C2D295DAD0067BF01 /* MyPageLogoutSection */, - 086F8A162D265C5200CA4FC9 /* MyPageListSection */, - 086F8A0D2D26296600CA4FC9 /* MyPageCommentSection */, - 086F8A082D2621DB00CA4FC9 /* MyPageMyCommentTitleSection */, - 086F8A032D23CB2400CA4FC9 /* MyPageProfileSection */, - 086F89F02D2269D800CA4FC9 /* MyPageView.swift */, - ); - path = View; - sourceTree = ""; - }; - 086F8A032D23CB2400CA4FC9 /* MyPageProfileSection */ = { - isa = PBXGroup; - children = ( - 086F8A042D23CB3300CA4FC9 /* MyPageProfileSection.swift */, - 086F8A062D23CB3800CA4FC9 /* MyPageProfileSectionCell.swift */, - ); - path = MyPageProfileSection; - sourceTree = ""; - }; - 086F8A082D2621DB00CA4FC9 /* MyPageMyCommentTitleSection */ = { - isa = PBXGroup; - children = ( - 086F8A092D2621EE00CA4FC9 /* MyPageMyCommentTitleSection.swift */, - 086F8A0B2D2621F400CA4FC9 /* MyPageMyCommentTitleSectionCell.swift */, - ); - path = MyPageMyCommentTitleSection; - sourceTree = ""; - }; - 086F8A0D2D26296600CA4FC9 /* MyPageCommentSection */ = { - isa = PBXGroup; - children = ( - 086F8A0E2D26297900CA4FC9 /* MyPageCommentSection.swift */, - 086F8A102D26297D00CA4FC9 /* MyPageCommentSectionCell.swift */, - ); - path = MyPageCommentSection; - sourceTree = ""; - }; - 086F8A162D265C5200CA4FC9 /* MyPageListSection */ = { - isa = PBXGroup; - children = ( - 086F8A172D265C5F00CA4FC9 /* MyPageListSection.swift */, - 086F8A192D265C6300CA4FC9 /* MyPageListSectionCell.swift */, - ); - path = MyPageListSection; - sourceTree = ""; - }; - 088DE2482D12F3250030FA9E /* DetailInfoSection */ = { - isa = PBXGroup; - children = ( - 088DE2492D12F3360030FA9E /* DetailInfoSection.swift */, - 088DE24B2D12F33B0030FA9E /* DetailInfoSectionCell.swift */, - ); - path = DetailInfoSection; - sourceTree = ""; - }; - 088DE24D2D13018C0030FA9E /* DetailCommentTitleSection */ = { - isa = PBXGroup; - children = ( - 088DE24E2D13019A0030FA9E /* DetailCommentTitleSection.swift */, - 088DE2502D13019E0030FA9E /* DetailCommentTitleSectionCell.swift */, - ); - path = DetailCommentTitleSection; - sourceTree = ""; - }; - 088DE2522D144A6E0030FA9E /* DetailCommentSection */ = { - isa = PBXGroup; - children = ( - 088DE2532D144A7E0030FA9E /* DetailCommentSection.swift */, - 088DE2552D144A830030FA9E /* DetailCommentSectionCell.swift */, - 088DE2572D144B0F0030FA9E /* DetailCommentProfileView.swift */, - 088DE2592D1458620030FA9E /* DetailCommentImageCell.swift */, - ); - path = DetailCommentSection; - sourceTree = ""; - }; - 088DE25B2D145E1F0030FA9E /* DetailSimilarSection */ = { - isa = PBXGroup; - children = ( - 088DE25C2D145E3A0030FA9E /* DetailSimilarSection.swift */, - 088DE25E2D145E3F0030FA9E /* DetailSimilarSectionCell.swift */, - ); - path = DetailSimilarSection; - sourceTree = ""; - }; - 0899523F2D031E470022AEF9 /* Main */ = { - isa = PBXGroup; - children = ( - 089952612D0366D30022AEF9 /* SearchMainView.swift */, - 0899525F2D0366C40022AEF9 /* SearchMainController.swift */, - 089952632D0366DA0022AEF9 /* SearchMainReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 089952402D031E500022AEF9 /* SortedController */ = { - isa = PBXGroup; - children = ( - 089952452D031E740022AEF9 /* SearchSortedView.swift */, - 089952412D031E650022AEF9 /* SearchSortedController.swift */, - 089952432D031E6D0022AEF9 /* SearchSortedReactor.swift */, - ); - path = SortedController; - sourceTree = ""; - }; - 089952472D033A0E0022AEF9 /* PopUpAPI */ = { - isa = PBXGroup; - children = ( - 089952482D033A1C0022AEF9 /* PopUpAPIEndPoint.swift */, - 083C861A2D08731C003F441C /* ResponseDTO */, - 083C86192D087316003F441C /* RequestDTO */, - ); - path = PopUpAPI; - sourceTree = ""; - }; - 089952562D03478D0022AEF9 /* CategoryController */ = { - isa = PBXGroup; - children = ( - 0899525B2D0347BD0022AEF9 /* SearchCategoryView.swift */, - 089952572D0347AC0022AEF9 /* SearchCategoryController.swift */, - 089952592D0347B40022AEF9 /* SearchCategoryReactor.swift */, - ); - path = CategoryController; - sourceTree = ""; - }; - 0899525D2D0366950022AEF9 /* BeforeSearch */ = { - isa = PBXGroup; - children = ( - 086DD9312D0095DA00B97D3B /* View */, - 086DD9292D0086AA00B97D3B /* SearchController.swift */, - 086DD92B2D0086B100B97D3B /* SearchReactor.swift */, - ); - path = BeforeSearch; - sourceTree = ""; - }; - 0899525E2D0366A20022AEF9 /* AfterSearch */ = { - isa = PBXGroup; - children = ( - 089952762D0476590022AEF9 /* View */, - 089952652D046CCD0022AEF9 /* SearchResultController.swift */, - 089952672D046CD80022AEF9 /* SearchResultReactor.swift */, - ); - path = AfterSearch; - sourceTree = ""; - }; - 0899526F2D0474430022AEF9 /* PopUpAPI */ = { - isa = PBXGroup; - children = ( - 089952502D033C410022AEF9 /* GetSearchBottomPopUpListResponse.swift */, - 0899526D2D0474340022AEF9 /* GetSearchPopUpListResponse.swift */, - 083C861F2D087445003F441C /* GetPopUpDetailResponse.swift */, - 08A2E4982D1C08D600102313 /* GetPopUpCommentResponse.swift */, - ); - path = PopUpAPI; - sourceTree = ""; - }; - 089952712D0475D70022AEF9 /* SearchResultCountSection */ = { - isa = PBXGroup; - children = ( - 089952722D0475E90022AEF9 /* SearchResultCountSection.swift */, - 089952742D0475F20022AEF9 /* SearchResultCountSectionCell.swift */, - ); - path = SearchResultCountSection; - sourceTree = ""; - }; - 089952762D0476590022AEF9 /* View */ = { - isa = PBXGroup; - children = ( - 089952712D0475D70022AEF9 /* SearchResultCountSection */, - 089952692D046CDE0022AEF9 /* SearchResultView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08A2E4772D1B069300102313 /* ImageDetail */ = { - isa = PBXGroup; - children = ( - 08A2E4782D1B06A300102313 /* ImageDetailView.swift */, - 08A2E47A2D1B06AA00102313 /* ImageDetailController.swift */, - 08A2E47C2D1B06B000102313 /* ImageDetailReactor.swift */, - ); - path = ImageDetail; - sourceTree = ""; - }; - 08A2E47E2D1BCDB800102313 /* CommentDetail */ = { - isa = PBXGroup; - children = ( - 08A2E4872D1BD86600102313 /* View */, - 08A2E4812D1BCDEA00102313 /* CommentDetailController.swift */, - 08A2E4832D1BCDEF00102313 /* CommentDetailReactor.swift */, - ); - path = CommentDetail; - sourceTree = ""; - }; - 08A2E4872D1BD86600102313 /* View */ = { - isa = PBXGroup; - children = ( - 08A2E4882D1BDA7500102313 /* CommentDetailContentSection */, - 08A2E4852D1BD85C00102313 /* CommentDetailImageSection.swift */, - 08A2E47F2D1BCDE300102313 /* CommentDetailView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08A2E4882D1BDA7500102313 /* CommentDetailContentSection */ = { - isa = PBXGroup; - children = ( - 08A2E4892D1BDA8400102313 /* CommentDetailContentSection.swift */, - 08A2E48B2D1BDA8A00102313 /* CommentDetailContentSectionCell.swift */, - ); - path = CommentDetailContentSection; - sourceTree = ""; - }; - 08A2E48D2D1BF6D900102313 /* CommentList */ = { - isa = PBXGroup; - children = ( - 08A2E49A2D1C414C00102313 /* View */, - 08A2E4902D1BF6EA00102313 /* CommentListController.swift */, - 08A2E4922D1BF6EF00102313 /* CommentListReactor.swift */, - ); - path = CommentList; - sourceTree = ""; - }; - 08A2E49A2D1C414C00102313 /* View */ = { - isa = PBXGroup; - children = ( - 08A2E49B2D1C415500102313 /* CommentListTitleSection */, - 08A2E48E2D1BF6E500102313 /* CommentListView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08A2E49B2D1C415500102313 /* CommentListTitleSection */ = { - isa = PBXGroup; - children = ( - 08A2E49C2D1C416800102313 /* CommentListTitleSection.swift */, - 08A2E49E2D1C417000102313 /* CommentListTitleSectionCell.swift */, - ); - path = CommentListTitleSection; - sourceTree = ""; - }; - 08B191332CF366500057BC04 /* Extension */ = { - isa = PBXGroup; - children = ( - 08B1919F2CF4AA0E0057BC04 /* Reactive+.swift */, - 0841BABB2CFB59E200049E31 /* String?+.swift */, - 08B191402CF367FF0057BC04 /* UIFont+.swift */, - 08B1913E2CF367FA0057BC04 /* UIColor+.swift */, - 08B191352CF366670057BC04 /* UICollectionReusableView+.swift */, - 08B191342CF366670057BC04 /* UICollectionViewCell+.swift */, - 08B191362CF366670057BC04 /* UITableViewCell+.swift */, - 08B1913A2CF366A00057BC04 /* UIApplication+.swift */, - 08B191772CF442230057BC04 /* UIImage+.swift */, - 08DC62122CF8B833002A2F44 /* Optional+.swift */, - 0841BAAB2CFA35F300049E31 /* UILabel+.swift */, - 0841BABD2CFB5AA600049E31 /* Date?+.swift */, - 086DD8C72CFDEA9200B97D3B /* UIView+.swift */, - 086DD8DF2CFF2C3700B97D3B /* UIImageView+.swift */, - 086DD92F2D0090E900B97D3B /* UITextField+.swift */, - 081898B62D2D23A90067BF01 /* UINavigationController+.swift */, - ); - path = Extension; - sourceTree = ""; - }; - 08B1914A2CF41D680057BC04 /* Font */ = { - isa = PBXGroup; - children = ( - 08B191422CF41D680057BC04 /* GothicA1-Bold.ttf */, - 08B191432CF41D680057BC04 /* GothicA1-Light.ttf */, - 08B191442CF41D680057BC04 /* GothicA1-Medium.ttf */, - 08B191452CF41D680057BC04 /* GothicA1-Regular.ttf */, - 08B191462CF41D680057BC04 /* Poppins-Bold.ttf */, - 08B191472CF41D680057BC04 /* Poppins-Light.ttf */, - 08B191482CF41D680057BC04 /* Poppins-Medium.ttf */, - 08B191492CF41D680057BC04 /* Poppins-Regular.ttf */, - ); - path = Font; - sourceTree = ""; - }; - 08B191572CF41E550057BC04 /* Main */ = { - isa = PBXGroup; - children = ( - 08B1915E2CF430C00057BC04 /* View */, - 08B191582CF41E610057BC04 /* SignUpMainController.swift */, - 08B1915A2CF41E690057BC04 /* SignUpMainReactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 08B1915E2CF430C00057BC04 /* View */ = { - isa = PBXGroup; - children = ( - 08B1915C2CF41E6F0057BC04 /* SignUpMainView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08B1915F2CF430D40057BC04 /* Components */ = { - isa = PBXGroup; - children = ( - 08B191B12CF5C0A60057BC04 /* PPPicker.swift */, - 08B191702CF4398D0057BC04 /* PPLabel.swift */, - 08B191752CF440C40057BC04 /* PPButton.swift */, - 08B191662CF432220057BC04 /* PPCancelHeaderView.swift */, - BD226D502CF6DB290038C984 /* PPReturnHeaderView.swift */, - 08B191A32CF5A7030057BC04 /* PPSegmentedControl.swift */, - 08B191642CF430F80057BC04 /* PPProgressIndicator */, - ); - path = Components; - sourceTree = ""; - }; - 08B191642CF430F80057BC04 /* PPProgressIndicator */ = { - isa = PBXGroup; - children = ( - 08B191602CF430E70057BC04 /* PPProgressView.swift */, - 08B191622CF430F30057BC04 /* PPProgressIndicator.swift */, - ); - path = PPProgressIndicator; - sourceTree = ""; - }; - 08B191682CF434A10057BC04 /* Step1 */ = { - isa = PBXGroup; - children = ( - 08B191722CF43D900057BC04 /* View */, - 08B191692CF434B80057BC04 /* SignUpStep1Controller.swift */, - 08B1916D2CF434CF0057BC04 /* SignUpStep1Reactor.swift */, - ); - path = Step1; - sourceTree = ""; - }; - 08B191722CF43D900057BC04 /* View */ = { - isa = PBXGroup; - children = ( - 08B1916B2CF434C30057BC04 /* SignUpStep1View.swift */, - 08B191732CF43DF40057BC04 /* SignUpCheckBoxButton.swift */, - 08B191792CF452B30057BC04 /* SignUpTermsView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08B1917B2CF46DD40057BC04 /* TermsDetail */ = { - isa = PBXGroup; - children = ( - 08B1917E2CF46DF20057BC04 /* TermsDetailView.swift */, - 08B1917C2CF46DE30057BC04 /* TermsDetailController.swift */, - ); - path = TermsDetail; - sourceTree = ""; - }; - 08B191802CF48A5C0057BC04 /* Step2 */ = { - isa = PBXGroup; - children = ( - 08B191832CF48A820057BC04 /* SignUpStep2View.swift */, - 08B191812CF48A7B0057BC04 /* SignUpStep2Controller.swift */, - 08B191852CF48A8B0057BC04 /* SignUpStep2Reactor.swift */, - 08B191872CF48FAE0057BC04 /* NickNameState.swift */, - 081898DB2D326DC10067BF01 /* IntroState.swift */, - ); - path = Step2; - sourceTree = ""; - }; - 08B191892CF49FDE0057BC04 /* Step3 */ = { - isa = PBXGroup; - children = ( - 08B191992CF4A63E0057BC04 /* View */, - 08B1918E2CF4A0020057BC04 /* SignUpStep3Controller.swift */, - 08B191902CF4A00E0057BC04 /* SignUpStep3Reactor.swift */, - ); - path = Step3; - sourceTree = ""; - }; - 08B191922CF4A0E00057BC04 /* Step4 */ = { - isa = PBXGroup; - children = ( - 08B191AA2CF5BF8E0057BC04 /* AgeSelectedModal */, - 08B191A92CF5BF860057BC04 /* Main */, - ); - path = Step4; - sourceTree = ""; - }; - 08B191992CF4A63E0057BC04 /* View */ = { - isa = PBXGroup; - children = ( - 08B1919A2CF4A7700057BC04 /* TagSection */, - 08B1918C2CF49FF70057BC04 /* SignUpStep3View.swift */, - ); - path = View; - sourceTree = ""; - }; - 08B1919A2CF4A7700057BC04 /* TagSection */ = { - isa = PBXGroup; - children = ( - 08B1919B2CF4A77C0057BC04 /* TagSection.swift */, - 08B1919D2CF4A7830057BC04 /* TagSectionCell.swift */, - ); - path = TagSection; - sourceTree = ""; - }; - 08B191A82CF5A94B0057BC04 /* View */ = { - isa = PBXGroup; - children = ( - 08B191932CF4A0F00057BC04 /* SignUpStep4View.swift */, - 08B191A62CF5A9430057BC04 /* AgeSelectedButton.swift */, - ); - path = View; - sourceTree = ""; - }; - 08B191A92CF5BF860057BC04 /* Main */ = { - isa = PBXGroup; - children = ( - 08B191A82CF5A94B0057BC04 /* View */, - 08B191952CF4A0FA0057BC04 /* SignUpStep4Controller.swift */, - 08B191972CF4A1010057BC04 /* SignUpStep4Reactor.swift */, - ); - path = Main; - sourceTree = ""; - }; - 08B191AA2CF5BF8E0057BC04 /* AgeSelectedModal */ = { - isa = PBXGroup; - children = ( - 08B191AD2CF5BFA60057BC04 /* AgeSelectedView.swift */, - 08B191AB2CF5BF9D0057BC04 /* AgeSelectedController.swift */, - 08B191AF2CF5BFAE0057BC04 /* AgeSelectedReactor.swift */, - ); - path = AgeSelectedModal; - sourceTree = ""; - }; - 08CBE9FF2D38986E00248007 /* ResponseDTO */ = { - isa = PBXGroup; - children = ( - BD91034F2CF6149D00BBCCAE /* LoginResponseDTO.swift */, - 08CBEA022D38989E00248007 /* PostTokenReissueResponseDTO.swift */, - ); - path = ResponseDTO; - sourceTree = ""; - }; - 08CBEA042D38990600248007 /* AuthAPI */ = { - isa = PBXGroup; - children = ( - BD91037A2CF614A900BBCCAE /* LoginResponse.swift */, - 08CBEA052D38991600248007 /* PostTokenReissueResponse.swift */, - ); - path = AuthAPI; - sourceTree = ""; - }; - 08CBEA382D3FABD300248007 /* ToastMaker */ = { - isa = PBXGroup; - children = ( - 08B191A12CF4AE890057BC04 /* ToastMaker.swift */, - 08CBEA392D3FABE100248007 /* ToastView.swift */, - 08CBEA3B2D3FABED00248007 /* BookMarkToastView.swift */, - ); - path = ToastMaker; - sourceTree = ""; - }; - 08DC61F62CF76831002A2F44 /* SignUpComplete */ = { - isa = PBXGroup; - children = ( - 08DC61F72CF76843002A2F44 /* SignUpCompleteView.swift */, - 08DC61F92CF7684F002A2F44 /* SignUpCompleteController.swift */, - 08DC61FB2CF76862002A2F44 /* SignUpCompleteReactor.swift */, - ); - path = SignUpComplete; - sourceTree = ""; - }; - 08DC62012CF8ABDA002A2F44 /* Home */ = { - isa = PBXGroup; - children = ( - 086DD8CA2CFDFE9900B97D3B /* List */, - 086DD8C92CFDFE9100B97D3B /* Main */, - ); - path = Home; - sourceTree = ""; - }; - 08DC62082CF8ADB7002A2F44 /* View */ = { - isa = PBXGroup; - children = ( - 0841BAB22CFABEA900049E31 /* HomePopularCardSection */, - 0841BAAD2CFA38CB00049E31 /* HomeCardSection */, - 0841BAA62CFA353500049E31 /* HomeTitleSection */, - 0841BAA12CFA319400049E31 /* SpacingSection */, - 08DC62092CF8ADFF002A2F44 /* ImageBannerSection */, - 0841BAB72CFAC41300049E31 /* SectionBackGroundDecorationView.swift */, - 08DC62022CF8AC06002A2F44 /* HomeView.swift */, - 0841BAB92CFAE5BE00049E31 /* HomeHeaderView.swift */, - ); - path = View; - sourceTree = ""; - }; - 08DC62092CF8ADFF002A2F44 /* ImageBannerSection */ = { - isa = PBXGroup; - children = ( - 0841BA9D2CFA085700049E31 /* ImageBannerSection */, - 0841BA9E2CFA085F00049E31 /* ImageBannerChildSection */, - ); - path = ImageBannerSection; - sourceTree = ""; - }; - 08DC620E2CF8B36D002A2F44 /* ResponseDTO */ = { - isa = PBXGroup; - children = ( - BD9103512CF6149D00BBCCAE /* BannerPopUpStoreDTO.swift */, - BD9103542CF6149D00BBCCAE /* PopUpStoreResponseDTO.swift */, - BD9103522CF6149D00BBCCAE /* GetHomeInfoResponseDTO.swift */, - ); - path = ResponseDTO; - sourceTree = ""; - }; - 08DE8A0E2D5255000049BCAC /* DetailEmptyCommetSection */ = { - isa = PBXGroup; - children = ( - 08DE8A0F2D5255110049BCAC /* DetailEmptyCommetSection.swift */, - 08DE8A112D5255180049BCAC /* DetailEmptyCommetSectionCell.swift */, - ); - path = DetailEmptyCommetSection; - sourceTree = ""; - }; - 08DE8A132D525A4A0049BCAC /* Strings */ = { - isa = PBXGroup; - children = ( - 08DE8A172D525BA20049BCAC /* Terms.plist */, - ); - path = Strings; - sourceTree = ""; - }; - 08DE8A192D5261CD0049BCAC /* Terms */ = { - isa = PBXGroup; - children = ( - 08DE8A1A2D5261DE0049BCAC /* MyPageTermsController.swift */, - 08DE8A1C2D5261E70049BCAC /* MyPageTermsReactor.swift */, - ); - path = Terms; - sourceTree = ""; - }; - 08DE8A3D2D54DCAF0049BCAC /* MyCommentedPopUpGridSection */ = { - isa = PBXGroup; - children = ( - 08DE8A3E2D54DCC40049BCAC /* MyCommentedPopUpGridSection.swift */, - 08DE8A402D54DCCA0049BCAC /* MyCommentedPopUpGridSectionCell.swift */, - ); - path = MyCommentedPopUpGridSection; - sourceTree = ""; - }; - 4E685EB52D12CEB6001EF91C /* FillterSheetView */ = { - isa = PBXGroup; - children = ( - 4E685EAA2D12CEB6001EF91C /* BalloonBackgroundView.swift */, - 4E685EAB2D12CEB6001EF91C /* BalloonChipCell.swift */, - 4E685EAD2D12CEB6001EF91C /* FilterBottomSheetReactor.swift */, - 4E685EAE2D12CEB6001EF91C /* FilterBottomSheetView.swift */, - 4E685EAF2D12CEB6001EF91C /* FilterBottomSheetViewController.swift */, - 4E685EB02D12CEB6001EF91C /* FilterCell.swift */, - 4E685EB12D12CEB6001EF91C /* FilterChip.swift */, - 4E685EB22D12CEB6001EF91C /* FilterChipsView.swift */, - ); - path = FillterSheetView; - sourceTree = ""; - }; - 4E685EB72D12CEB6001EF91C /* Repository */ = { - isa = PBXGroup; - children = ( - 4E685EB62D12CEB6001EF91C /* MapRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - 4E685EB92D12CEB6001EF91C /* UseCase */ = { - isa = PBXGroup; - children = ( - 4E685EB82D12CEB6001EF91C /* MapUseCase.swift */, - ); - path = UseCase; - sourceTree = ""; - }; - 4E685EBD2D12CEB6001EF91C /* MapDomain */ = { - isa = PBXGroup; - children = ( - 4E685EB72D12CEB6001EF91C /* Repository */, - 4E685EB92D12CEB6001EF91C /* UseCase */, - 4E685EBA2D12CEB6001EF91C /* MapAPIEndpoint.swift */, - 4E685EBB2D12CEB6001EF91C /* MapPopUpStore.swift */, - 4E685EBC2D12CEB6001EF91C /* MapPopUpStoreDTO.swift */, - ); - path = MapDomain; - sourceTree = ""; - }; - 4E685EC02D12CEB6001EF91C /* MapPopupCardView */ = { - isa = PBXGroup; - children = ( - 4E685EBE2D12CEB6001EF91C /* MapPopupCarouselView.swift */, - 4E685EBF2D12CEB6001EF91C /* PopupCardCell.swift */, - ); - path = MapPopupCardView; - sourceTree = ""; - }; - 4E685EC52D12CEB6001EF91C /* StoreListView */ = { - isa = PBXGroup; - children = ( - 4E685EC12D12CEB6001EF91C /* StoreListCell.swift */, - 4E685EC22D12CEB6001EF91C /* StoreListReactor.swift */, - 4E685EC32D12CEB6001EF91C /* StoreListView.swift */, - 4EA998992D21C2FC009DC30B /* StoreListSection.swift */, - 4E685EC42D12CEB6001EF91C /* StoreListViewController.swift */, - ); - path = StoreListView; - sourceTree = ""; - }; - 4E685ECD2D12CEB6001EF91C /* Map */ = { - isa = PBXGroup; - children = ( - 4EC23F432D6F7E6D00558673 /* MapView */, - 4E685EB52D12CEB6001EF91C /* FillterSheetView */, - 4E685EC52D12CEB6001EF91C /* StoreListView */, - 4EED9BAA2D2272F500B288E7 /* Common */, - 4EE5A3D12D40E3B100A2469A /* FindMap */, - ); - path = Map; - sourceTree = ""; - }; - 4E755B1B2D2B9ABF00ADFB21 /* Admin */ = { - isa = PBXGroup; - children = ( - 4E9C127F2D2BD01F006744D6 /* Domain */, - 4E9C127B2D2BCFE4006744D6 /* Data */, - 4E755B222D2B9C5D00ADFB21 /* AdminViewController.swift */, - 4E755B242D2B9C6C00ADFB21 /* AdminView.swift */, - 4E755B262D2B9C7C00ADFB21 /* AdminStoreCell.swift */, - 4E755B282D2BA65A00ADFB21 /* AdminReactor.swift */, - 4E9405302D6F7C790002B590 /* AdminRegister */, - 4E94052F2D6F7C670002B590 /* AdminBottomSheet */, - 4EEA1D8E2D352012003E7DE9 /* ImageCell.swift */, - 4EDDEFB22D2D284B00CFAFA5 /* Common */, - ); - path = Admin; - sourceTree = ""; - }; - 4E94052F2D6F7C670002B590 /* AdminBottomSheet */ = { - isa = PBXGroup; - children = ( - 4E9C12772D2BC7A0006744D6 /* AdminBottomSheetView.swift */, - 4E9C12792D2BC811006744D6 /* AdminBottomSheetViewController.swift */, - 4E6CA4842D34D6ED0034D09A /* AdminBottomSheetReactor.swift */, - ); - path = AdminBottomSheet; - sourceTree = ""; - }; - 4E9405302D6F7C790002B590 /* AdminRegister */ = { - isa = PBXGroup; - children = ( - 4E9C12802D2BE0A6006744D6 /* PopUpStoreRegisterViewController.swift */, - 4E643FC02D738D7F0046AF29 /* PopUpStoreRegisterReactor.swift */, - 4E643FC22D738D930046AF29 /* PopUpStoreRegisterView.swift */, - ); - path = AdminRegister; - sourceTree = ""; - }; - 4E9C127B2D2BCFE4006744D6 /* Data */ = { - isa = PBXGroup; - children = ( - 4E685EBD2D12CEB6001EF91C /* MapDomain */, - 4E9C127E2D2BD012006744D6 /* Remote */, - 4E9C127D2D2BD007006744D6 /* Repository */, - 4E9C127C2D2BCFF1006744D6 /* DTO */, - ); - path = Data; - sourceTree = ""; - }; - 4E9C127C2D2BCFF1006744D6 /* DTO */ = { - isa = PBXGroup; - children = ( - 4E755B1C2D2B9AD300ADFB21 /* GetAdminPopUpStoreListResponseDTO.swift */, - 4E755B202D2B9BAB00ADFB21 /* AdminResponseDTO.swift */, - ); - path = DTO; - sourceTree = ""; - }; - 4E9C127D2D2BD007006744D6 /* Repository */ = { - isa = PBXGroup; - children = ( - 4E755B2E2D2BA7FB00ADFB21 /* AdminRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - 4E9C127E2D2BD012006744D6 /* Remote */ = { - isa = PBXGroup; - children = ( - 4E755B1E2D2B9AE500ADFB21 /* AdminAPIEndpoint.swift */, - ); - path = Remote; - sourceTree = ""; - }; - 4E9C127F2D2BD01F006744D6 /* Domain */ = { - isa = PBXGroup; - children = ( - 4E755B2A2D2BA76E00ADFB21 /* AdminUseCase.swift */, - ); - path = Domain; - sourceTree = ""; - }; - 4EA2C93B2D424D2600F4D97C /* MapGuideView */ = { - isa = PBXGroup; - children = ( - 4E6A066F2D42A96100B2A658 /* FullScreenMapViewController.swift */, - 4EA2C93C2D424D3300F4D97C /* MapDirectionRepository.swift */, - 4EA2C93E2D424D7400F4D97C /* MapDirectionUseCase.swift */, - 4EA2C9402D424D8400F4D97C /* GetPopUpDirectionResponseDTO.swift */, - 4E9790C42D40E13500210499 /* MapGuideViewController.swift */, - 4EE5A3D22D40E4A600A2469A /* MapGuideReactor.swift */, - 4EA2C9422D424DF900F4D97C /* FindDirectionEndPoint.swift */, - ); - path = MapGuideView; - sourceTree = ""; - }; - 4EC23F432D6F7E6D00558673 /* MapView */ = { - isa = PBXGroup; - children = ( - 4E8AA29C2D59A2340029DF75 /* MarkerTooltipView.swift */, - 4E685EC72D12CEB6001EF91C /* MapMarker.swift */, - 4E685EC82D12CEB6001EF91C /* MapReactor.swift */, - 4E685EC92D12CEB6001EF91C /* MapSearchInput.swift */, - 4E685ECB2D12CEB6001EF91C /* MapView.swift */, - 4E685ECC2D12CEB6001EF91C /* MapViewController.swift */, - ); - path = MapView; - sourceTree = ""; - }; - 4EDDEFB22D2D284B00CFAFA5 /* Common */ = { - isa = PBXGroup; - children = ( - 4EDDEFB32D2D285900CFAFA5 /* DateTimePickerManager.swift */, - 4EEA1D902D352027003E7DE9 /* ExtendedImage.swift */, - ); - path = Common; - sourceTree = ""; - }; - 4EE5A3D12D40E3B100A2469A /* FindMap */ = { - isa = PBXGroup; - children = ( - 4EA2C93B2D424D2600F4D97C /* MapGuideView */, - ); - path = FindMap; - sourceTree = ""; - }; - 4EED9BAA2D2272F500B288E7 /* Common */ = { - isa = PBXGroup; - children = ( - 4EAB809C2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift */, - 4EAB809E2D3F8EF50041AF30 /* ViewportBounds.swift */, - 4EED9BAB2D22730400B288E7 /* FilterType.swift */, - 4E6C07052D4B6E56008A962A /* RegionDefinitions.swift */, - 4E685EC62D12CEB6001EF91C /* MapFilterChips.swift */, - 4E9A465F2D55D1270010578A /* MapUtilities.swift */, - 4E6C07092D4B6E81008A962A /* ClusteringManager.swift */, - 4E6C07072D4B6E74008A962A /* ClusteringModels.swift */, - 4EDE57022D5E70650014D924 /* LocationPermissionBottomSheet.swift */, - 4E685EC02D12CEB6001EF91C /* MapPopupCardView */, - ); - path = Common; - sourceTree = ""; - }; - BD9103502CF6149D00BBCCAE /* AuthAPI */ = { - isa = PBXGroup; - children = ( - BD91034E2CF6149D00BBCCAE /* AuthAPIEndPoint.swift */, - 08CBE9FF2D38986E00248007 /* ResponseDTO */, - ); - path = AuthAPI; - sourceTree = ""; - }; - BD9103562CF6149D00BBCCAE /* HomeAPI */ = { - isa = PBXGroup; - children = ( - BD9103532CF6149D00BBCCAE /* HomeAPIEndpoint.swift */, - 08DC620E2CF8B36D002A2F44 /* ResponseDTO */, - ); - path = HomeAPI; - sourceTree = ""; - }; - BD91035A2CF6149D00BBCCAE /* Repository */ = { - isa = PBXGroup; - children = ( - BD9103572CF6149D00BBCCAE /* AuthAPIRepositoryImpl.swift */, - BD9103582CF6149D00BBCCAE /* HomeAPIRepository.swift */, - BD9103592CF6149D00BBCCAE /* SignUpRepositoryImpl.swift */, - 086DD8D92CFF194700B97D3B /* UserAPIRepositoryImpl.swift */, - 089952522D033C940022AEF9 /* PopUpAPIRepositoryImpl.swift */, - 083C86772D0EE382003F441C /* CommentAPIRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - BD91035F2CF6149D00BBCCAE /* SignUpAPI */ = { - isa = PBXGroup; - children = ( - BD91035B2CF6149D00BBCCAE /* CheckNickNameRequestDTO.swift */, - BD91035C2CF6149D00BBCCAE /* GetCategoryListResponseDTO.swift */, - BD91035D2CF6149D00BBCCAE /* SignUpAPIEndpoint.swift */, - BD91035E2CF6149D00BBCCAE /* SignUpRequestDTO.swift */, - ); - path = SignUpAPI; - sourceTree = ""; - }; - BD9103602CF6149D00BBCCAE /* Network */ = { - isa = PBXGroup; - children = ( - 08DC62102CF8B446002A2F44 /* SortedRequestDTO.swift */, - 083C86712D0EE2A3003F441C /* CommentAPI */, - 089952472D033A0E0022AEF9 /* PopUpAPI */, - 086DD8D42CFF181500B97D3B /* UserAPI */, - BD9103502CF6149D00BBCCAE /* AuthAPI */, - BD9103562CF6149D00BBCCAE /* HomeAPI */, - BD91035F2CF6149D00BBCCAE /* SignUpAPI */, - ); - path = Network; - sourceTree = ""; - }; - BD9103752CF614A900BBCCAE /* UseCase */ = { - isa = PBXGroup; - children = ( - BD9103702CF614A900BBCCAE /* HomeAPIUseCaseImpl.swift */, - BD9103722CF614A900BBCCAE /* SignUpAPIUseCaseImpl.swift */, - BD9103742CF614A900BBCCAE /* AuthAPIUseCaseImpl.swift */, - 086DD8DD2CFF19C400B97D3B /* UserAPIUseCaseImpl.swift */, - 089952542D033D480022AEF9 /* PopUpAPIUseCaseImpl.swift */, - 083C86792D0EE3BB003F441C /* CommentAPIUseCaseImpl.swift */, - ); - path = UseCase; - sourceTree = ""; +/* Begin PBXCopyFilesBuildPhase section */ + 0543C5CF2DF86C740070BB93 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 0543C5D32DF86C7B0070BB93 /* Domain.framework in Embed Frameworks */, + 0543C5CE2DF86C740070BB93 /* Data.framework in Embed Frameworks */, + 0543C5DB2DF86C800070BB93 /* PresentationInterface.framework in Embed Frameworks */, + 0543C5DF2DF86C830070BB93 /* SearchFeatureInterface.framework in Embed Frameworks */, + 0543C5D92DF86C7F0070BB93 /* Presentation.framework in Embed Frameworks */, + 0543C5D72DF86C7E0070BB93 /* Infrastructure.framework in Embed Frameworks */, + 0543C5DD2DF86C810070BB93 /* SearchFeature.framework in Embed Frameworks */, + 0543C5D52DF86C7C0070BB93 /* DomainInterface.framework in Embed Frameworks */, + 0543C5D12DF86C790070BB93 /* DesignSystem.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; }; - BD91037B2CF614A900BBCCAE /* Entities */ = { - isa = PBXGroup; - children = ( - 08CBEA042D38990600248007 /* AuthAPI */, - 086F89DE2D1E7CBE00CA4FC9 /* UserAPI */, - BD9103762CF614A900BBCCAE /* BannerPopUpStore.swift */, - BD9103772CF614A900BBCCAE /* Category.swift */, - BD9103782CF614A900BBCCAE /* GetHomeInfoResponse.swift */, - BD9103792CF614A900BBCCAE /* PopUpStoreResponse.swift */, - 0899526F2D0474430022AEF9 /* PopUpAPI */, +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 05229DD02D99519200D88E73 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + 05734C582DCDFAC20093825D /* SearchFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SearchFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C592DCDFAC20093825D /* SearchFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SearchFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C5E2DCE04CE0093825D /* PresentationInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PresentationInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05BDD3D52DB66E1700C1E192 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6072DB53A4900508FFD /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6082DB53A4900508FFD /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D6092DB53A4900508FFD /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D60A2DB53A4900508FFD /* Presentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Presentation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05CFFC422DCC83290051129F /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BDCA41BD2CF35AC0005EECF6 /* Poppool.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Poppool.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 05878B6F2DAD2E10004F81E2 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + /Localized/Resource/LaunchScreen.storyboard, + Resource/Debug.xcconfig, + Resource/Info.plist, ); - path = Entities; - sourceTree = ""; + target = BDCA41BC2CF35AC0005EECF6 /* Poppool */; }; - BD91037F2CF614A900BBCCAE /* Repository */ = { - isa = PBXGroup; - children = ( - BD91037C2CF614A900BBCCAE /* AuthRepository.swift */, +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + 05878B702DAD2E10004F81E2 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { + isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; + buildPhase = BDCA41BB2CF35AC0005EECF6 /* Resources */; + membershipExceptions = ( + Resource/Base.lproj/LaunchScreen.storyboard, ); - path = Repository; - sourceTree = ""; }; - BD91038F2CF6166800BBCCAE /* View */ = { - isa = PBXGroup; - children = ( - BD91038E2CF6166800BBCCAE /* SplashView.swift */, +/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 058789AD2DAD2E10004F81E2 /* Poppool */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (05878B6F2DAD2E10004F81E2 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 05878B702DAD2E10004F81E2 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Poppool; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + BDCA41BA2CF35AC0005EECF6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0543C5D02DF86C790070BB93 /* DesignSystem.framework in Frameworks */, + 05734C662DCE05070093825D /* PanModal in Frameworks */, + 0543C5D42DF86C7C0070BB93 /* DomainInterface.framework in Frameworks */, + 0543C5D62DF86C7E0070BB93 /* Infrastructure.framework in Frameworks */, + 0522C1D92DB67C5900B141FF /* RxSwift in Frameworks */, + 0543C5D82DF86C7F0070BB93 /* Presentation.framework in Frameworks */, + 0543C5DE2DF86C830070BB93 /* SearchFeatureInterface.framework in Frameworks */, + 4E15142E2D994A3A00DFD08F /* KakaoSDKAuth in Frameworks */, + 0543C5D22DF86C7B0070BB93 /* Domain.framework in Frameworks */, + 05734C6E2DCE05680093825D /* Tabman in Frameworks */, + 05734C712DCE059D0093825D /* Then in Frameworks */, + 05BBA73E2DB75DA60047A061 /* KakaoSDKUser in Frameworks */, + 05734C6B2DCE05550093825D /* ReactorKit in Frameworks */, + 0522C3C62DB67D7800B141FF /* RxGesture in Frameworks */, + 0543C5DC2DF86C810070BB93 /* SearchFeature.framework in Frameworks */, + 4EE360FD2D91876300D2441D /* NMapsMap in Frameworks */, + 0543C5DA2DF86C800070BB93 /* PresentationInterface.framework in Frameworks */, + 05734C682DCE05240093825D /* SnapKit in Frameworks */, + 0543C5CD2DF86C740070BB93 /* Data.framework in Frameworks */, + 05734C632DCE04FA0093825D /* Pageboy in Frameworks */, + 4E15142A2D99480200DFD08F /* NMapsMap in Frameworks */, ); - path = View; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - BD9103912CF6166800BBCCAE /* Splash */ = { +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05C1D6062DB53A4900508FFD /* Frameworks */ = { isa = PBXGroup; children = ( - BD91038F2CF6166800BBCCAE /* View */, - BD9103902CF6166800BBCCAE /* SplashController.swift */, + 05734C5E2DCE04CE0093825D /* PresentationInterface.framework */, + 05734C582DCDFAC20093825D /* SearchFeature.framework */, + 05734C592DCDFAC20093825D /* SearchFeatureInterface.framework */, + 05CFFC422DCC83290051129F /* DesignSystem.framework */, + 05BDD3D52DB66E1700C1E192 /* DomainInterface.framework */, + 05C1D6072DB53A4900508FFD /* Data.framework */, + 05C1D6082DB53A4900508FFD /* Domain.framework */, + 05C1D6092DB53A4900508FFD /* Infrastructure.framework */, + 05C1D60A2DB53A4900508FFD /* Presentation.framework */, ); - path = Splash; + name = Frameworks; sourceTree = ""; }; BDCA41B42CF35AC0005EECF6 = { isa = PBXGroup; children = ( - BDCA41BF2CF35AC0005EECF6 /* Poppool */, - BDCA41D62CF35AC1005EECF6 /* PoppoolTests */, - BDCA41E02CF35AC1005EECF6 /* PoppoolUITests */, + 05229DD02D99519200D88E73 /* .swiftlint.yml */, + 058789AD2DAD2E10004F81E2 /* Poppool */, + 05C1D6062DB53A4900508FFD /* Frameworks */, BDCA41BE2CF35AC0005EECF6 /* Products */, ); sourceTree = ""; @@ -3020,43 +163,10 @@ isa = PBXGroup; children = ( BDCA41BD2CF35AC0005EECF6 /* Poppool.app */, - BDCA41D32CF35AC1005EECF6 /* PoppoolTests.xctest */, - BDCA41DD2CF35AC1005EECF6 /* PoppoolUITests.xctest */, ); name = Products; sourceTree = ""; }; - BDCA41BF2CF35AC0005EECF6 /* Poppool */ = { - isa = PBXGroup; - children = ( - BDE30CE02CF87A9700C21E08 /* Poppool.entitlements */, - 083A256B2CF361190099B58E /* Application */, - 083A256D2CF3613C0099B58E /* Presentation */, - 083A259E2CF362310099B58E /* Domain */, - 083A259F2CF362360099B58E /* Data */, - 083A25A02CF3623C0099B58E /* Infrastructure */, - 083A256C2CF361210099B58E /* Resource */, - ); - path = Poppool; - sourceTree = ""; - }; - BDCA41D62CF35AC1005EECF6 /* PoppoolTests */ = { - isa = PBXGroup; - children = ( - BDCA41D72CF35AC1005EECF6 /* PoppoolTests.swift */, - ); - path = PoppoolTests; - sourceTree = ""; - }; - BDCA41E02CF35AC1005EECF6 /* PoppoolUITests */ = { - isa = PBXGroup; - children = ( - BDCA41E12CF35AC1005EECF6 /* PoppoolUITests.swift */, - BDCA41E32CF35AC1005EECF6 /* PoppoolUITestsLaunchTests.swift */, - ); - path = PoppoolUITests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -3064,76 +174,37 @@ isa = PBXNativeTarget; buildConfigurationList = BDCA41E72CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "Poppool" */; buildPhases = ( + 05229DCF2D99507C00D88E73 /* Run SwiftLint */, BDCA41B92CF35AC0005EECF6 /* Sources */, BDCA41BA2CF35AC0005EECF6 /* Frameworks */, BDCA41BB2CF35AC0005EECF6 /* Resources */, + 0543C5CF2DF86C740070BB93 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 058789AD2DAD2E10004F81E2 /* Poppool */, + ); name = Poppool; packageProductDependencies = ( - BDCA41F12CF35D0D005EECF6 /* SnapKit */, - BDCA41F42CF35D33005EECF6 /* Kingfisher */, - BDCA41F72CF35D9A005EECF6 /* RxSwift */, - BDCA41FD2CF35EE7005EECF6 /* ReactorKit */, - BDCA42002CF35EFE005EECF6 /* RxKeyboard */, - BDCA42032CF35F76005EECF6 /* PanModal */, - BDCA42062CF35FA6005EECF6 /* Tabman */, - BDCA42092CF35FB1005EECF6 /* Pageboy */, - BDCA420C2CF35FD2005EECF6 /* RxGesture */, - BDCA420F2CF35FF5005EECF6 /* Lottie */, - 083A25CF2CF364B70099B58E /* Alamofire */, - 08B191B92CF609AE0057BC04 /* RxKakaoSDK */, - 08B191BB2CF609AE0057BC04 /* RxKakaoSDKAuth */, - 08B191BD2CF609AE0057BC04 /* RxKakaoSDKUser */, - 088DE2432D104EE70030FA9E /* SwiftSoup */, - 088DE2462D12DB5C0030FA9E /* GoogleMaps */, - 4E5825662D1951DF00EE83EF /* FloatingPanel */, - 4EA9989C2D21C404009DC30B /* RxDataSources */, - 082197A02D426DCB0054094A /* Then */, + 4E1514292D99480200DFD08F /* NMapsMap */, + 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */, + 0522C1D82DB67C5900B141FF /* RxSwift */, + 0522C3C52DB67D7800B141FF /* RxGesture */, + 05BBA73D2DB75DA60047A061 /* KakaoSDKUser */, + 05734C622DCE04FA0093825D /* Pageboy */, + 05734C652DCE05070093825D /* PanModal */, + 05734C672DCE05240093825D /* SnapKit */, + 05734C6A2DCE05550093825D /* ReactorKit */, + 05734C6D2DCE05680093825D /* Tabman */, + 05734C702DCE059D0093825D /* Then */, ); productName = Poppool; productReference = BDCA41BD2CF35AC0005EECF6 /* Poppool.app */; productType = "com.apple.product-type.application"; }; - BDCA41D22CF35AC1005EECF6 /* PoppoolTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = BDCA41EA2CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "PoppoolTests" */; - buildPhases = ( - BDCA41CF2CF35AC1005EECF6 /* Sources */, - BDCA41D02CF35AC1005EECF6 /* Frameworks */, - BDCA41D12CF35AC1005EECF6 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - BDCA41D52CF35AC1005EECF6 /* PBXTargetDependency */, - ); - name = PoppoolTests; - productName = PoppoolTests; - productReference = BDCA41D32CF35AC1005EECF6 /* PoppoolTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - BDCA41DC2CF35AC1005EECF6 /* PoppoolUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = BDCA41ED2CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "PoppoolUITests" */; - buildPhases = ( - BDCA41D92CF35AC1005EECF6 /* Sources */, - BDCA41DA2CF35AC1005EECF6 /* Frameworks */, - BDCA41DB2CF35AC1005EECF6 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - BDCA41DF2CF35AC1005EECF6 /* PBXTargetDependency */, - ); - name = PoppoolUITests; - productName = PoppoolUITests; - productReference = BDCA41DD2CF35AC1005EECF6 /* PoppoolUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -3142,23 +213,14 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1540; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1640; TargetAttributes = { BDCA41BC2CF35AC0005EECF6 = { CreatedOnToolsVersion = 15.4; }; - BDCA41D22CF35AC1005EECF6 = { - CreatedOnToolsVersion = 15.4; - TestTargetID = BDCA41BC2CF35AC0005EECF6; - }; - BDCA41DC2CF35AC1005EECF6 = { - CreatedOnToolsVersion = 15.4; - TestTargetID = BDCA41BC2CF35AC0005EECF6; - }; }; }; buildConfigurationList = BDCA41B82CF35AC0005EECF6 /* Build configuration list for PBXProject "Poppool" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = ko; hasScannedForEncodings = 0; knownRegions = ( @@ -3167,31 +229,23 @@ ); mainGroup = BDCA41B42CF35AC0005EECF6; packageReferences = ( - BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */, - BDCA41F32CF35D33005EECF6 /* XCRemoteSwiftPackageReference "Kingfisher" */, - BDCA41F62CF35D9A005EECF6 /* XCRemoteSwiftPackageReference "RxSwift" */, - BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */, - BDCA41FC2CF35EE7005EECF6 /* XCRemoteSwiftPackageReference "ReactorKit" */, - BDCA41FF2CF35EFE005EECF6 /* XCRemoteSwiftPackageReference "RxKeyboard" */, - BDCA42022CF35F76005EECF6 /* XCRemoteSwiftPackageReference "PanModal" */, - BDCA42052CF35FA6005EECF6 /* XCRemoteSwiftPackageReference "Tabman" */, - BDCA42082CF35FB1005EECF6 /* XCRemoteSwiftPackageReference "Pageboy" */, - BDCA420B2CF35FD2005EECF6 /* XCRemoteSwiftPackageReference "RxGesture" */, - BDCA420E2CF35FF5005EECF6 /* XCRemoteSwiftPackageReference "lottie-spm" */, - 083A25CE2CF364B70099B58E /* XCRemoteSwiftPackageReference "Alamofire" */, - 088DE2422D104EE70030FA9E /* XCRemoteSwiftPackageReference "SwiftSoup" */, - 088DE2452D12DB5C0030FA9E /* XCRemoteSwiftPackageReference "ios-maps-sdk" */, - 4E5825652D1951DF00EE83EF /* XCRemoteSwiftPackageReference "FloatingPanel" */, - 4EA9989B2D21C404009DC30B /* XCRemoteSwiftPackageReference "RxDataSources" */, - 0821979F2D426DCB0054094A /* XCRemoteSwiftPackageReference "Then" */, - ); + 08F403312D884F4D00BFA61A /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */, + 4E1514282D99480200DFD08F /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */, + 05BDD5C32DB674E500C1E192 /* XCRemoteSwiftPackageReference "SnapKit" */, + 05BDD5D62DB6783C00C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */, + 0522C3C42DB67D7800B141FF /* XCRemoteSwiftPackageReference "RxGesture" */, + 05734C612DCE04FA0093825D /* XCRemoteSwiftPackageReference "Pageboy" */, + 05734C642DCE05070093825D /* XCRemoteSwiftPackageReference "PanModal" */, + 05734C692DCE05550093825D /* XCRemoteSwiftPackageReference "ReactorKit" */, + 05734C6C2DCE05680093825D /* XCRemoteSwiftPackageReference "Tabman" */, + 05734C6F2DCE059D0093825D /* XCRemoteSwiftPackageReference "Then" */, + ); + preferredProjectObjectVersion = 56; productRefGroup = BDCA41BE2CF35AC0005EECF6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( BDCA41BC2CF35AC0005EECF6 /* Poppool */, - BDCA41D22CF35AC1005EECF6 /* PoppoolTests */, - BDCA41DC2CF35AC1005EECF6 /* PoppoolUITests */, ); }; /* End PBXProject section */ @@ -3201,530 +255,43 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 08B191562CF41D6F0057BC04 /* PP_splash.json in Resources */, - 08B1914B2CF41D690057BC04 /* GothicA1-Bold.ttf in Resources */, - 08DE8A182D525BA20049BCAC /* Terms.plist in Resources */, - 08B1914D2CF41D690057BC04 /* GothicA1-Medium.ttf in Resources */, - 08B1914F2CF41D690057BC04 /* Poppins-Bold.ttf in Resources */, - 08B191502CF41D690057BC04 /* Poppins-Light.ttf in Resources */, - 08B191552CF41D6F0057BC04 /* PP_loading.json in Resources */, - BDCA41CA2CF35AC1005EECF6 /* Assets.xcassets in Resources */, - BDCA41CD2CF35AC1005EECF6 /* Base in Resources */, - 08B191512CF41D690057BC04 /* Poppins-Medium.ttf in Resources */, - 08B1914C2CF41D690057BC04 /* GothicA1-Light.ttf in Resources */, - 08B1914E2CF41D690057BC04 /* GothicA1-Regular.ttf in Resources */, - 08B191522CF41D690057BC04 /* Poppins-Regular.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - BDCA41D12CF35AC1005EECF6 /* Resources */ = { - isa = PBXResourcesBuildPhase; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 05229DCF2D99507C00D88E73 /* Run SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; - }; - BDCA41DB2CF35AC1005EECF6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run SwiftLint"; + outputFileListPaths = ( + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; -/* End PBXResourcesBuildPhase section */ +/* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ BDCA41B92CF35AC0005EECF6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 081898AE2D2CFC230067BF01 /* GetWithdrawlListResponseDTO.swift in Sources */, - 088DE25A2D1458620030FA9E /* DetailCommentImageCell.swift in Sources */, - 089952662D046CCD0022AEF9 /* SearchResultController.swift in Sources */, - 0818993F2D35FBE00067BF01 /* GetRecentPopUpResponseDTO.swift in Sources */, - 08B191962CF4A0FA0057BC04 /* SignUpStep4Controller.swift in Sources */, - 0841BABC2CFB59E200049E31 /* String?+.swift in Sources */, - 0841BA872CF9F62400049E31 /* PreSignedURLDTO.swift in Sources */, - 086F89EC2D2009EB00CA4FC9 /* SubLoginController.swift in Sources */, - 083C86262D087A4E003F441C /* DetailTitleSectionCell.swift in Sources */, - 4E9A46602D55D1270010578A /* MapUtilities.swift in Sources */, - 086DD8DE2CFF19C400B97D3B /* UserAPIUseCaseImpl.swift in Sources */, - 081898C32D30AE2C0067BF01 /* GetMyProfileResponseDTO.swift in Sources */, - 08A2E4802D1BCDE300102313 /* CommentDetailView.swift in Sources */, - 081898BA2D2E5F4C0067BF01 /* MyCommentView.swift in Sources */, - 089952492D033A1C0022AEF9 /* PopUpAPIEndPoint.swift in Sources */, - 083A25C82CF363C00099B58E /* LoginController.swift in Sources */, - 081899292D3500C50067BF01 /* FAQReactor.swift in Sources */, - 083A25B72CF362670099B58E /* RequestEndpoint.swift in Sources */, - 4E6C070A2D4B6E81008A962A /* ClusteringManager.swift in Sources */, - 08A2E47B2D1B06AA00102313 /* ImageDetailController.swift in Sources */, - 083A25B82CF362670099B58E /* IndicatorMaker.swift in Sources */, - 08DE8A102D5255110049BCAC /* DetailEmptyCommetSection.swift in Sources */, - 086F89E62D1FE91800CA4FC9 /* OtherUserCommentSectionCell.swift in Sources */, - 08B191B62CF6092B0057BC04 /* AppleLoginService.swift in Sources */, - 081899542D363E6A0067BF01 /* BookMarkPopUpViewTypeModalReactor.swift in Sources */, - 083C866B2D0ECB4F003F441C /* InstaGuideSectionCell.swift in Sources */, - 083C86202D087445003F441C /* GetPopUpDetailResponse.swift in Sources */, - 4EA2C9432D424DF900F4D97C /* FindDirectionEndPoint.swift in Sources */, - 081898FD2D33D9ED0067BF01 /* GetBlockUserListResponse.swift in Sources */, - 083A25B22CF362670099B58E /* NetworkError.swift in Sources */, - 081899522D363E640067BF01 /* BookMarkPopUpViewTypeModalController.swift in Sources */, - BD9103692CF6149D00BBCCAE /* HomeAPIRepository.swift in Sources */, - 083A25832CF361EF0099B58E /* BaseViewController.swift in Sources */, - 089952752D0475F20022AEF9 /* SearchResultCountSectionCell.swift in Sources */, - 4EECA3942D56770B00A07CCA /* MapPopUpStore.swift in Sources */, - 4E9C12782D2BC7A0006744D6 /* AdminBottomSheetView.swift in Sources */, - 086DD9362D00963900B97D3B /* SearchTitleSection.swift in Sources */, - 083A25922CF361F90099B58E /* ViewConvention.swift in Sources */, - 086F89D92D1E79E200CA4FC9 /* GetOtherUserCommentListRequestDTO.swift in Sources */, - 083C864F2D0DD3A6003F441C /* AddCommentImageSection.swift in Sources */, - 08B191372CF366680057BC04 /* UICollectionViewCell+.swift in Sources */, - 083A25B42CF362670099B58E /* Responsable.swift in Sources */, - 08CBEA0D2D38ED0D00248007 /* CountButtonView.swift in Sources */, - 0841BAA82CFA354500049E31 /* HomeTitleSection.swift in Sources */, - 08B191782CF442230057BC04 /* UIImage+.swift in Sources */, - 08DE8A122D5255180049BCAC /* DetailEmptyCommetSectionCell.swift in Sources */, - 081898BC2D2E5F510067BF01 /* MyCommentReactor.swift in Sources */, - 0841BA892CF9F62400049E31 /* PreSignedURLResponseDTO.swift in Sources */, - 088DE24A2D12F3360030FA9E /* DetailInfoSection.swift in Sources */, - 081898D02D30EA900067BF01 /* PutUserTailoredInfoRequestDTO.swift in Sources */, - 081898E62D3391CB0067BF01 /* GetMyCommentResponse.swift in Sources */, - 0818991E2D34DF7D0067BF01 /* MyPageNoticeDetailView.swift in Sources */, - BD9103682CF6149D00BBCCAE /* AuthAPIRepositoryImpl.swift in Sources */, - 08DC61F82CF76843002A2F44 /* SignUpCompleteView.swift in Sources */, - BD9103612CF6149D00BBCCAE /* AuthAPIEndPoint.swift in Sources */, - 086DD9422D01EEF700B97D3B /* SearchCountTitleSectionCell.swift in Sources */, - 081898A72D2CC01D0067BF01 /* WithdrawlReasonReactor.swift in Sources */, - 081899332D35F1090067BF01 /* MyPageBookmarkView.swift in Sources */, - 08CBEA3A2D3FABE100248007 /* ToastView.swift in Sources */, - 081899052D34080B0067BF01 /* BlockUserListSectionCell.swift in Sources */, - 4E685EEA2D12CEB6001EF91C /* MapViewController.swift in Sources */, - 4E6C07062D4B6E56008A962A /* RegionDefinitions.swift in Sources */, - 089952582D0347AC0022AEF9 /* SearchCategoryController.swift in Sources */, - 0841BAAA2CFA354C00049E31 /* HomeTitleSectionCell.swift in Sources */, - 08B191612CF430E70057BC04 /* PPProgressView.swift in Sources */, - 086F89EA2D2009E300CA4FC9 /* SubLoginView.swift in Sources */, - 081899352D35F10F0067BF01 /* MyPageBookmarkController.swift in Sources */, - 083A25B32CF362670099B58E /* Requestable.swift in Sources */, - BD9103932CF6166800BBCCAE /* SplashController.swift in Sources */, - 08B1916A2CF434B80057BC04 /* SignUpStep1Controller.swift in Sources */, - 4E685ED52D12CEB6001EF91C /* FilterChip.swift in Sources */, - 083C861C2D087337003F441C /* GetPopUpDetailRequestDTO.swift in Sources */, - BD9103882CF614A900BBCCAE /* GetHomeInfoResponse.swift in Sources */, - 081899022D3407F50067BF01 /* BlockUserListSection.swift in Sources */, - 081899222D34DF8E0067BF01 /* MyPageNoticeDetailReactor.swift in Sources */, - 4E685EDB2D12CEB6001EF91C /* MapAPIEndpoint.swift in Sources */, - 086F89CC2D1E42B000CA4FC9 /* CommentUserBlockController.swift in Sources */, - 08DC62032CF8AC06002A2F44 /* HomeView.swift in Sources */, - BD9103622CF6149D00BBCCAE /* LoginResponseDTO.swift in Sources */, - 083C86642D0EC4A5003F441C /* InstaCommentAddReactor.swift in Sources */, - 08DE8A3F2D54DCC40049BCAC /* MyCommentedPopUpGridSection.swift in Sources */, - 081898C52D30AEF40067BF01 /* GetMyProfileResponse.swift in Sources */, - BD9103922CF6166800BBCCAE /* SplashView.swift in Sources */, - 0899526E2D0474340022AEF9 /* GetSearchPopUpListResponse.swift in Sources */, - 08B191392CF366680057BC04 /* UITableViewCell+.swift in Sources */, - 08A2E48F2D1BF6E500102313 /* CommentListView.swift in Sources */, - 4E755B1D2D2B9AD300ADFB21 /* GetAdminPopUpStoreListResponseDTO.swift in Sources */, - 083A25992CF362090099B58E /* Sectionable.swift in Sources */, - 086DD8E02CFF2C3700B97D3B /* UIImageView+.swift in Sources */, - 081898DA2D32559B0067BF01 /* PutUserProfileRequestDTO.swift in Sources */, - 08B191AC2CF5BF9D0057BC04 /* AgeSelectedController.swift in Sources */, - 4E755B1F2D2B9AE500ADFB21 /* AdminAPIEndpoint.swift in Sources */, - 083C86622D0EC49E003F441C /* InstaCommentAddController.swift in Sources */, - 08B1919C2CF4A77C0057BC04 /* TagSection.swift in Sources */, - BD91038B2CF614A900BBCCAE /* AuthRepository.swift in Sources */, - 083A25902CF361F90099B58E /* TestDynamicCell.swift in Sources */, - 08DC61F32CF75037002A2F44 /* KeyChainService.swift in Sources */, - 086DD93B2D009A1C00B97D3B /* CancelableTagSection.swift in Sources */, - 4E755B232D2B9C5D00ADFB21 /* AdminViewController.swift in Sources */, - 08A2E49F2D1C417000102313 /* CommentListTitleSectionCell.swift in Sources */, - 086F89D52D1E6DB100CA4FC9 /* OtherUserCommentController.swift in Sources */, - 083C866E2D0ECB87003F441C /* InstaGuideChildSection.swift in Sources */, - 081898A32D2CC0110067BF01 /* WithdrawlReasonView.swift in Sources */, - 08B1917A2CF452B30057BC04 /* SignUpTermsView.swift in Sources */, - 08B191B82CF6092F0057BC04 /* AuthServiceable.swift in Sources */, - 0841BAB82CFAC41300049E31 /* SectionBackGroundDecorationView.swift in Sources */, - 086F89F52D2269E300CA4FC9 /* MyPageReactor.swift in Sources */, - 089952682D046CD80022AEF9 /* SearchResultReactor.swift in Sources */, - BDCA41C52CF35AC0005EECF6 /* TestViewController.swift in Sources */, - 086DD9402D01EEEB00B97D3B /* SearchCountTitleSection.swift in Sources */, - 083C864C2D0DCF9B003F441C /* AddCommentDescriptionSectionCell.swift in Sources */, - 086DD8CE2CFDFEB000B97D3B /* HomeListView.swift in Sources */, - 08B191C22CF615CA0057BC04 /* Secrets.swift in Sources */, - 083A258C2CF361F90099B58E /* ControllerConvention.swift in Sources */, - 083C86242D087A44003F441C /* DetailTitleSection.swift in Sources */, - 08A2E4822D1BCDEA00102313 /* CommentDetailController.swift in Sources */, - 0841BAA32CFA31A300049E31 /* SpacingSection.swift in Sources */, - 08B191982CF4A1010057BC04 /* SignUpStep4Reactor.swift in Sources */, - 086F8A0A2D2621EE00CA4FC9 /* MyPageMyCommentTitleSection.swift in Sources */, - 089952602D0366C40022AEF9 /* SearchMainController.swift in Sources */, - 081898D42D30F5840067BF01 /* CategoryEditModalController.swift in Sources */, - 088DE2562D144A830030FA9E /* DetailCommentSectionCell.swift in Sources */, - 08B1919E2CF4A7830057BC04 /* TagSectionCell.swift in Sources */, - 081899142D34CAEA0067BF01 /* GetNoticeDetailResponse.swift in Sources */, - 081898D22D30F57D0067BF01 /* CategoryEditModalView.swift in Sources */, - 083C860B2D073A15003F441C /* DetailView.swift in Sources */, - 086F89C52D1E347E00CA4FC9 /* CommentUserInfoController.swift in Sources */, - BD9103862CF614A900BBCCAE /* BannerPopUpStore.swift in Sources */, - 08B191862CF48A8B0057BC04 /* SignUpStep2Reactor.swift in Sources */, - 08DC62052CF8AC0E002A2F44 /* HomeController.swift in Sources */, - 4EA2C93D2D424D3300F4D97C /* MapDirectionRepository.swift in Sources */, - 083C86702D0ECB8E003F441C /* InstaGuideChildSectionCell.swift in Sources */, - 082197B02D4E4E190054094A /* NormalCommentEditView.swift in Sources */, - 086F89D02D1E60A100CA4FC9 /* PostUserBlockRequestDTO.swift in Sources */, - 081898CE2D30D5C60067BF01 /* InfoEditModalReactor.swift in Sources */, - 083C865D2D0DEFD5003F441C /* CommentCheckReactor.swift in Sources */, - BD9103632CF6149D00BBCCAE /* BannerPopUpStoreDTO.swift in Sources */, - 4EDE57032D5E70650014D924 /* LocationPermissionBottomSheet.swift in Sources */, - 081898D62D30F58A0067BF01 /* CategoryEditModalReactor.swift in Sources */, - 081898B32D2D20D70067BF01 /* WithdrawlCompleteView.swift in Sources */, - 086DD92A2D0086AA00B97D3B /* SearchController.swift in Sources */, - 083C860F2D073A23003F441C /* DetailReactor.swift in Sources */, - BD91038A2CF614A900BBCCAE /* LoginResponse.swift in Sources */, - 086F89F92D226EEB00CA4FC9 /* GetMyPageResponse.swift in Sources */, - 083A25BF2CF362770099B58E /* Logger.swift in Sources */, - 08DC61FC2CF76862002A2F44 /* SignUpCompleteReactor.swift in Sources */, - 08DE8A412D54DCCA0049BCAC /* MyCommentedPopUpGridSectionCell.swift in Sources */, - 0818992F2D3506290067BF01 /* FAQDropdownSection.swift in Sources */, - BD9103832CF614A900BBCCAE /* SignUpAPIUseCaseImpl.swift in Sources */, - 4E6CA4852D34D6ED0034D09A /* AdminBottomSheetReactor.swift in Sources */, - 4EE5A3D32D40E4A600A2469A /* MapGuideReactor.swift in Sources */, - 08DC62072CF8AC14002A2F44 /* HomeReactor.swift in Sources */, - 081898F72D33D6B70067BF01 /* BlockUserManageReactor.swift in Sources */, - 082197B22D4E4E200054094A /* NormalCommentEditController.swift in Sources */, - 081899502D363E5C0067BF01 /* BookMarkPopUpViewTypeModalView.swift in Sources */, - 081899452D35FEA10067BF01 /* RecentPopUpSection.swift in Sources */, - 083A259A2CF362090099B58E /* SectionDecorationItem.swift in Sources */, - BD9103892CF614A900BBCCAE /* PopUpStoreResponse.swift in Sources */, - 083A258F2CF361F90099B58E /* ReactorConvention.swift in Sources */, - 086F89C72D1E348400CA4FC9 /* CommentUserInfoReactor.swift in Sources */, - 0818990E2D34B68C0067BF01 /* GetNoticeListResponseDTO.swift in Sources */, - 086DD8C82CFDEA9200B97D3B /* UIView+.swift in Sources */, - 08A2E4842D1BCDEF00102313 /* CommentDetailReactor.swift in Sources */, - 4E685ED92D12CEB6001EF91C /* MapRepository.swift in Sources */, - 083A25B62CF362670099B58E /* MultipartEndPoint.swift in Sources */, - 0818993B2D35F1250067BF01 /* MyPageRecentController.swift in Sources */, - 089952462D031E740022AEF9 /* SearchSortedView.swift in Sources */, - 08DC620B2CF8AE0F002A2F44 /* ImageBannerSection.swift in Sources */, - 086DD9302D0090E900B97D3B /* UITextField+.swift in Sources */, - 0818991A2D34D6430067BF01 /* NoticeListSectionCell.swift in Sources */, - 086F89D72D1E6DB700CA4FC9 /* OtherUserCommentReactor.swift in Sources */, - 083C867A2D0EE3BB003F441C /* CommentAPIUseCaseImpl.swift in Sources */, - 4E7823A82D2E84E800AC5110 /* AdminRepository.swift in Sources */, - 086F89DB2D1E7A6C00CA4FC9 /* GetOtherUserCommentedPopUpListResponseDTO.swift in Sources */, - 086F89D32D1E6DA600CA4FC9 /* OtherUserCommentView.swift in Sources */, - 4E685ECE2D12CEB6001EF91C /* BalloonBackgroundView.swift in Sources */, - 4E755B292D2BA65A00ADFB21 /* AdminReactor.swift in Sources */, - 086DD8D62CFF182100B97D3B /* UserAPIEndPoint.swift in Sources */, - 081899372D35F1140067BF01 /* MyPageBookmarkReactor.swift in Sources */, - 4E685EE22D12CEB6001EF91C /* StoreListView.swift in Sources */, - 086F89C32D1E347700CA4FC9 /* CommentUserInfoView.swift in Sources */, - 08B191942CF4A0F00057BC04 /* SignUpStep4View.swift in Sources */, - 08B1918D2CF49FF70057BC04 /* SignUpStep3View.swift in Sources */, - 086F89CE2D1E42B500CA4FC9 /* CommentUserBlockReactor.swift in Sources */, - BD9103642CF6149D00BBCCAE /* GetHomeInfoResponseDTO.swift in Sources */, - 081898E22D338FA40067BF01 /* ListCountButtonSectionCell.swift in Sources */, - 08A2E47D2D1B06B000102313 /* ImageDetailReactor.swift in Sources */, - 08B191B42CF609260057BC04 /* KakaoLoginService.swift in Sources */, - BDCA41C12CF35AC0005EECF6 /* AppDelegate.swift in Sources */, - 083C863A2D0C7F0A003F441C /* CommentSelectedReactor.swift in Sources */, - 0841BAB42CFABED700049E31 /* HomePopularCardSection.swift in Sources */, - 081899182D34D63E0067BF01 /* NoticeListSection.swift in Sources */, - 083C86362D0C7EF4003F441C /* CommentSelectedController.swift in Sources */, - 08A2E4792D1B06A300102313 /* ImageDetailView.swift in Sources */, - 4E7823A92D2E84FB00AC5110 /* AdminStoreCell.swift in Sources */, - 088DE2542D144A7E0030FA9E /* DetailCommentSection.swift in Sources */, - 4E755B2B2D2BA76E00ADFB21 /* AdminUseCase.swift in Sources */, - 083C86292D088080003F441C /* DetailContentSection.swift in Sources */, - 08DC61FA2CF7684F002A2F44 /* SignUpCompleteController.swift in Sources */, - 083C864A2D0DCF96003F441C /* AddCommentDescriptionSection.swift in Sources */, - 4E9790C52D40E13500210499 /* MapGuideViewController.swift in Sources */, - 08B191A42CF5A7030057BC04 /* PPSegmentedControl.swift in Sources */, - 081898DC2D326DC10067BF01 /* IntroState.swift in Sources */, - 0899525C2D0347BD0022AEF9 /* SearchCategoryView.swift in Sources */, - 083C865B2D0DEFCF003F441C /* CommentCheckController.swift in Sources */, - 083C86382D0C7EFC003F441C /* CommentSelectedView.swift in Sources */, - 0818994A2D36322B0067BF01 /* PopUpCardSection.swift in Sources */, - 086F8A052D23CB3300CA4FC9 /* MyPageProfileSection.swift in Sources */, - 081899252D3500B80067BF01 /* FAQView.swift in Sources */, - 086F89EE2D2009F100CA4FC9 /* SubLoginReactor.swift in Sources */, - 08DC61F52CF765B5002A2F44 /* UserDefaultService.swift in Sources */, - 08A2E46C2D15BC5000102313 /* CommentLikeRequestDTO.swift in Sources */, - 086F8A072D23CB3800CA4FC9 /* MyPageProfileSectionCell.swift in Sources */, - 4E643FC12D738D7F0046AF29 /* PopUpStoreRegisterReactor.swift in Sources */, - 08B191AE2CF5BFA60057BC04 /* AgeSelectedView.swift in Sources */, - 4E685EE72D12CEB6001EF91C /* MapSearchInput.swift in Sources */, - 08A2E4912D1BF6EA00102313 /* CommentListController.swift in Sources */, - 0899524D2D033AA70022AEF9 /* GetOpenPopUpListResponseDTO.swift in Sources */, - 081898962D2965C90067BF01 /* ProfileEditView.swift in Sources */, - 083C86602D0EC496003F441C /* InstaCommentAddView.swift in Sources */, - 08DC62132CF8B833002A2F44 /* Optional+.swift in Sources */, - 081898F02D33A3A30067BF01 /* MyCommentSortedModalReactor.swift in Sources */, - 081898E42D3391550067BF01 /* GetMyCommentedPopUpResponseDTO.swift in Sources */, - 088DE25D2D145E3A0030FA9E /* DetailSimilarSection.swift in Sources */, - 4EEA1D912D352027003E7DE9 /* ExtendedImage.swift in Sources */, - 083C863D2D0C8BC4003F441C /* NormalCommentAddController.swift in Sources */, - 08B1916E2CF434CF0057BC04 /* SignUpStep1Reactor.swift in Sources */, - 08B1918F2CF4A0020057BC04 /* SignUpStep3Controller.swift in Sources */, - 08A2E4952D1C078300102313 /* GetPopUpCommentRequestDTO.swift in Sources */, - 08B191912CF4A00E0057BC04 /* SignUpStep3Reactor.swift in Sources */, - 083A259C2CF362090099B58E /* InOutputable.swift in Sources */, - 089952642D0366DA0022AEF9 /* SearchMainReactor.swift in Sources */, - 086F89E02D1E7CC700CA4FC9 /* GetOtherUserCommentedPopUpListResponse.swift in Sources */, - BD9103652CF6149D00BBCCAE /* HomeAPIEndpoint.swift in Sources */, - 086DD8E32CFF356300B97D3B /* HomeCardGridSection.swift in Sources */, - 0841BABE2CFB5AA600049E31 /* Date?+.swift in Sources */, - 083A258D2CF361F90099B58E /* ConventionCollectionViewCell.swift in Sources */, - 4E685EE12D12CEB6001EF91C /* StoreListReactor.swift in Sources */, - 4E685EE52D12CEB6001EF91C /* MapMarker.swift in Sources */, - 081898FB2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift in Sources */, - 08DC620D2CF8AE16002A2F44 /* ImageBannerSectionCell.swift in Sources */, - 081899412D35FDA10067BF01 /* GetRecentPopUpResponse.swift in Sources */, - 08B1915B2CF41E690057BC04 /* SignUpMainReactor.swift in Sources */, - 4E685ED62D12CEB6001EF91C /* FilterChipsView.swift in Sources */, - 08B191882CF48FAE0057BC04 /* NickNameState.swift in Sources */, - 08A2E48C2D1BDA8A00102313 /* CommentDetailContentSectionCell.swift in Sources */, - 081898982D2965D20067BF01 /* ProfileEditReactor.swift in Sources */, - 083C86562D0DD7EE003F441C /* AddCommentSectionCell.swift in Sources */, - 08B191B22CF5C0A60057BC04 /* PPPicker.swift in Sources */, - 086DD92E2D0086B900B97D3B /* SearchView.swift in Sources */, - 0841BA822CF9F5DF00049E31 /* PreSignedService.swift in Sources */, - 086DD92C2D0086B100B97D3B /* SearchReactor.swift in Sources */, - 08A2E4972D1C07F500102313 /* GetPopUpCommentResponseDTO.swift in Sources */, - BD226D512CF6DB290038C984 /* PPReturnHeaderView.swift in Sources */, - 08B1913F2CF367FA0057BC04 /* UIColor+.swift in Sources */, - 086F8A0C2D2621F400CA4FC9 /* MyPageMyCommentTitleSectionCell.swift in Sources */, - BD91036D2CF6149D00BBCCAE /* SignUpAPIEndpoint.swift in Sources */, - 08A2E4862D1BD85C00102313 /* CommentDetailImageSection.swift in Sources */, - 0818993D2D35F12A0067BF01 /* MyPageRecentReactor.swift in Sources */, - 0899526A2D046CDE0022AEF9 /* SearchResultView.swift in Sources */, - 081898B02D2CFCA40067BF01 /* GetWithdrawlListResponse.swift in Sources */, - 083C86692D0ECB47003F441C /* InstaGuideSection.swift in Sources */, - 081899392D35F11F0067BF01 /* MyPageRecentView.swift in Sources */, - 088DE24C2D12F33B0030FA9E /* DetailInfoSectionCell.swift in Sources */, - 083C86762D0EE2CF003F441C /* PostCommentRequestDTO.swift in Sources */, - 083C861E2D08737F003F441C /* GetPopUpDetailResponseDTO.swift in Sources */, - 086F8A0F2D26297900CA4FC9 /* MyPageCommentSection.swift in Sources */, - 08B1916C2CF434C30057BC04 /* SignUpStep1View.swift in Sources */, - 083A25BC2CF362670099B58E /* ProviderImpl.swift in Sources */, - 4EEA1D8F2D352012003E7DE9 /* ImageCell.swift in Sources */, - 086DD93D2D009A2600B97D3B /* CancelableTagSectionCell.swift in Sources */, - 4E9C127A2D2BC811006744D6 /* AdminBottomSheetViewController.swift in Sources */, - 086DD8DA2CFF194700B97D3B /* UserAPIRepositoryImpl.swift in Sources */, - 4E685ECF2D12CEB6001EF91C /* BalloonChipCell.swift in Sources */, - 083A25CA2CF363C60099B58E /* LoginReactor.swift in Sources */, - 081898BE2D2E5F590067BF01 /* MyCommentController.swift in Sources */, - 083A25BA2CF362670099B58E /* TokenInterceptor.swift in Sources */, - 0841BAC32CFB600800049E31 /* TabbarController.swift in Sources */, - 089952442D031E6D0022AEF9 /* SearchSortedReactor.swift in Sources */, - 086F89E42D1FE91300CA4FC9 /* OtherUserCommentSection.swift in Sources */, - 083C86782D0EE382003F441C /* CommentAPIRepository.swift in Sources */, - 08B191B02CF5BFAE0057BC04 /* AgeSelectedReactor.swift in Sources */, - 4E685EDA2D12CEB6001EF91C /* MapUseCase.swift in Sources */, - 083C863F2D0C8BCE003F441C /* NormalCommentAddView.swift in Sources */, - 4EA9989A2D21C2FC009DC30B /* StoreListSection.swift in Sources */, - 083C86452D0DCDE9003F441C /* AddCommentTitleSectionCell.swift in Sources */, - 083C86412D0C8BD8003F441C /* NormalCommentAddReactor.swift in Sources */, - 089952732D0475E90022AEF9 /* SearchResultCountSection.swift in Sources */, - 08CBEA062D38991600248007 /* PostTokenReissueResponse.swift in Sources */, - 4EED9BAC2D22730400B288E7 /* FilterType.swift in Sources */, - 08B191A02CF4AA0E0057BC04 /* Reactive+.swift in Sources */, - 4E685ED32D12CEB6001EF91C /* FilterBottomSheetViewController.swift in Sources */, - 089952512D033C410022AEF9 /* GetSearchBottomPopUpListResponse.swift in Sources */, - 0841BABA2CFAE5BE00049E31 /* HomeHeaderView.swift in Sources */, - 081898E82D3392480067BF01 /* GetMyCommentRequestDTO.swift in Sources */, - 0841BAAC2CFA35F300049E31 /* UILabel+.swift in Sources */, - 081898F52D33D6B10067BF01 /* BlockUserManageController.swift in Sources */, - 08CBEA3E2D3FF6A100248007 /* PopUpCardView.swift in Sources */, - 086F89F32D2269DE00CA4FC9 /* MyPageController.swift in Sources */, - 0818994C2D3632320067BF01 /* PopUpCardSectionCell.swift in Sources */, - BD91036A2CF6149D00BBCCAE /* SignUpRepositoryImpl.swift in Sources */, - 081898EC2D33A3960067BF01 /* MyCommentSortedModalView.swift in Sources */, - BD91036C2CF6149D00BBCCAE /* GetCategoryListResponseDTO.swift in Sources */, - 086F8A182D265C5F00CA4FC9 /* MyPageListSection.swift in Sources */, - 086F8A1A2D265C6300CA4FC9 /* MyPageListSectionCell.swift in Sources */, - 081899272D3500BF0067BF01 /* FAQController.swift in Sources */, - 08B191672CF432220057BC04 /* PPCancelHeaderView.swift in Sources */, - BD9103812CF614A900BBCCAE /* HomeAPIUseCaseImpl.swift in Sources */, - 08DC62112CF8B446002A2F44 /* SortedRequestDTO.swift in Sources */, - 4EAB809F2D3F8EF50041AF30 /* ViewportBounds.swift in Sources */, - 08B191712CF4398D0057BC04 /* PPLabel.swift in Sources */, - 4E685ED22D12CEB6001EF91C /* FilterBottomSheetView.swift in Sources */, - 0899524F2D033B5A0022AEF9 /* GetSearchPopUpListRequestDTO.swift in Sources */, - 0899525A2D0347B40022AEF9 /* SearchCategoryReactor.swift in Sources */, - 083A25B92CF362670099B58E /* FormDataInterceptor.swift in Sources */, - 4E685EE42D12CEB6001EF91C /* MapFilterChips.swift in Sources */, - BD91036E2CF6149D00BBCCAE /* SignUpRequestDTO.swift in Sources */, - 08B191632CF430F30057BC04 /* PPProgressIndicator.swift in Sources */, - 081898942D2965C20067BF01 /* ProfileEditController.swift in Sources */, - 0818992D2D3506240067BF01 /* FAQDropdownSectionCell.swift in Sources */, - 08B191742CF43DF40057BC04 /* SignUpCheckBoxButton.swift in Sources */, - 086DD8D32CFDFF1500B97D3B /* HomePopUpType.swift in Sources */, - 4E643FC32D738D930046AF29 /* PopUpStoreRegisterView.swift in Sources */, - 081898CC2D30D5BF0067BF01 /* InfoEditModalController.swift in Sources */, - 08DE8A0D2D5236840049BCAC /* PutCommentRequestDTO.swift in Sources */, - 4E685EE92D12CEB6001EF91C /* MapView.swift in Sources */, - 4E755B212D2B9BAB00ADFB21 /* AdminResponseDTO.swift in Sources */, - 08B191382CF366680057BC04 /* UICollectionReusableView+.swift in Sources */, - 4E685EE02D12CEB6001EF91C /* StoreListCell.swift in Sources */, - 4EDDEFB42D2D285900CFAFA5 /* DateTimePickerManager.swift in Sources */, - 08B1915D2CF41E6F0057BC04 /* SignUpMainView.swift in Sources */, - 0841BAA52CFA31A900049E31 /* SpacingSectionCell.swift in Sources */, - 4E685EDF2D12CEB6001EF91C /* PopupCardCell.swift in Sources */, - 083A25B52CF362670099B58E /* Endpoint.swift in Sources */, - 081898A52D2CC0180067BF01 /* WithdrawlReasonController.swift in Sources */, - BDCA41C32CF35AC0005EECF6 /* SceneDelegate.swift in Sources */, - 08B1917F2CF46DF20057BC04 /* TermsDetailView.swift in Sources */, - 4E6A06702D42A96100B2A658 /* FullScreenMapViewController.swift in Sources */, - 4E685EDD2D12CEB6001EF91C /* MapPopUpStoreDTO.swift in Sources */, - 08B191412CF367FF0057BC04 /* UIFont+.swift in Sources */, - 0841BA8E2CF9F8A100049E31 /* ImageBannerChildSection.swift in Sources */, - 083C860D2D073A1C003F441C /* DetailController.swift in Sources */, - 08A2E4932D1BF6EF00102313 /* CommentListReactor.swift in Sources */, - 088DE2582D144B0F0030FA9E /* DetailCommentProfileView.swift in Sources */, - 0818989E2D2BAA610067BF01 /* WithdrawlCheckModalController.swift in Sources */, - 081898902D295DC80067BF01 /* MyPageLogoutSectionCell.swift in Sources */, - 081898EE2D33A39D0067BF01 /* MyCommentSortedModalController.swift in Sources */, - 081898AA2D2CEA2F0067BF01 /* WithdrawlCheckSectionCell.swift in Sources */, - 086DD8CC2CFDFEA800B97D3B /* HomeListController.swift in Sources */, - 083A258E2CF361F90099B58E /* ConventionTableViewCell.swift in Sources */, - 086DD9342D00962500B97D3B /* SearchTitleSectionCell.swift in Sources */, - 08B191592CF41E610057BC04 /* SignUpMainController.swift in Sources */, - 0899526C2D0473EC0022AEF9 /* GetSearchPopUpListResponseDTO.swift in Sources */, - 081898B52D2D20E30067BF01 /* WithdrawlCompleteController.swift in Sources */, - 086F89F12D2269D800CA4FC9 /* MyPageView.swift in Sources */, - 081898F32D33D6AC0067BF01 /* BlockUserManageView.swift in Sources */, - 08A2E48A2D1BDA8400102313 /* CommentDetailContentSection.swift in Sources */, - 4E685EDE2D12CEB6001EF91C /* MapPopupCarouselView.swift in Sources */, - 08B1917D2CF46DE30057BC04 /* TermsDetailController.swift in Sources */, - 08CBEA0B2D38DBD600248007 /* LastLoginView.swift in Sources */, - 4E8AA29D2D59A2340029DF75 /* MarkerTooltipView.swift in Sources */, - 0841BAB12CFA38F500049E31 /* HomeCardSectionCell.swift in Sources */, - 4E685EE32D12CEB6001EF91C /* StoreListViewController.swift in Sources */, - 0818990A2D34B3620067BF01 /* MyPageNoticeController.swift in Sources */, - 083C862B2D08808C003F441C /* DetailContentSectionCell.swift in Sources */, - BD91036B2CF6149D00BBCCAE /* CheckNickNameRequestDTO.swift in Sources */, - 0841BAAF2CFA38EA00049E31 /* HomeCardSection.swift in Sources */, - 081898A02D2BAA670067BF01 /* WithdrawlCheckModalReactor.swift in Sources */, - 08B191842CF48A820057BC04 /* SignUpStep2View.swift in Sources */, - 0841BA882CF9F62400049E31 /* PresignedURLRequestDTO.swift in Sources */, - 0841BA8C2CF9F67100049E31 /* PreSignedAPIEndPoint.swift in Sources */, - 08DE8A1B2D5261DE0049BCAC /* MyPageTermsController.swift in Sources */, - 4E78706F2D37CB2200465FC9 /* PopUpStoreRegisterViewController.swift in Sources */, - 081899082D34B35A0067BF01 /* MyPageNoticeView.swift in Sources */, - 4E78706E2D37CB1900465FC9 /* ProfileEditListButton.swift in Sources */, - 081898D82D310C160067BF01 /* PutUserCategoryRequestDTO.swift in Sources */, - 08A2E4992D1C08D600102313 /* GetPopUpCommentResponse.swift in Sources */, - 08CBEA3C2D3FABED00248007 /* BookMarkToastView.swift in Sources */, - 0818988E2D295DC30067BF01 /* MyPageLogoutSection.swift in Sources */, - 089952552D033D480022AEF9 /* PopUpAPIUseCaseImpl.swift in Sources */, - BD9103662CF6149D00BBCCAE /* PopUpStoreResponseDTO.swift in Sources */, - 0841BA802CF9F34100049E31 /* ImageBannerChildSectionCell.swift in Sources */, - 4EA2C93F2D424D7400F4D97C /* MapDirectionUseCase.swift in Sources */, - 088DE2512D13019E0030FA9E /* DetailCommentTitleSectionCell.swift in Sources */, - 083C86592D0DEFC3003F441C /* CommentCheckView.swift in Sources */, - 082197AB2D4E3EEF0054094A /* CommentMyMenuReactor.swift in Sources */, - 083C86732D0EE2B1003F441C /* CommentAPIEndPoint.swift in Sources */, - 081898AC2D2CEA940067BF01 /* WithdrawlCheckSection.swift in Sources */, - 08B1913B2CF366A00057BC04 /* UIApplication+.swift in Sources */, - 083C86542D0DD7E9003F441C /* AddCommentSection.swift in Sources */, - 081898E02D338F9C0067BF01 /* ListCountButtonSection.swift in Sources */, - 4E685EE62D12CEB6001EF91C /* MapReactor.swift in Sources */, - 083A25BB2CF362670099B58E /* Provider.swift in Sources */, - 0899524B2D033A9C0022AEF9 /* GetClosePopUpListResponseDTO.swift in Sources */, - 0818990C2D34B3670067BF01 /* MyPageNoticeReactor.swift in Sources */, - 08B191A72CF5A9430057BC04 /* AgeSelectedButton.swift in Sources */, - 081898CA2D30D5BA0067BF01 /* InfoEditModalView.swift in Sources */, - 081899102D34B7240067BF01 /* GetNoticeListResponse.swift in Sources */, - 089952622D0366D30022AEF9 /* SearchMainView.swift in Sources */, - BD9103872CF614A900BBCCAE /* Category.swift in Sources */, - 083A25822CF361EF0099B58E /* BaseTabmanController.swift in Sources */, - 088DE25F2D145E3F0030FA9E /* DetailSimilarSectionCell.swift in Sources */, - 082197AD2D4E49370054094A /* DeleteCommentRequestDTO.swift in Sources */, - 082197B42D4E4E280054094A /* NormalCommentEditReactor.swift in Sources */, - 081898FF2D33DA440067BF01 /* GetBlockUserListRequestDTO.swift in Sources */, - BD9103852CF614A900BBCCAE /* AuthAPIUseCaseImpl.swift in Sources */, - 08CBEA032D38989E00248007 /* PostTokenReissueResponseDTO.swift in Sources */, - 08DE8A1D2D5261E70049BCAC /* MyPageTermsReactor.swift in Sources */, - 08B191762CF440C40057BC04 /* PPButton.swift in Sources */, - 083A25CC2CF363CB0099B58E /* LoginView.swift in Sources */, - 083A259B2CF362090099B58E /* SectionSupplementaryItem.swift in Sources */, - 4EA2C9412D424D8400F4D97C /* GetPopUpDirectionResponseDTO.swift in Sources */, - 086F89CA2D1E42A700CA4FC9 /* CommentUserBlockView.swift in Sources */, - 086DD8D02CFDFEB900B97D3B /* HomeListReactor.swift in Sources */, - 081899202D34DF880067BF01 /* MyPageNoticeDetailController.swift in Sources */, - 4EAB809D2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift in Sources */, - 083C86472D0DCDFB003F441C /* AddCommentTitleSection.swift in Sources */, - 0841BAB62CFABEDC00049E31 /* HomePopularCardSectionCell.swift in Sources */, - 082197A92D4E3EE90054094A /* CommentMyMenuController.swift in Sources */, - 08B191822CF48A7B0057BC04 /* SignUpStep2Controller.swift in Sources */, - 086F8A112D26297D00CA4FC9 /* MyPageCommentSectionCell.swift in Sources */, - 4E685ED12D12CEB6001EF91C /* FilterBottomSheetReactor.swift in Sources */, - 4E6C07082D4B6E74008A962A /* ClusteringModels.swift in Sources */, - 08A2E49D2D1C416800102313 /* CommentListTitleSection.swift in Sources */, - 0818989C2D2BAA570067BF01 /* WithdrawlCheckModalView.swift in Sources */, - 081899122D34CA9E0067BF01 /* GetNoticeDetailResponseDTO.swift in Sources */, - 083A25912CF361F90099B58E /* TestDynamicSection.swift in Sources */, - 086DD8D82CFF185200B97D3B /* PostBookmarkPopUpRequestDTO.swift in Sources */, - 089952422D031E650022AEF9 /* SearchSortedController.swift in Sources */, - 089952532D033C940022AEF9 /* PopUpAPIRepositoryImpl.swift in Sources */, - 4E755B252D2B9C6C00ADFB21 /* AdminView.swift in Sources */, - 4E685ED42D12CEB6001EF91C /* FilterCell.swift in Sources */, - 088DE24F2D13019A0030FA9E /* DetailCommentTitleSection.swift in Sources */, - 081898B72D2D23A90067BF01 /* UINavigationController+.swift in Sources */, - 08B191A22CF4AE890057BC04 /* ToastMaker.swift in Sources */, - 082197A72D4E3EE00054094A /* CommentMyMenuView.swift in Sources */, - 086F89F72D226DF600CA4FC9 /* GetMyPageResponseDTO.swift in Sources */, - 083C86512D0DD3AB003F441C /* AddCommentImageSectionCell.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BDCA41CF2CF35AC1005EECF6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BDCA41D82CF35AC1005EECF6 /* PoppoolTests.swift in Sources */, - 4EDE57012D5E6A5F0014D924 /* MapViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - BDCA41D92CF35AC1005EECF6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BDCA41E42CF35AC1005EECF6 /* PoppoolUITestsLaunchTests.swift in Sources */, - 4E9A465E2D50B2DB0010578A /* AdminStoreCell.swift in Sources */, - BDCA41E22CF35AC1005EECF6 /* PoppoolUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - BDCA41D52CF35AC1005EECF6 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BDCA41BC2CF35AC0005EECF6 /* Poppool */; - targetProxy = BDCA41D42CF35AC1005EECF6 /* PBXContainerItemProxy */; - }; - BDCA41DF2CF35AC1005EECF6 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BDCA41BC2CF35AC0005EECF6 /* Poppool */; - targetProxy = BDCA41DE2CF35AC1005EECF6 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - BDCA41CB2CF35AC1005EECF6 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - BDCA41CC2CF35AC1005EECF6 /* Base */, - 08DE8A162D525A9B0049BCAC /* ko */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ BDCA41E52CF35AC2005EECF6 /* Debug */ = { isa = XCBuildConfiguration; @@ -3824,6 +391,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Distribution"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -3849,15 +417,19 @@ }; BDCA41E82CF35AC2005EECF6 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 058789AD2DAD2E10004F81E2 /* Poppool */; + baseConfigurationReferenceRelativePath = Resource/Debug.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Poppool/Poppool.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = Poppool/Resource/Poppool.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Poppool/Resource/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "사용자가 프로필 사진을 업로드하거나 댓글에 사진을 추가할 수 있도록 카메라를 사용합니다."; @@ -3874,11 +446,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = poppoolProvisioningProfile; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = PoppoolGitHubAction; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3886,20 +458,25 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; BDCA41E92CF35AC2005EECF6 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 058789AD2DAD2E10004F81E2 /* Poppool */; + baseConfigurationReferenceRelativePath = Resource/Debug.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Poppool/Poppool.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = Poppool/Resource/Poppool.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Poppool/Resource/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "사용자가 프로필 사진을 업로드하거나 댓글에 사진을 추가할 수 있도록 카메라를 사용합니다."; @@ -3916,11 +493,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = poppoolProvisioningProfile; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = PoppoolGitHubAction; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3928,82 +505,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; - BDCA41EB2CF35AC2005EECF6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2U86LHQK8Q; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.poppool.PoppoolTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Poppool.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Poppool"; - }; - name = Debug; - }; - BDCA41EC2CF35AC2005EECF6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2U86LHQK8Q; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.poppool.PoppoolTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Poppool.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Poppool"; - }; - name = Release; - }; - BDCA41EE2CF35AC2005EECF6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2U86LHQK8Q; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.poppool.PoppoolUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Poppool; - }; - name = Debug; - }; - BDCA41EF2CF35AC2005EECF6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2U86LHQK8Q; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.poppool.PoppoolUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Poppool; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -4028,260 +530,158 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - BDCA41EA2CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "PoppoolTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - BDCA41EB2CF35AC2005EECF6 /* Debug */, - BDCA41EC2CF35AC2005EECF6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - BDCA41ED2CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "PoppoolUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - BDCA41EE2CF35AC2005EECF6 /* Debug */, - BDCA41EF2CF35AC2005EECF6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 0821979F2D426DCB0054094A /* XCRemoteSwiftPackageReference "Then" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/devxoul/Then"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.0.0; - }; - }; - 083A25CE2CF364B70099B58E /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.10.1; - }; - }; - 088DE2422D104EE70030FA9E /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/scinfu/SwiftSoup"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.7.6; - }; - }; - 088DE2452D12DB5C0030FA9E /* XCRemoteSwiftPackageReference "ios-maps-sdk" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/googlemaps/ios-maps-sdk"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 9.2.0; - }; - }; - 4E5825652D1951DF00EE83EF /* XCRemoteSwiftPackageReference "FloatingPanel" */ = { + 0522C3C42DB67D7800B141FF /* XCRemoteSwiftPackageReference "RxGesture" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/scenee/FloatingPanel.git"; + repositoryURL = "https://github.com/RxSwiftCommunity/RxGesture"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.8.6; - }; - }; - 4EA9989B2D21C404009DC30B /* XCRemoteSwiftPackageReference "RxDataSources" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/RxSwiftCommunity/RxDataSources.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.0.2; - }; - }; - BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SnapKit/SnapKit.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.7.1; + minimumVersion = 4.0.4; }; }; - BDCA41F32CF35D33005EECF6 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + 05734C612DCE04FA0093825D /* XCRemoteSwiftPackageReference "Pageboy" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + repositoryURL = "https://github.com/uias/Pageboy"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 8.1.1; + minimumVersion = 4.2.0; }; }; - BDCA41F62CF35D9A005EECF6 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + 05734C642DCE05070093825D /* XCRemoteSwiftPackageReference "PanModal" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; + repositoryURL = "https://github.com/slackhq/PanModal"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 6.8.0; + minimumVersion = 1.2.7; }; }; - BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */ = { + 05734C692DCE05550093825D /* XCRemoteSwiftPackageReference "ReactorKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kakao/kakao-ios-sdk-rx"; + repositoryURL = "https://github.com/ReactorKit/ReactorKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.23.0; + minimumVersion = 3.2.0; }; }; - BDCA41FC2CF35EE7005EECF6 /* XCRemoteSwiftPackageReference "ReactorKit" */ = { + 05734C6C2DCE05680093825D /* XCRemoteSwiftPackageReference "Tabman" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ReactorKit/ReactorKit.git"; + repositoryURL = "https://github.com/uias/Tabman"; requirement = { kind = upToNextMajorVersion; minimumVersion = 3.2.0; }; }; - BDCA41FF2CF35EFE005EECF6 /* XCRemoteSwiftPackageReference "RxKeyboard" */ = { + 05734C6F2DCE059D0093825D /* XCRemoteSwiftPackageReference "Then" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/RxSwiftCommunity/RxKeyboard.git"; + repositoryURL = "https://github.com/devxoul/Then"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.1; + minimumVersion = 3.0.0; }; }; - BDCA42022CF35F76005EECF6 /* XCRemoteSwiftPackageReference "PanModal" */ = { + 05BDD5C32DB674E500C1E192 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/slackhq/PanModal.git"; + repositoryURL = "https://github.com/SnapKit/SnapKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.7; + minimumVersion = 5.7.1; }; }; - BDCA42052CF35FA6005EECF6 /* XCRemoteSwiftPackageReference "Tabman" */ = { + 05BDD5D62DB6783C00C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uias/Tabman.git"; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 3.2.0; + minimumVersion = 6.9.0; }; }; - BDCA42082CF35FB1005EECF6 /* XCRemoteSwiftPackageReference "Pageboy" */ = { + 08F403312D884F4D00BFA61A /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uias/Pageboy.git"; + repositoryURL = "https://github.com/kakao/kakao-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.2.0; + minimumVersion = 2.24.0; }; }; - BDCA420B2CF35FD2005EECF6 /* XCRemoteSwiftPackageReference "RxGesture" */ = { + 4E1514282D99480200DFD08F /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/RxSwiftCommunity/RxGesture.git"; + repositoryURL = "https://github.com/navermaps/SPM-NMapsMap"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.0.4; + minimumVersion = 3.21.0; }; }; - BDCA420E2CF35FF5005EECF6 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { + 4EE360FB2D91876300D2441D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/airbnb/lottie-spm.git"; + repositoryURL = "https://github.com/navermaps/SPM-NMapsMap"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.5.0; + minimumVersion = 3.21.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 082197A02D426DCB0054094A /* Then */ = { - isa = XCSwiftPackageProductDependency; - package = 0821979F2D426DCB0054094A /* XCRemoteSwiftPackageReference "Then" */; - productName = Then; - }; - 083A25CF2CF364B70099B58E /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = 083A25CE2CF364B70099B58E /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - 088DE2432D104EE70030FA9E /* SwiftSoup */ = { - isa = XCSwiftPackageProductDependency; - package = 088DE2422D104EE70030FA9E /* XCRemoteSwiftPackageReference "SwiftSoup" */; - productName = SwiftSoup; - }; - 088DE2462D12DB5C0030FA9E /* GoogleMaps */ = { - isa = XCSwiftPackageProductDependency; - package = 088DE2452D12DB5C0030FA9E /* XCRemoteSwiftPackageReference "ios-maps-sdk" */; - productName = GoogleMaps; - }; - 08B191B92CF609AE0057BC04 /* RxKakaoSDK */ = { - isa = XCSwiftPackageProductDependency; - package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; - productName = RxKakaoSDK; - }; - 08B191BB2CF609AE0057BC04 /* RxKakaoSDKAuth */ = { + 0522C1D82DB67C5900B141FF /* RxSwift */ = { isa = XCSwiftPackageProductDependency; - package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; - productName = RxKakaoSDKAuth; + package = 05BDD5D62DB6783C00C1E192 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; }; - 08B191BD2CF609AE0057BC04 /* RxKakaoSDKUser */ = { + 0522C3C52DB67D7800B141FF /* RxGesture */ = { isa = XCSwiftPackageProductDependency; - package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; - productName = RxKakaoSDKUser; + package = 0522C3C42DB67D7800B141FF /* XCRemoteSwiftPackageReference "RxGesture" */; + productName = RxGesture; }; - 4E5825662D1951DF00EE83EF /* FloatingPanel */ = { + 05734C622DCE04FA0093825D /* Pageboy */ = { isa = XCSwiftPackageProductDependency; - package = 4E5825652D1951DF00EE83EF /* XCRemoteSwiftPackageReference "FloatingPanel" */; - productName = FloatingPanel; + package = 05734C612DCE04FA0093825D /* XCRemoteSwiftPackageReference "Pageboy" */; + productName = Pageboy; }; - 4EA9989C2D21C404009DC30B /* RxDataSources */ = { + 05734C652DCE05070093825D /* PanModal */ = { isa = XCSwiftPackageProductDependency; - package = 4EA9989B2D21C404009DC30B /* XCRemoteSwiftPackageReference "RxDataSources" */; - productName = RxDataSources; + package = 05734C642DCE05070093825D /* XCRemoteSwiftPackageReference "PanModal" */; + productName = PanModal; }; - BDCA41F12CF35D0D005EECF6 /* SnapKit */ = { + 05734C672DCE05240093825D /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */; + package = 05BDD5C32DB674E500C1E192 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; - BDCA41F42CF35D33005EECF6 /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = BDCA41F32CF35D33005EECF6 /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; - BDCA41F72CF35D9A005EECF6 /* RxSwift */ = { - isa = XCSwiftPackageProductDependency; - package = BDCA41F62CF35D9A005EECF6 /* XCRemoteSwiftPackageReference "RxSwift" */; - productName = RxSwift; - }; - BDCA41FD2CF35EE7005EECF6 /* ReactorKit */ = { + 05734C6A2DCE05550093825D /* ReactorKit */ = { isa = XCSwiftPackageProductDependency; - package = BDCA41FC2CF35EE7005EECF6 /* XCRemoteSwiftPackageReference "ReactorKit" */; + package = 05734C692DCE05550093825D /* XCRemoteSwiftPackageReference "ReactorKit" */; productName = ReactorKit; }; - BDCA42002CF35EFE005EECF6 /* RxKeyboard */ = { + 05734C6D2DCE05680093825D /* Tabman */ = { isa = XCSwiftPackageProductDependency; - package = BDCA41FF2CF35EFE005EECF6 /* XCRemoteSwiftPackageReference "RxKeyboard" */; - productName = RxKeyboard; + package = 05734C6C2DCE05680093825D /* XCRemoteSwiftPackageReference "Tabman" */; + productName = Tabman; }; - BDCA42032CF35F76005EECF6 /* PanModal */ = { + 05734C702DCE059D0093825D /* Then */ = { isa = XCSwiftPackageProductDependency; - package = BDCA42022CF35F76005EECF6 /* XCRemoteSwiftPackageReference "PanModal" */; - productName = PanModal; + package = 05734C6F2DCE059D0093825D /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; }; - BDCA42062CF35FA6005EECF6 /* Tabman */ = { + 05BBA73D2DB75DA60047A061 /* KakaoSDKUser */ = { isa = XCSwiftPackageProductDependency; - package = BDCA42052CF35FA6005EECF6 /* XCRemoteSwiftPackageReference "Tabman" */; - productName = Tabman; + package = 08F403312D884F4D00BFA61A /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKUser; }; - BDCA42092CF35FB1005EECF6 /* Pageboy */ = { + 4E1514292D99480200DFD08F /* NMapsMap */ = { isa = XCSwiftPackageProductDependency; - package = BDCA42082CF35FB1005EECF6 /* XCRemoteSwiftPackageReference "Pageboy" */; - productName = Pageboy; + package = 4E1514282D99480200DFD08F /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */; + productName = NMapsMap; }; - BDCA420C2CF35FD2005EECF6 /* RxGesture */ = { + 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */ = { isa = XCSwiftPackageProductDependency; - package = BDCA420B2CF35FD2005EECF6 /* XCRemoteSwiftPackageReference "RxGesture" */; - productName = RxGesture; + productName = KakaoSDKAuth; }; - BDCA420F2CF35FF5005EECF6 /* Lottie */ = { + 4EE360FC2D91876300D2441D /* NMapsMap */ = { isa = XCSwiftPackageProductDependency; - package = BDCA420E2CF35FF5005EECF6 /* XCRemoteSwiftPackageReference "lottie-spm" */; - productName = Lottie; + package = 4EE360FB2D91876300D2441D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */; + productName = NMapsMap; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Poppool/Poppool.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Poppool/Poppool.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 717ac91b..810c5679 100644 --- a/Poppool/Poppool.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Poppool/Poppool.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9287fcb2d41c7fcf69aee03103ff9eac9ec5b01fe72ca36a57109537b58b6a8d", + "originHash" : "893e9d5956a6d674d140654334c58c276c99001321206803c07c48415f2e6ac3", "pins" : [ { "identity" : "alamofire", @@ -19,40 +19,13 @@ "version" : "2.8.6" } }, - { - "identity" : "ios-maps-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googlemaps/ios-maps-sdk", - "state" : { - "revision" : "9fa352d6eca4a731949efcdb27ed851f1fcd4447", - "version" : "9.3.0" - } - }, { "identity" : "kakao-ios-sdk", "kind" : "remoteSourceControl", "location" : "https://github.com/kakao/kakao-ios-sdk.git", "state" : { - "revision" : "ab4309c1950550add307046ad1e08024c7514603", - "version" : "2.23.0" - } - }, - { - "identity" : "kakao-ios-sdk-rx", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kakao/kakao-ios-sdk-rx", - "state" : { - "revision" : "fa5ce05d610c4b026df8d42e891a32f31a239d58", - "version" : "2.23.0" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "3db26ab625d194c38e68c1a40e43d1bc12743fe0", - "version" : "8.2.0" + "revision" : "bfe2fe42f730ccfe59e85f6e9eda2f4578e9a307", + "version" : "2.24.0" } }, { @@ -64,15 +37,6 @@ "version" : "4.5.1" } }, - { - "identity" : "ohhttpstubs", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", - "state" : { - "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version" : "9.1.0" - } - }, { "identity" : "pageboy", "kind" : "remoteSourceControl", @@ -100,15 +64,6 @@ "version" : "3.2.0" } }, - { - "identity" : "rxalamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/RxSwiftCommunity/RxAlamofire.git", - "state" : { - "revision" : "9535b58695b91fb67f56d58d6fd0c76462d7743a", - "version" : "6.1.2" - } - }, { "identity" : "rxdatasources", "kind" : "remoteSourceControl", @@ -154,13 +109,31 @@ "version" : "5.7.1" } }, + { + "identity" : "spm-nmapsgeometry", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsGeometry.git", + "state" : { + "revision" : "436d5e2e684f557faf5ef5862fd6633a42d7af11", + "version" : "1.0.2" + } + }, + { + "identity" : "spm-nmapsmap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsMap", + "state" : { + "revision" : "ad89e53fdfec3b8d8994280fb0414b5a7b1c3e8e", + "version" : "3.21.0" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup", "state" : { - "revision" : "18ad8b8ff0f03f3c0a5544ffccfa2ea1c051ae6e", - "version" : "2.8.0" + "revision" : "bba848db50462894e7fc0891d018dfecad4ef11e", + "version" : "2.8.7" } }, { diff --git a/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme b/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme index 8971a232..f14947f9 100644 --- a/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme +++ b/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme @@ -1,6 +1,6 @@ + allowLocationSimulation = "YES" + consoleMode = "0" + structuredConsoleMode = "1"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Poppool/Poppool.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Poppool/Poppool.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..08de0be8 --- /dev/null +++ b/Poppool/Poppool.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/Poppool/Poppool.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Poppool/Poppool.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..7bcd7a79 --- /dev/null +++ b/Poppool/Poppool.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,159 @@ +{ + "originHash" : "e3308f4d8d004a111784c1f182de9af75f8c85dd7807e55541802130c4c227fb", + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" + } + }, + { + "identity" : "floatingpanel", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scenee/FloatingPanel.git", + "state" : { + "revision" : "a1f20cedb14bd1ddc63e30a8dd10c85e8f1fa011", + "version" : "2.8.7" + } + }, + { + "identity" : "kakao-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kakao/kakao-ios-sdk.git", + "state" : { + "revision" : "2c780b70d4197be3f72deee2fd82ea3e3b767007", + "version" : "2.24.1" + } + }, + { + "identity" : "lottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/airbnb/lottie-ios", + "state" : { + "revision" : "047aa81b77adcbf583a966dfef620d17650cc656", + "version" : "4.5.1" + } + }, + { + "identity" : "pageboy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Pageboy.git", + "state" : { + "revision" : "be0c1f6f1964cfb07f9d819b0863f2c3f255f612", + "version" : "4.2.0" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/slackhq/PanModal.git", + "state" : { + "revision" : "b012aecb6b67a8e46369227f893c12544846613f", + "version" : "1.2.7" + } + }, + { + "identity" : "reactorkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactorKit/ReactorKit.git", + "state" : { + "revision" : "8fa33f09c6f6621a2aa536d739956d53b84dd139", + "version" : "3.2.0" + } + }, + { + "identity" : "rxdatasources", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RxSwiftCommunity/RxDataSources.git", + "state" : { + "revision" : "90c29b48b628479097fe775ed1966d75ac374518", + "version" : "5.0.2" + } + }, + { + "identity" : "rxgesture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RxSwiftCommunity/RxGesture.git", + "state" : { + "revision" : "1b137c576b4aaaab949235752278956697c9e4a0", + "version" : "4.0.4" + } + }, + { + "identity" : "rxkeyboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RxSwiftCommunity/RxKeyboard.git", + "state" : { + "revision" : "63f6377975c962a1d89f012a6f1e5bebb2c502b7", + "version" : "2.0.1" + } + }, + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift.git", + "state" : { + "revision" : "5dd1907d64f0d36f158f61a466bab75067224893", + "version" : "6.9.0" + } + }, + { + "identity" : "snapkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SnapKit/SnapKit.git", + "state" : { + "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", + "version" : "5.7.1" + } + }, + { + "identity" : "spm-nmapsgeometry", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsGeometry.git", + "state" : { + "revision" : "436d5e2e684f557faf5ef5862fd6633a42d7af11", + "version" : "1.0.2" + } + }, + { + "identity" : "spm-nmapsmap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsMap", + "state" : { + "revision" : "ad89e53fdfec3b8d8994280fb0414b5a7b1c3e8e", + "version" : "3.21.0" + } + }, + { + "identity" : "tabman", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uias/Tabman.git", + "state" : { + "revision" : "3b2213290eb93e55bb50b49d1a179033005c11ab", + "version" : "3.2.0" + } + }, + { + "identity" : "then", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devxoul/Then", + "state" : { + "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", + "version" : "3.0.0" + } + }, + { + "identity" : "weakmaptable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactorKit/WeakMapTable.git", + "state" : { + "revision" : "cb05d64cef2bbf51e85c53adee937df46540a74e", + "version" : "1.2.1" + } + } + ], + "version" : 3 +} diff --git a/Poppool/Poppool/Application/AppDelegate.swift b/Poppool/Poppool/Application/AppDelegate.swift index 2489f0f6..82d5d5a8 100644 --- a/Poppool/Poppool/Application/AppDelegate.swift +++ b/Poppool/Poppool/Application/AppDelegate.swift @@ -1,43 +1,104 @@ -// -// AppDelegate.swift -// Poppool -// -// Created by Porori on 11/24/24. -// - -import UIKit -import RxKakaoSDKAuth -import KakaoSDKAuth -import RxKakaoSDKCommon -import GoogleMaps import CoreLocation +import UIKit + +import Data +import Domain +import DomainInterface +import Infrastructure +import Presentation +import PresentationInterface +import SearchFeature +import SearchFeatureInterface + +import KakaoSDKCommon +import NMapsMap @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - RxKakaoSDK.initSDK(appKey: Secrets.kakaoAuthAppkey.rawValue, loggingEnable: false) - GMSServices.provideAPIKey(Secrets.popPoolApiKey.rawValue) + KakaoSDK.initSDK(appKey: Secrets.kakaoAuthAppKey) + NMFAuthManager.shared().clientId = Secrets.naverMapClientID + let locationManager = CLLocationManager() - locationManager.requestWhenInUseAuthorization() // 권한 요청 초기화 - return true + locationManager.requestWhenInUseAuthorization() - } + self.registerDependencies() + self.registerFactory() - // MARK: UISceneSession Lifecycle + return true + } + // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } +} - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } +// MARK: - Dependency +extension AppDelegate { + /// 의존성 등록을 위한 메서드 + private func registerDependencies() { + // MARK: Register Service + DIContainer.register(Provider.self) { return ProviderImpl() } + DIContainer.register(UserDefaultService.self) { return UserDefaultService() } + DIContainer.register(KeyChainService.self) { return KeyChainService() } + // MARK: Resolve service + @Dependency var provider: Provider + @Dependency var userDefaultService: UserDefaultService -} + // MARK: Register repository + DIContainer.register(MapRepository.self) { return MapRepositoryImpl(provider: provider) } + DIContainer.register(AdminRepository.self) { return AdminRepositoryImpl(provider: provider) } + DIContainer.register(UserAPIRepository.self) { return UserAPIRepositoryImpl(provider: provider) } + DIContainer.register(PopUpAPIRepository.self) { return PopUpAPIRepositoryImpl(provider: provider) } + DIContainer.register(CommentAPIRepository.self) { return CommentAPIRepositoryImpl(provider: provider) } + DIContainer.register(HomeAPIRepository.self) { return HomeAPIRepositoryImpl(provider: provider) } + DIContainer.register(AuthAPIRepository.self) { return AuthAPIRepositoryImpl(provider: provider) } + DIContainer.register(SignUpRepository.self) { return SignUpRepositoryImpl(provider: provider) } + DIContainer.register(MapDirectionRepository.self) { return MapDirectionRepositoryImpl(provider: provider) } + DIContainer.register(PreSignedRepository.self) { return PreSignedRepositoryImpl() } + DIContainer.register(KakaoLoginRepository.self) { return KakaoLoginRepositoryImpl() } + DIContainer.register(AppleLoginRepository.self) { return AppleLoginRepositoryImpl() } + DIContainer.register(CategoryRepository.self) { return CategoryRepositoryImpl(provider: provider) } + DIContainer.register(SearchAPIRepository.self) { return SearchAPIRepositoryImpl(provider: provider, userDefaultService: userDefaultService) } + + // MARK: Resolve repository + @Dependency var mapRepository: MapRepository + @Dependency var adminRepository: AdminRepository + @Dependency var userAPIRepository: UserAPIRepository + @Dependency var popUpAPIRepository: PopUpAPIRepository + @Dependency var commentAPIRepository: CommentAPIRepository + @Dependency var homeAPIRepository: HomeAPIRepository + @Dependency var authAPIRepository: AuthAPIRepository + @Dependency var signUpRepository: SignUpRepository + @Dependency var preSignedRepository: PreSignedRepository + @Dependency var kakaoLoginRepository: KakaoLoginRepository + @Dependency var appleLoginRepository: AppleLoginRepository + @Dependency var categoryRepository: CategoryRepository + @Dependency var searchAPIRepository: SearchAPIRepository + // MARK: Register UseCase + DIContainer.register(MapUseCase.self) { return MapUseCaseImpl(repository: mapRepository) } + DIContainer.register(AdminUseCase.self) { return AdminUseCaseImpl(repository: adminRepository) } + DIContainer.register(UserAPIUseCase.self) { return UserAPIUseCaseImpl(repository: userAPIRepository) } + DIContainer.register(PopUpAPIUseCase.self) { return PopUpAPIUseCaseImpl(repository: popUpAPIRepository) } + DIContainer.register(CommentAPIUseCase.self) { return CommentAPIUseCaseImpl(repository: commentAPIRepository) } + DIContainer.register(HomeAPIUseCase.self) { return HomeAPIUseCaseImpl(repository: homeAPIRepository) } + DIContainer.register(AuthAPIUseCase.self) { return AuthAPIUseCaseImpl(repository: authAPIRepository) } + DIContainer.register(SignUpAPIUseCase.self) { return SignUpAPIUseCaseImpl(repository: signUpRepository) } + DIContainer.register(PreSignedUseCase.self) { return PreSignedUseCaseImpl(repository: preSignedRepository) } + DIContainer.register(KakaoLoginUseCase.self) { return KakaoLoginUseCaseImpl(repository: kakaoLoginRepository) } + DIContainer.register(AppleLoginUseCase.self) { return AppleLoginUseCaseImpl(repository: appleLoginRepository) } + DIContainer.register(FetchCategoryListUseCase.self) { return FetchCategoryListUseCaseImpl(repository: categoryRepository) } + DIContainer.register(FetchKeywordBasePopupListUseCase.self) { return FetchKeywordBasePopupListUseCaseImpl(repository: searchAPIRepository) } + } + + private func registerFactory() { + DIContainer.register(PopupSearchFactory.self) { return PopupSearchFactoryImpl() } + DIContainer.register(DetailFactory.self) { return DetailFactoryImpl() } + DIContainer.register(CategorySelectorFactory.self) { return CategorySelectorFactoryImpl() } + DIContainer.register(FilterSelectorFactory.self) { return FilterSelectorFactoryImpl() } + } +} diff --git a/Poppool/Poppool/Application/SceneDelegate.swift b/Poppool/Poppool/Application/SceneDelegate.swift index 8f18f6ac..1df47b66 100644 --- a/Poppool/Poppool/Application/SceneDelegate.swift +++ b/Poppool/Poppool/Application/SceneDelegate.swift @@ -1,86 +1,7 @@ -//// -//// SceneDelegate.swift -//// Poppool -//// -//// Created by Porori on 11/24/24. -//// -// -//import UIKit -//import RxKakaoSDKAuth -//import KakaoSDKAuth -//import RxSwift -// -//class SceneDelegate: UIResponder, UIWindowSceneDelegate { -// -// var window: UIWindow? -// static let appDidBecomeActive = PublishSubject() -// -// func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { -// guard let windowScene = (scene as? UIWindowScene) else { return } -// window = UIWindow(windowScene: windowScene) -// -// // Debug: Admin Page Test -// let provider = ProviderImpl() -// let repository = DefaultAdminRepository(provider: provider) -// let useCase = DefaultAdminUseCase(repository: repository) -// let reactor = AdminReactor(useCase: useCase) -// let adminVC = AdminViewController() -// adminVC.reactor = reactor -// -// let navigationController = UINavigationController(rootViewController: adminVC) -// -// let rootViewController = LoginController() -// rootViewController.reactor = LoginReactor() -// -// let rootVC = WaveTabBarController() -// -// let rootViewController = DetailController() -// rootViewController.reactor = DetailReactor(popUpID: 8) -// -// let rootViewController = SearchMainController() -// rootViewController.reactor = SearchMainReactor() -// -// let navigationController = UINavigationController(rootViewController: rootVC) -// let navigationController = WaveTabBarController() -// -// window?.rootViewController = navigationController -// window?.makeKeyAndVisible() -// } -// -// func sceneDidDisconnect(_ scene: UIScene) { -// } -// -// func sceneDidBecomeActive(_ scene: UIScene) { -// SceneDelegate.appDidBecomeActive.onNext(()) -// } -// -// func sceneWillResignActive(_ scene: UIScene) { -// } -// -// func sceneWillEnterForeground(_ scene: UIScene) { -// } -// -// func sceneDidEnterBackground(_ scene: UIScene) { -// } -// -// func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { -// if let url = URLContexts.first?.url { -// if AuthApi.isKakaoTalkLoginUrl(url) { -// _ = AuthController.rx.handleOpenUrl(url: url) -// } -// } -// } -//} -// -// SceneDelegate.swift -// Poppool -// -// Created by Porori on 11/24/24. -// - import UIKit -import RxKakaoSDKAuth +import Presentation + import KakaoSDKAuth import RxSwift @@ -90,8 +11,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { static let appDidBecomeActive = PublishSubject() static let appDidDisconnect = PublishSubject() - private let disposeBag = DisposeBag() - + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) @@ -119,9 +39,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { if let url = URLContexts.first?.url { if AuthApi.isKakaoTalkLoginUrl(url) { - _ = AuthController.rx.handleOpenUrl(url: url) + _ = AuthController.handleOpenUrl(url: url) } } } } - diff --git a/Poppool/Poppool/Application/TestViewController.swift b/Poppool/Poppool/Application/TestViewController.swift deleted file mode 100644 index c6847136..00000000 --- a/Poppool/Poppool/Application/TestViewController.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// ViewController.swift -// Poppool -// -// Created by Porori on 11/24/24. -// - -import UIKit - -import SnapKit -import RxSwift -import RxGesture -import RxCocoa - -class TestViewController: UIViewController { - - private let topView: UIView = { - let view = UIView() - view.backgroundColor = .w100 - view.alpha = 0 - return view - }() - - private let topViewLabel: UILabel = { - let label = UILabel() - label.text = "Top View Label" - return label - }() - - private let bottomView: UIView = { - let view = UIView() - view.backgroundColor = .w100 - return view - }() - - private let gestureBar: UIView = { - let view = UIView() - view.backgroundColor = .g200 - return view - }() - - private let listButton: PPButton = { - let button = PPButton(style: .secondary, text: "리스트 버튼") - return button - }() - - private let disposeBag = DisposeBag() - - private var bottomViewTopConstraints: Constraint? - - enum ModalState { - case top - case middle - case bottom - } - - var modalState: ModalState = .bottom - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .blue - setUpConstratins() - bind() - } - - func setUpConstratins() { - view.addSubview(listButton) - listButton.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview().inset(20) - make.height.equalTo(50) - } - - view.addSubview(topView) - topView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(104) - } - - topView.addSubview(topViewLabel) - topViewLabel.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - view.addSubview(bottomView) - bottomView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - bottomViewTopConstraints = make.top.equalTo(topView.snp.bottom).offset(700).constraint - make.height.equalTo(700) - } - - bottomView.addSubview(gestureBar) - gestureBar.snp.makeConstraints { make in - make.width.equalTo(50) - make.height.equalTo(20) - make.top.equalToSuperview().inset(20) - make.centerX.equalToSuperview() - } - } - - func bind() { - listButton.rx.tap - .withUnretained(self) - .subscribe { (owner, _) in - print("listButtonTapped") - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 124) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - owner.modalState = .middle - } - } - .disposed(by: disposeBag) - - gestureBar.rx.swipeGesture(.up) - .skip(1) - .withUnretained(self) - .subscribe { (owner, gesture) in - print("swipe up") - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 0) - owner.topView.alpha = 1 - owner.view.layoutIfNeeded() - owner.modalState = .top - } - } - .disposed(by: disposeBag) - - gestureBar.rx.swipeGesture(.down) - .skip(1) - .withUnretained(self) - .subscribe { (owner, gesture) in - print("swipe down") - switch owner.modalState { - case .top: - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 124) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - owner.modalState = .middle - } - case .middle: - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 700) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - owner.modalState = .bottom - } - case .bottom: - break - } - - } - .disposed(by: disposeBag) - } -} diff --git a/Poppool/Poppool/Data/Network/SortedRequestDTO.swift b/Poppool/Poppool/Data/Network/SortedRequestDTO.swift deleted file mode 100644 index 70752653..00000000 --- a/Poppool/Poppool/Data/Network/SortedRequestDTO.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SortedRequestDTO.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - -import Foundation - -struct SortedRequestDTO: Encodable { - var page: Int32? - var size: Int32? - var sort: String? -} diff --git a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetBlockUserListRequestDTO.swift b/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetBlockUserListRequestDTO.swift deleted file mode 100644 index a06c73c1..00000000 --- a/Poppool/Poppool/Data/Network/UserAPI/RequesetDTO/GetBlockUserListRequestDTO.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// GetBlockUserListRequestDTO.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - -import Foundation - -struct GetBlockUserListRequestDTO: Encodable { - var page: Int32? - var size: Int32? - var sort: String? -} diff --git a/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift b/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift deleted file mode 100644 index 0f3bd9ae..00000000 --- a/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// CommentAPIRepository.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import Foundation - -import RxSwift - -final class CommentAPIRepository { - - private let provider: Provider - private let tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { - self.provider = provider - } - - func postCommentAdd(request: PostCommentRequestDTO) -> Completable { - let endPoint = CommentAPIEndPoint.postCommentAdd(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func deleteComment(request: DeleteCommentRequestDTO) -> Completable { - let endPoint = CommentAPIEndPoint.deleteComment(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func editComment(request: PutCommentRequestDTO) -> Completable { - let endPoint = CommentAPIEndPoint.editComment(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } -} diff --git a/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift deleted file mode 100644 index 2ee2ed50..00000000 --- a/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// PopUpAPIRepositoryImpl.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import Foundation - -import RxSwift - -struct PopUpAPIRepositoryImpl { - private let provider: Provider - private let tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { - self.provider = provider - } - - func postBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.postBookmarkPopUp(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func getClosePopUpList(request: GetSearchPopUpListRequestDTO) -> Observable { - let endPoint = PopUpAPIEndPoint.getClosePopUpList(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getOpenPopUpList(request: GetSearchPopUpListRequestDTO) -> Observable { - let endPoint = PopUpAPIEndPoint.getOpenPopUpList(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getSearchPopUpList(request: GetSearchPopUpListRequestDTO) -> Observable { - let endPoint = PopUpAPIEndPoint.getSearchPopUpList(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getPopUpDetail(request: GetPopUpDetailRequestDTO) -> Observable { - let endPoint = PopUpAPIEndPoint.getPopUpDetail(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getPopUpComment(request: GetPopUpCommentRequestDTO) -> Observable { - let endPoint = PopUpAPIEndPoint.getPopUpComment(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } -} diff --git a/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift deleted file mode 100644 index 5e4cdab2..00000000 --- a/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// UserAPIRepositoryImpl.swift -// Poppool -// -// Created by SeoJunYoung on 12/3/24. -// - -import Foundation - -import RxSwift - -final class UserAPIRepositoryImpl { - - private let provider: Provider - private let tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { - self.provider = provider - } - - func postBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.postBookmarkPopUp(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func deleteBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.deleteBookmarkPopUp(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func postCommentLike(request: CommentLikeRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.postCommentLike(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func deleteCommentLike(request: CommentLikeRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.deleteCommentLike(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func postUserBlock(request: PostUserBlockRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.postUserBlock(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func deleteUserBlock(request: PostUserBlockRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.deleteUserBlock(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func getOtherUserCommentList(request: GetOtherUserCommentListRequestDTO) -> Observable { - let endPoint = UserAPIEndPoint.getOtherUserCommentPopUpList(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getMyPage() -> Observable { - let endPoint = UserAPIEndPoint.getMyPage() - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func postLogout() -> Completable { - let endPoint = UserAPIEndPoint.postLogout() - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func getWithdrawlList() -> Observable { - let endPoint = UserAPIEndPoint.getWithdrawlList() - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func postWithdrawl(request: PostWithdrawlListRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.postWithdrawl(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func getMyProfile() -> Observable { - let endPoint = UserAPIEndPoint.getMyProfile() - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func putUserTailoredInfo(request: PutUserTailoredInfoRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.putUserTailoredInfo(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func putUserCategory(request: PutUserCategoryRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.putUserCategory(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func putUserProfile(request: PutUserProfileRequestDTO) -> Completable { - let endPoint = UserAPIEndPoint.putUserProfile(request: request) - return provider.request(with: endPoint, interceptor: tokenInterceptor) - } - - func getMyCommentedPopUp(request: SortedRequestDTO) -> Observable { - let endPoint = UserAPIEndPoint.getMyCommentedPopUp(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getBlockUserList(request: GetBlockUserListRequestDTO) -> Observable { - let endPoint = UserAPIEndPoint.getBlockUserList(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getNoticeList() -> Observable { - let endPoint = UserAPIEndPoint.getNoticeList() - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getNoticeDetail(noticeID: Int64) -> Observable { - let endPoint = UserAPIEndPoint.getNoticeDetail(noticeID: noticeID) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getRecentPopUp(request: SortedRequestDTO) -> Observable { - let endPoint = UserAPIEndPoint.getRecentPopUp(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } - - func getBookmarkPopUp(request: SortedRequestDTO) -> Observable { - let endPoint = UserAPIEndPoint.getBookmarkPopUp(request: request) - return provider.requestData(with: endPoint, interceptor: tokenInterceptor) - } -} diff --git a/Poppool/Poppool/Domain/Entities/AuthAPI/LoginResponse.swift b/Poppool/Poppool/Domain/Entities/AuthAPI/LoginResponse.swift deleted file mode 100644 index 7cb0f2a1..00000000 --- a/Poppool/Poppool/Domain/Entities/AuthAPI/LoginResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// LoginResponse.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - -import Foundation - -struct LoginResponse { - var userId: String - var grantType: String - var accessToken: String - var refreshToken: String - var accessTokenExpiresAt: String - var refreshTokenExpiresAt: String - var socialType: String - var isRegisteredUser: Bool -} diff --git a/Poppool/Poppool/Domain/Entities/AuthAPI/PostTokenReissueResponse.swift b/Poppool/Poppool/Domain/Entities/AuthAPI/PostTokenReissueResponse.swift deleted file mode 100644 index 299e9612..00000000 --- a/Poppool/Poppool/Domain/Entities/AuthAPI/PostTokenReissueResponse.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// PostTokenReissueResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/16/25. -// - -import Foundation - -struct PostTokenReissueResponse { - var accessToken: String? - var refreshToken: String? - var accessTokenExpiresAt: String? - var refreshTokenExpiresAt: String? -} diff --git a/Poppool/Poppool/Domain/Entities/BannerPopUpStore.swift b/Poppool/Poppool/Domain/Entities/BannerPopUpStore.swift deleted file mode 100644 index 11868aaf..00000000 --- a/Poppool/Poppool/Domain/Entities/BannerPopUpStore.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// BannerPopUpStore.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - -import Foundation - -struct BannerPopUpStore { - var id: Int64 - var name: String - var mainImageUrl: String -} diff --git a/Poppool/Poppool/Domain/Entities/Category.swift b/Poppool/Poppool/Domain/Entities/Category.swift deleted file mode 100644 index 9f7da1cd..00000000 --- a/Poppool/Poppool/Domain/Entities/Category.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Category.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - -import Foundation - -struct Category { - let categoryId: Int64 - let category: String -} diff --git a/Poppool/Poppool/Domain/Entities/GetBlockUserListResponse.swift b/Poppool/Poppool/Domain/Entities/GetBlockUserListResponse.swift deleted file mode 100644 index bc7f1adc..00000000 --- a/Poppool/Poppool/Domain/Entities/GetBlockUserListResponse.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GetBlockUserListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - -import Foundation - -struct GetBlockUserListResponse { - var blockedUserInfoList: [GetBlockUserListDataResponse] - var totalPages: Int32 - var totalElements: Int32 -} - -struct GetBlockUserListDataResponse { - var userId: String? - var profileImageUrl: String? - var nickname: String? - var instagramId: String? -} diff --git a/Poppool/Poppool/Domain/Entities/GetHomeInfoResponse.swift b/Poppool/Poppool/Domain/Entities/GetHomeInfoResponse.swift deleted file mode 100644 index a12a3fce..00000000 --- a/Poppool/Poppool/Domain/Entities/GetHomeInfoResponse.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// GetHomeInfoResponse.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - -import Foundation - -struct GetHomeInfoResponse { - var bannerPopUpStoreList: [BannerPopUpStore] - var nickname: String? - var customPopUpStoreList: [PopUpStoreResponse] - var customPopUpStoreTotalPages: Int32 - var customPopUpStoreTotalElements: Int64 - var popularPopUpStoreList: [PopUpStoreResponse] - var popularPopUpStoreTotalPages: Int32 - var popularPopUpStoreTotalElements: Int64 - var newPopUpStoreList: [PopUpStoreResponse] - var newPopUpStoreTotalPages: Int32 - var newPopUpStoreTotalElements: Int64 - var loginYn: Bool -} diff --git a/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift b/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift deleted file mode 100644 index 85d346d8..00000000 --- a/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// GetMyCommentResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - -import Foundation - -struct GetMyCommentedPopUpResponse { - var popUpInfoList: [GetMyCommentedPopUpDataResponse] -} - - -struct GetMyCommentedPopUpDataResponse { - var popUpStoreId: Int64 - var popUpStoreName: String? - var desc: String? - var mainImageUrl: String? - var startDate: String? - var endDate: String? - var address: String? - var closedYn: Bool -} diff --git a/Poppool/Poppool/Domain/Entities/GetMyPageResponse.swift b/Poppool/Poppool/Domain/Entities/GetMyPageResponse.swift deleted file mode 100644 index 0586e572..00000000 --- a/Poppool/Poppool/Domain/Entities/GetMyPageResponse.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// GetMyPageResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/30/24. -// - -import Foundation - -struct GetMyPageResponse { - var nickname: String? - var profileImageUrl: String? - var intro: String? - var instagramId: String? - var loginYn: Bool - var adminYn: Bool - var myCommentedPopUpList: [GetMyPagePopUpResponse] -} - -struct GetMyPagePopUpResponse { - var popUpStoreId: Int64 - var popUpStoreName: String? - var mainImageUrl: String? -} diff --git a/Poppool/Poppool/Domain/Entities/GetMyProfileResponse.swift b/Poppool/Poppool/Domain/Entities/GetMyProfileResponse.swift deleted file mode 100644 index 081da110..00000000 --- a/Poppool/Poppool/Domain/Entities/GetMyProfileResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// GetMyProfileResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - -import Foundation - -struct GetMyProfileResponse { - var profileImageUrl: String? - var nickname: String? - var email: String? - var instagramId: String? - var intro: String? - var gender: String? - var age: Int32 - var interestCategoryList: [Category] -} diff --git a/Poppool/Poppool/Domain/Entities/GetNoticeDetailResponse.swift b/Poppool/Poppool/Domain/Entities/GetNoticeDetailResponse.swift deleted file mode 100644 index a259e9d4..00000000 --- a/Poppool/Poppool/Domain/Entities/GetNoticeDetailResponse.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// GetNoticeDetailResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - -import Foundation - -struct GetNoticeDetailResponse { - var id: Int64 - var title: String? - var content: String? - var createDateTime: String? -} diff --git a/Poppool/Poppool/Domain/Entities/GetNoticeListResponse.swift b/Poppool/Poppool/Domain/Entities/GetNoticeListResponse.swift deleted file mode 100644 index 045e2164..00000000 --- a/Poppool/Poppool/Domain/Entities/GetNoticeListResponse.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// GetNoticeListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - -import Foundation - -struct GetNoticeListResponse { - var noticeInfoList: [GetNoticeListDataResponse] -} - -struct GetNoticeListDataResponse { - var id: Int64 - var title: String? - var createdDateTime: String? -} diff --git a/Poppool/Poppool/Domain/Entities/GetOtherUserCommentedPopUpListResponse.swift b/Poppool/Poppool/Domain/Entities/GetOtherUserCommentedPopUpListResponse.swift deleted file mode 100644 index 71fce6de..00000000 --- a/Poppool/Poppool/Domain/Entities/GetOtherUserCommentedPopUpListResponse.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// GetOtherUserCommentedPopUpListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - -import Foundation - -struct GetOtherUserCommentedPopUpListResponse { - var popUpInfoList: [GetOtherUserCommentedPopUpResponse] -} - -struct GetOtherUserCommentedPopUpResponse { - var popUpStoreId: Int64 - var popUpStoreName: String? - var desc: String? - var mainImageUrl: String? - var startDate: String? - var endDate: String? - var address: String? - var closedYn: Bool -} diff --git a/Poppool/Poppool/Domain/Entities/GetRecentPopUpResponse.swift b/Poppool/Poppool/Domain/Entities/GetRecentPopUpResponse.swift deleted file mode 100644 index 6960a262..00000000 --- a/Poppool/Poppool/Domain/Entities/GetRecentPopUpResponse.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// GetRecentPopUpResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - -import Foundation - -struct GetRecentPopUpResponse { - var popUpInfoList: [GetRecentPopUpDataResponse] - var totalPages: Int32 - var totalElements: Int32 -} - -struct GetRecentPopUpDataResponse { - var popUpStoreId: Int64 - var popUpStoreName: String? - var desc: String? - var mainImageUrl: String? - var startDate: String? - var endDate: String? - var address: String? - var closeYn: Bool -} -extension GetRecentPopUpDataResponse { - func toStoreItem() -> StoreItem { - return StoreItem( - id: self.popUpStoreId, - thumbnailURL: self.mainImageUrl ?? "", - category: "카테고리", - title: self.popUpStoreName ?? "제목 없음", - location: self.address ?? "주소 없음", - dateRange: "\(self.startDate ?? "") ~ \(self.endDate ?? "")", - isBookmarked: self.closeYn - ) - } -} diff --git a/Poppool/Poppool/Domain/Entities/GetWithdrawlListResponse.swift b/Poppool/Poppool/Domain/Entities/GetWithdrawlListResponse.swift deleted file mode 100644 index 9bdf33d6..00000000 --- a/Poppool/Poppool/Domain/Entities/GetWithdrawlListResponse.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// GetWithdrawlListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - -import Foundation - -struct GetWithdrawlListResponse { - var withDrawlSurveyList: [GetWithdrawlListDataResponse] -} - -struct GetWithdrawlListDataResponse { - var id: Int64 - var survey: String? -} diff --git a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpCommentResponse.swift b/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpCommentResponse.swift deleted file mode 100644 index 9befaef6..00000000 --- a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpCommentResponse.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// GetPopUpCommentResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - -import Foundation - -struct GetPopUpCommentResponse { - let commentList: [GetPopUpDetailCommentResponse] -} diff --git a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpDetailResponse.swift b/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpDetailResponse.swift deleted file mode 100644 index 55497518..00000000 --- a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetPopUpDetailResponse.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// GetPopUpDetailResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - -import Foundation - -struct GetPopUpDetailResponse { - let name: String? - let desc: String? - let startDate: String? - let endDate: String? - let startTime: String? - let endTime: String? - let address: String? - let commentCount: Int64 - let bookmarkYn: Bool - let loginYn: Bool - let hasCommented: Bool - let mainImageUrl: String? - let imageList: [GetPopUpDetailImageResponse] - let commentList: [GetPopUpDetailCommentResponse] - let similarPopUpStoreList: [GetPopUpDetailSimilarResponse] -} - -struct GetPopUpDetailImageResponse { - let id: Int64 - let imageUrl: String? -} - -struct GetPopUpDetailCommentResponse { - let commentId: Int64 - let creator: String? - let nickname: String? - let instagramId: String? - let profileImageUrl: String? - let content: String? - let likeYn: Bool - let likeCount: Int64 - let myCommentYn: Bool - let createDateTime: String? - let commentImageList: [GetPopUpDetailImageResponse] -} - -struct GetPopUpDetailSimilarResponse { - let id: Int64 - let name: String? - let mainImageUrl: String? - let endDate: String? -} diff --git a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchBottomPopUpListResponse.swift b/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchBottomPopUpListResponse.swift deleted file mode 100644 index 9ae5c6b1..00000000 --- a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchBottomPopUpListResponse.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// GetSearchBottomPopUpListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import Foundation - -struct GetSearchBottomPopUpListResponse { - var popUpStoreList: [PopUpStoreResponse] - var loginYn: Bool - var totalPages: Int32 - var totalElements: Int64 -} diff --git a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchPopUpListResponse.swift b/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchPopUpListResponse.swift deleted file mode 100644 index c0cb86f5..00000000 --- a/Poppool/Poppool/Domain/Entities/PopUpAPI/GetSearchPopUpListResponse.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// GetSearchPopUpListResponse.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import Foundation - -struct GetSearchPopUpListResponse { - var popUpStoreList: [PopUpStoreResponse] - var loginYn: Bool -} diff --git a/Poppool/Poppool/Domain/Entities/PopUpStoreResponse.swift b/Poppool/Poppool/Domain/Entities/PopUpStoreResponse.swift deleted file mode 100644 index 48cd3914..00000000 --- a/Poppool/Poppool/Domain/Entities/PopUpStoreResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// PopUpStoreResponse.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - -import Foundation - -struct PopUpStoreResponse { - let id: Int64 - let category: String? - let name: String? - let address: String? - let mainImageUrl: String? - let startDate: String? - let endDate: String? - let bookmarkYn: Bool -} diff --git a/Poppool/Poppool/Domain/Repository/AuthRepository.swift b/Poppool/Poppool/Domain/Repository/AuthRepository.swift deleted file mode 100644 index 4aea0f51..00000000 --- a/Poppool/Poppool/Domain/Repository/AuthRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AuthRepository.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - -import Foundation -import RxSwift - -//protocol AuthRepository { -// -// /// 네트워크 요청을 처리하는 프로바이더 -// var provider: Provider { get set } -// -// /// 로그인 시도 메서드 -// /// - Parameters: -// /// - userCredential: 사용자 자격 증명 정보 (Encodable) -// /// - socialType: 소셜 로그인 타입 (예: "google", "facebook") -// /// - Returns: 로그인 응답을 나타내는 Observable 객체 -// func tryLogIn(userCredential: Encodable, socialType: String) -> Observable -//} diff --git a/Poppool/Poppool/Domain/UseCase/AuthAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/AuthAPIUseCaseImpl.swift deleted file mode 100644 index bb96f2c2..00000000 --- a/Poppool/Poppool/Domain/UseCase/AuthAPIUseCaseImpl.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// AuthAPIUseCaseImpl.swift -// Poppool -// -// Created by Porori on 11/25/24. -// - -import Foundation -import RxSwift - -final class AuthAPIUseCaseImpl { - - var repository: AuthAPIRepositoryImpl - - init(repository: AuthAPIRepositoryImpl) { - self.repository = repository - } - - func postTryLogin(userCredential: Encodable, socialType: String) -> Observable { - return repository.tryLogIn(userCredential: userCredential, socialType: socialType) - } - - func postTokenReissue() -> Observable { - let endPoint = AuthAPIEndPoint.postTokenReissue() - return repository.postTokenReissue().map { $0.toDomain() } - } -} diff --git a/Poppool/Poppool/Domain/UseCase/CommentAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/CommentAPIUseCaseImpl.swift deleted file mode 100644 index 1cd85b34..00000000 --- a/Poppool/Poppool/Domain/UseCase/CommentAPIUseCaseImpl.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// CommentAPIUseCaseImpl.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import Foundation - -import RxSwift - -final class CommentAPIUseCaseImpl { - - var repository: CommentAPIRepository - - init(repository: CommentAPIRepository) { - self.repository = repository - } - - func postCommentAdd(popUpStoreId: Int64, content: String?, commentType: String?, imageUrlList: [String?]) -> Completable { - return repository.postCommentAdd(request: .init(popUpStoreId: popUpStoreId, content: content, commentType: commentType, imageUrlList: imageUrlList)) - } - - func deleteComment(popUpStoreId: Int64, commentId: Int64) -> Completable { - return repository.deleteComment(request: .init(popUpStoreId: popUpStoreId, commentId: commentId)) - } - - func editComment(popUpStoreId: Int64, commentId: Int64, content: String?, imageUrlList: [PutCommentImageDataRequestDTO]?) -> Completable { - return repository.editComment(request: .init(popUpStoreId: popUpStoreId, commentId: commentId, content: content, imageUrlList: imageUrlList)) - } -} diff --git a/Poppool/Poppool/Domain/UseCase/HomeAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/HomeAPIUseCaseImpl.swift deleted file mode 100644 index 3c28efac..00000000 --- a/Poppool/Poppool/Domain/UseCase/HomeAPIUseCaseImpl.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// HomeAPIUseCaseImpl.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - -import Foundation -import RxSwift - -final class HomeAPIUseCaseImpl { - var repository = HomeAPIRepository(provider: ProviderImpl()) - - func fetchHome( - page: Int32?, - size: Int32?, - sort: String? - ) -> Observable { - return repository.fetchHome(request: .init(page: page, size: size, sort: sort)) - } - - func fetchCustomPopUp( - page: Int32?, - size: Int32?, - sort: String? - ) -> Observable { - return repository.fetchCustomPopUp(request: .init(page: page, size: size, sort: sort)) - } - - func fetchNewPopUp( - page: Int32?, - size: Int32?, - sort: String? - ) -> Observable { - return repository.fetchNewPopUp(request: .init(page: page, size: size, sort: sort)) - } - - func fetchPopularPopUp( - page: Int32?, - size: Int32?, - sort: String? - ) -> Observable { - return repository.fetchPopularPopUp(request: .init(page: page, size: size, sort: sort)) - } -} diff --git a/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift deleted file mode 100644 index 2b00740c..00000000 --- a/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// PopUpAPIUseCaseImpl.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import Foundation - -import RxSwift - -final class PopUpAPIUseCaseImpl { - - var repository: PopUpAPIRepositoryImpl - - init(repository: PopUpAPIRepositoryImpl) { - self.repository = repository - } - - func getSearchBottomPopUpList(isOpen: Bool, categories: [Int64], page: Int32?, size: Int32, sort: String?) -> Observable { - var categoryString: String? - if !categories.isEmpty { - categoryString = categories.map { String($0) + "," }.reduce("", +) - } - let request = GetSearchPopUpListRequestDTO(categories: categoryString, page: page, size: size, sortCode: sort) - if isOpen { - return repository.getOpenPopUpList(request: request).map { $0.toDomain() } - } else { - return repository.getClosePopUpList(request: request).map { $0.toDomain() } - } - } - - func getSearchPopUpList(query: String?) -> Observable { - return repository.getSearchPopUpList(request: .init(query: query)).map { $0.toDomain() } - } - - func getPopUpDetail(commentType: String?, popUpStoredId: Int64, isViewCount: Bool? = true) -> Observable { - return repository.getPopUpDetail(request: .init(commentType: commentType, popUpStoreId: popUpStoredId, viewCountYn: isViewCount)).map { $0.toDomain() } - } - - func getPopUpComment(commentType: String?, page: Int32?, size: Int32?, sort: String?, popUpStoreId: Int64) -> Observable { - let request:GetPopUpCommentRequestDTO = .init(commentType: commentType, page: page, size: size, sort: sort, popUpStoreId: popUpStoreId) - return repository.getPopUpComment(request: request).map { $0.toDomain() } - } -} diff --git a/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift deleted file mode 100644 index 2eab0c45..00000000 --- a/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// UserAPIUseCaseImpl.swift -// Poppool -// -// Created by SeoJunYoung on 12/3/24. -// - -import RxSwift - -final class UserAPIUseCaseImpl { - - var repository: UserAPIRepositoryImpl - - init(repository: UserAPIRepositoryImpl) { - self.repository = repository - } - - func postBookmarkPopUp(popUpID: Int64) -> Completable { - return repository.postBookmarkPopUp(request: .init(popUpStoreId: popUpID)) - } - - func deleteBookmarkPopUp(popUpID: Int64) -> Completable { - return repository.deleteBookmarkPopUp(request: .init(popUpStoreId: popUpID)) - } - - func postCommentLike(commentId: Int64) -> Completable { - return repository.postCommentLike(request: .init(commentId: commentId)) - } - - func deleteCommentLike(commentId: Int64) -> Completable { - return repository.deleteCommentLike(request: .init(commentId: commentId)) - } - - func postUserBlock(blockedUserId: String?) -> Completable { - return repository.postUserBlock(request: .init(blockedUserId: blockedUserId)) - } - - func deleteUserBlock(blockedUserId: String?) -> Completable { - return repository.deleteUserBlock(request: .init(blockedUserId: blockedUserId)) - } - - func getOtherUserCommentedPopUpList( - commenterId: String?, - commentType: String?, - page: Int32?, - size: Int32?, - sort: String? - ) -> Observable { - return repository.getOtherUserCommentList( - request: .init( - commenterId: commenterId, - commentType: commentType, - page: page, - size: size, - sort: sort) - ) - .map { $0.toDomain() } - } - - func getMyPage() -> Observable { - return repository.getMyPage().map { $0.toDomain() } - } - - func postLogout() -> Completable { - return repository.postLogout() - } - - func getWithdrawlList() -> Observable { - return repository.getWithdrawlList().map { $0.toDomain() } - } - - func postWithdrawl(surveyList: [GetWithdrawlListDataResponse]) -> Completable { - return repository.postWithdrawl(request: .init(checkedSurveyList: surveyList.map { .init(id: $0.id, survey: $0.survey)})) - } - - func getMyProfile() -> Observable { - return repository.getMyProfile().map { $0.toDomain() } - } - - func putUserTailoredInfo(gender: String?, age: Int32) -> Completable { - return repository.putUserTailoredInfo(request: .init(gender: gender, age: age)) - } - - func putUserCategory( - interestCategoriesToAdd: [Int64], - interestCategoriesToDelete: [Int64], - interestCategoriesToKeep: [Int64] - ) -> Completable { - return repository.putUserCategory( - request: .init( - interestCategoriesToAdd: interestCategoriesToAdd, - interestCategoriesToDelete: interestCategoriesToDelete, - interestCategoriesToKeep: interestCategoriesToKeep - ) - ) - } - - func putUserProfile(profileImageUrl: String?, nickname: String?, email: String?, instagramId: String?, intro: String?) -> Completable { - return repository.putUserProfile(request: .init(profileImageUrl: profileImageUrl, nickname: nickname, email: email, instagramId: instagramId, intro: intro)) - } - - func getMyCommentedPopUp(page: Int32?, size:Int32?, sort: String?) -> Observable { - return repository.getMyCommentedPopUp(request: .init(page: page, size: size, sort: sort)).map { $0.toDomain() } - } - - func getBlockUserList(page: Int32?, size: Int32?, sort: String?) -> Observable { - return repository.getBlockUserList(request: .init(page: page, size: size, sort: sort)).map { $0.toDomain() } - } - - func getNoticeList() -> Observable { - return repository.getNoticeList().map { $0.toDomain() } - } - - func getNoticeDetail(noticeID: Int64) -> Observable { - return repository.getNoticeDetail(noticeID: noticeID).map { $0.toDomain() } - } - - func getRecentPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { - return repository.getRecentPopUp(request: .init(page: page, size: size, sort: sort)).map { $0.toDomain() } - } - - func getBookmarkPopUp(page: Int32?, size: Int32?, sort: String?) -> Observable { - return repository.getBookmarkPopUp(request: .init(page: page, size: size, sort: sort)).map { $0.toDomain() } - } -} diff --git a/Poppool/Poppool/Infrastructure/KakaoLoginService.swift b/Poppool/Poppool/Infrastructure/KakaoLoginService.swift deleted file mode 100644 index 951c56b5..00000000 --- a/Poppool/Poppool/Infrastructure/KakaoLoginService.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// KakaoLoginService.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/13/24. -// - -import RxSwift -import KakaoSDKUser -import RxKakaoSDKUser -import KakaoSDKAuth - -final class KakaoLoginService: AuthServiceable { - - struct Credential: Encodable { - var id: String - var token: String - } - - var disposeBag = DisposeBag() - - func unlink() -> Observable { - return Observable.create { observer in - UserApi.shared.unlink { error in - if let error = error { - observer.onNext(()) - Logger.log(message: error.localizedDescription, category: .error) - } else { - observer.onNext(()) - observer.onCompleted() - } - } - return Disposables.create() - } - } - - func fetchUserCredential() -> Observable { - return Observable.create { [weak self] observer in - guard let self = self else { - Logger.log( - message: "KakaoTalk login Error", - category: .error, - fileName: #file, - line: #line - ) - return Disposables.create() - } - // 카카오톡 설치 유무 - guard UserApi.isKakaoTalkLoginAvailable() else { - Logger.log( - message: "KakaoTalk is not install", - category: .error, - fileName: #file, - line: #line - ) - UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in - if let error = error { - observer.onError(error) - } else { - if let self = self, let accessToken = oauthToken?.accessToken { - self.fetchUserId(observer: observer, accessToken: accessToken) - } - } - } - return Disposables.create() - } - // token을 획득하기 위한 로그인 - loginWithKakaoTalk() - .withUnretained(self) - .subscribe { (owner, loginResponse) in - owner.fetchUserId(observer: observer, accessToken: loginResponse.accessToken) - } onError: { _ in - observer.onError(AuthError.unknownError) - } - .disposed(by: disposeBag) - - return Disposables.create() - } - } -} - -private extension KakaoLoginService { - - func fetchUserId(observer: AnyObserver, accessToken: String) { - UserApi.shared.rx.me() - .subscribe(onSuccess: { user in - observer.onNext(.init(kakaoUserId: user.id,kakaoAccessToken: accessToken)) - }, onFailure: { _ in - observer.onError(AuthError.unknownError) - }) - .disposed(by: self.disposeBag) - } - - func loginWithKakaoTalk() -> Observable { - return UserApi.shared.rx.loginWithKakaoTalk() - .do { token in - Logger.log( - message: "KakaoTalk Login Response - \(token)", - category: .info, - fileName: #file, - line: #line - ) - } onError: { _ in - Logger.log( - message: "KakaoTalk Login Fail", - category: .error, - fileName: #file, - line: #line - ) - } - } - - func fetchUserProfile() -> Single { - return UserApi.shared.rx.me() - .do { user in - Logger.log( - message: "KakaoTalk Profile Response - \(user)", - category: .info, - fileName: #file, - line: #line - ) - } onError: { _ in - Logger.log( - message: "KakaoTalk Profile Fetch Fail", - category: .error, - fileName: #file, - line: #line - ) - } - } -} diff --git a/Poppool/Poppool/Infrastructure/Logger/Logger.swift b/Poppool/Poppool/Infrastructure/Logger/Logger.swift deleted file mode 100644 index cf3cbadb..00000000 --- a/Poppool/Poppool/Infrastructure/Logger/Logger.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Logger.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/9/24. -// - -import Foundation - -struct Logger { - enum Level { - case info - case debug - case network - case error - case event - case custom(categoryName: String) - - var categoryName: String { - switch self { - case .info: - return "Info" - case .debug: - return "Debug" - case .network: - return "Network" - case .error: - return "Error" - case .event: - return "Event" - case .custom(let categoryName): - return categoryName - } - } - - var categoryIcon: String { - switch self { - case .info: - return "✅" - case .debug: - return "⚠️" - case .network: - return "🌎" - case .error: - return "⛔️" - case .event: - return "🎉" - case .custom: - return "🍎" - } - } - } - - static var isShowFileName: Bool = false - static var isShowLine: Bool = false - static var isShowLog: Bool = true - - static private let noInputText = "Input is not found" - - static func log( - message: Any, - category: Level, - fileName: String = noInputText, - line: Int? = nil - ) { - if isShowLog { - print("\(category.categoryIcon) [\(category.categoryName)]: \(message)") - if isShowFileName { - guard let fileName = fileName.components(separatedBy: "/").last else { return } - print(" \(category.categoryIcon) [FileName]: \(fileName)") - } - if isShowLine { - guard let line = line else { return } - print(" \(category.categoryIcon) [Line]: \(line)") - } - } - } -} diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Responsable.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Responsable.swift deleted file mode 100644 index c4743d30..00000000 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Responsable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Responsable.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/16/24. -// - -import Foundation - -protocol Responsable { - associatedtype Response -} diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift deleted file mode 100644 index f25705e3..00000000 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// IndicatorMaker.swift -// MomsVillage -// -// Created by SeoJunYoung on 10/14/24. -// - -import UIKit - -import Lottie -import SnapKit - -struct IndicatorMaker { - - static let indicatorImageView: LottieAnimationView = { - let view = LottieAnimationView(name: "indicator") - view.loopMode = .loop - return view - }() - - static let overlayView: UIView = { - let view = UIView() - view.backgroundColor = .black.withAlphaComponent(0.1) - return view - }() - - static func showIndicator() { - DispatchQueue.main.async { - guard let topVC = UIApplication.topViewController() else { - print("Error: Cannot find top view controller") - return - } - - topVC.view.addSubview(overlayView) - overlayView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - overlayView.addSubview(indicatorImageView) - indicatorImageView.snp.makeConstraints { make in - make.size.equalTo(200) - make.center.equalToSuperview() - } - indicatorImageView.play() - topVC.view.isUserInteractionEnabled = false - } - } - - static func hideIndicator() { - DispatchQueue.main.async { - indicatorImageView.stop() - overlayView.removeFromSuperview() - indicatorImageView.removeFromSuperview() - UIApplication.topViewController()?.view.isUserInteractionEnabled = true - } - } -} diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift deleted file mode 100644 index efd1d123..00000000 --- a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FormDataInterceptor.swift -// MomsVillage -// -// Created by SeoJunYoung on 10/25/24. -// - -import Foundation -import Alamofire -import RxSwift - -final class FormDataInterceptor: RequestInterceptor { - - private var disposeBag = DisposeBag() - - func adapt( - _ urlRequest: URLRequest, - for session: Session, - completion: @escaping (Result) -> Void) { - } - - func retry( - _ request: Request, - for session: Session, - dueTo error: any Error, - completion: @escaping (RetryResult) -> Void - ) { - Logger.log(message: "FormDataInterceptor Retry Start", category: .network) - } -} diff --git a/Poppool/Poppool/Infrastructure/UserDefaultService.swift b/Poppool/Poppool/Infrastructure/UserDefaultService.swift deleted file mode 100644 index ffb4c2de..00000000 --- a/Poppool/Poppool/Infrastructure/UserDefaultService.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// UserDefaultService.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/2/24. -// - -import Foundation - -import RxSwift - -final class UserDefaultService { - - /// Userdefault 데이터 저장 메서드 - /// - Parameters: - /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 - /// - value: 저장하는 데이터 값 i.e) access token 등 - /// - to: 로컬 데이터베이스 타입 - DatabaseType - /// - Returns: 별도 안내 없음 - func save(key: String, value: String) { - UserDefaults.standard.set(value, forKey: key) - } - - /// Userdefault 데이터 저장 메서드 - /// - Parameters: - /// - key: 저장하는 데이터의 키 값 i.e) 유저 id 등 - /// - value: 저장하는 데이터 값 i.e) access token 등 - /// - to: 로컬 데이터베이스 타입 - DatabaseType - /// - Returns: 별도 안내 없음 - func save(key: String, value: [String]) { - UserDefaults.standard.set(value, forKey: key) - } - - /// Userdefault 데이터 발견 메서드 - /// - Parameters: - /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 - /// - from: 로컬 데이터베이스 타입 - DatabaseType - /// - Returns: 찾은 데이터 - String 타입 - func fetch(key: String) -> String? { - if let token = UserDefaults.standard.string(forKey: key) { - return token - } - return nil - } - - /// Userdefault 데이터 발견 메서드 - /// - Parameters: - /// - key: 찾는 데이터의 키 값 i.e) 유저 id 등 - /// - from: 로컬 데이터베이스 타입 - DatabaseType - /// - Returns: 찾은 데이터 - String 타입 - func fetchArray(key: String) -> [String]? { - if let token = UserDefaults.standard.array(forKey: key) as? [String] { - return token - } - return nil - } - - /// Userdefault 데이터 삭제 메서드 - /// - Parameters: - /// - key: 삭제하는 데이터의 키 값 i.e) 유저 id 등 - /// - from: 로컬 데이터베이스 타입 - DatabaseType - /// - Returns: 별도 안내 없음 - func delete(key: String) { - UserDefaults.standard.removeObject(forKey: key) - } -} diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift deleted file mode 100644 index b02ef0fe..00000000 --- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift +++ /dev/null @@ -1,111 +0,0 @@ -import ReactorKit -import RxSwift -import RxCocoa - - -final class PopUpStoreRegisterReactor: Reactor { - - // MARK: - Action - enum Action { - case updateName(String) - case updateAddress(String) - case updateLat(String) - case updateLon(String) - case updateDescription(String) - case selectCategory(String) - case addImage(ExtendedImage) - case removeImage(Int) - case tapSave - } - - // MARK: - Mutation - enum Mutation { - case setName(String) - case setAddress(String) - case setLat(String) - case setLon(String) - case setDescription(String) - case setCategory(String) - case addImage(ExtendedImage) - case removeImage(Int) - case setSaveEnabled(Bool) - } - - // MARK: - State - struct State { - var name: String = "" - var address: String = "" - var lat: String = "" - var lon: String = "" - var description: String = "" - var category: String = "" - var images: [ExtendedImage] = [] - var isSaveEnabled: Bool = false - } - - let initialState = State() - - // MARK: - Mutate - func mutate(action: Action) -> Observable { - switch action { - case let .updateName(name): - return .just(.setName(name)) - case let .updateAddress(address): - return .just(.setAddress(address)) - case let .updateLat(lat): - return .just(.setLat(lat)) - case let .updateLon(lon): - return .just(.setLon(lon)) - case let .updateDescription(desc): - return .just(.setDescription(desc)) - case let .selectCategory(category): - return .just(.setCategory(category)) - case let .addImage(image): - return .just(.addImage(image)) - case let .removeImage(index): - return .just(.removeImage(index)) - case .tapSave: - // API 호출 등 저장 로직은 여기서 처리하거나 별도 Service로 위임합니다. - // 이 예제에서는 저장 전 폼 유효성 검증만 진행합니다. - return .empty() - } - } - - // MARK: - Reduce - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .setName(name): - newState.name = name - case let .setAddress(address): - newState.address = address - case let .setLat(lat): - newState.lat = lat - case let .setLon(lon): - newState.lon = lon - case let .setDescription(desc): - newState.description = desc - case let .setCategory(category): - newState.category = category - case let .addImage(image): - newState.images.append(image) - case let .removeImage(index): - if index < newState.images.count { - newState.images.remove(at: index) - } - case .setSaveEnabled(let enabled): - newState.isSaveEnabled = enabled - } - - let isValid = !newState.name.isEmpty && - !newState.address.isEmpty && - !newState.lat.isEmpty && - !newState.lon.isEmpty && - !newState.description.isEmpty && - !newState.category.isEmpty && - !newState.images.isEmpty - newState.isSaveEnabled = isValid - - return newState - } -} diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift deleted file mode 100644 index 5c25cf83..00000000 --- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift +++ /dev/null @@ -1,265 +0,0 @@ -import UIKit -import SnapKit -import RxSwift -import RxCocoa - -final class PopUpStoreRegisterView: UIView { - // 상단 네비게이션 영역 - let logoImageView: UIImageView = { - let iv = UIImageView() - iv.image = UIImage(named: "image_login_logo") - iv.contentMode = .scaleAspectFit - return iv - }() - - let accountIdLabel: UILabel = { - let lbl = UILabel() - lbl.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - lbl.textColor = .black - return lbl - }() - - let menuButton: UIButton = { - let btn = UIButton(type: .system) - btn.setImage(UIImage(systemName: "adminlist"), for: .normal) - btn.tintColor = .black - return btn - }() - - // 타이틀 영역 - let backButton: UIButton = { - let btn = UIButton(type: .system) - btn.setImage(UIImage(systemName: "chevron.left"), for: .normal) - btn.tintColor = .black - return btn - }() - - let pageTitleLabel: UILabel = { - let lbl = UILabel() - lbl.text = "팝업스토어 등록" - lbl.font = UIFont.boldSystemFont(ofSize: 18) - lbl.textColor = .black - return lbl - }() - - // 입력 폼 영역 - let nameTextField: UITextField = { - let tf = UITextField() - tf.placeholder = "팝업스토어 이름을 입력해 주세요." - tf.font = UIFont.systemFont(ofSize: 14) - tf.borderStyle = .roundedRect - return tf - }() - - let categoryButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("카테고리 선택 ▾", for: .normal) - btn.setTitleColor(.darkGray, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) - btn.layer.cornerRadius = 8 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.lightGray.cgColor - btn.contentHorizontalAlignment = .left - btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8) - return btn - }() - - let addressTextField: UITextField = { - let tf = UITextField() - tf.placeholder = "팝업스토어 주소를 입력해 주세요." - tf.font = UIFont.systemFont(ofSize: 14) - tf.borderStyle = .roundedRect - return tf - }() - - let latTextField: UITextField = { - let tf = UITextField() - tf.placeholder = "위도" - tf.font = UIFont.systemFont(ofSize: 14) - tf.textAlignment = .center - tf.borderStyle = .roundedRect - return tf - }() - - let lonTextField: UITextField = { - let tf = UITextField() - tf.placeholder = "경도" - tf.font = UIFont.systemFont(ofSize: 14) - tf.textAlignment = .center - tf.borderStyle = .roundedRect - return tf - }() - - let descriptionTextView: UITextView = { - let tv = UITextView() - tv.font = UIFont.systemFont(ofSize: 14) - tv.layer.cornerRadius = 8 - tv.layer.borderWidth = 1 - tv.layer.borderColor = UIColor.lightGray.cgColor - tv.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7) - return tv - }() - - // 이미지 관련 영역 - let addImageButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("이미지 추가", for: .normal) - btn.setTitleColor(.systemBlue, for: .normal) - return btn - }() - - let removeAllButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("전체 삭제", for: .normal) - btn.setTitleColor(.red, for: .normal) - return btn - }() - - lazy var imagesCollectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.itemSize = CGSize(width: 80, height: 120) - layout.minimumLineSpacing = 8 - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - // 셀 등록 등은 실제 프로젝트에 맞게 설정합니다. - cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "ImageCell") - return cv - }() - - // 저장 버튼 - let saveButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("저장", for: .normal) - btn.setTitleColor(.white, for: .normal) - btn.backgroundColor = .lightGray - btn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) - btn.layer.cornerRadius = 8 - btn.isEnabled = false - return btn - }() - - // 기타 UI 요소는 필요에 따라 추가하세요. - - // MARK: - Initializer - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = UIColor(white: 0.95, alpha: 1) - setupLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Layout Setup - private func setupLayout() { - // 예시로 상단 네비게이션과 입력폼 일부만 배치합니다. - let navContainer = UIView() - addSubview(navContainer) - navContainer.snp.makeConstraints { make in - make.top.equalTo(self.safeAreaLayoutGuide.snp.top) - make.left.right.equalToSuperview() - make.height.equalTo(44) - } - - navContainer.addSubview(logoImageView) - logoImageView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(8) - make.centerY.equalToSuperview() - make.width.equalTo(22) - make.height.equalTo(35) - } - - navContainer.addSubview(accountIdLabel) - accountIdLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalTo(logoImageView.snp.right).offset(8) - } - - navContainer.addSubview(menuButton) - menuButton.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.right.equalToSuperview().inset(16) - make.width.height.equalTo(32) - } - - // 타이틀 영역 - let titleContainer = UIView() - addSubview(titleContainer) - titleContainer.snp.makeConstraints { make in - make.top.equalTo(navContainer.snp.bottom) - make.left.right.equalToSuperview() - make.height.equalTo(44) - } - - titleContainer.addSubview(backButton) - backButton.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalToSuperview().offset(8) - make.width.height.equalTo(32) - } - - titleContainer.addSubview(pageTitleLabel) - pageTitleLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalTo(backButton.snp.right).offset(4) - } - - // 입력폼 영역 (예시) - let formStack = UIStackView(arrangedSubviews: [nameTextField, categoryButton, addressTextField]) - formStack.axis = .vertical - formStack.spacing = 16 - addSubview(formStack) - formStack.snp.makeConstraints { make in - make.top.equalTo(titleContainer.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - } - - // 위/경도는 수평 스택 - let latLonStack = UIStackView(arrangedSubviews: [latTextField, lonTextField]) - latLonStack.axis = .horizontal - latLonStack.spacing = 16 - latLonStack.distribution = .fillEqually - addSubview(latLonStack) - latLonStack.snp.makeConstraints { make in - make.top.equalTo(formStack.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - } - - // 설명 텍스트뷰 - addSubview(descriptionTextView) - descriptionTextView.snp.makeConstraints { make in - make.top.equalTo(latLonStack.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(120) - } - - // 이미지 영역 (Add/Remove 버튼과 CollectionView) - let imageButtonStack = UIStackView(arrangedSubviews: [addImageButton, removeAllButton]) - imageButtonStack.axis = .horizontal - imageButtonStack.distribution = .fillEqually - imageButtonStack.spacing = 16 - addSubview(imageButtonStack) - imageButtonStack.snp.makeConstraints { make in - make.top.equalTo(descriptionTextView.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(40) - } - - addSubview(imagesCollectionView) - imagesCollectionView.snp.makeConstraints { make in - make.top.equalTo(imageButtonStack.snp.bottom).offset(8) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(130) - } - - // 저장 버튼 - addSubview(saveButton) - saveButton.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(16) - make.bottom.equalTo(self.safeAreaLayoutGuide.snp.bottom).offset(-16) - make.height.equalTo(44) - } - } -} diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift deleted file mode 100644 index a559ee54..00000000 --- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift +++ /dev/null @@ -1,1560 +0,0 @@ -import UIKit -import SnapKit -import ReactorKit -import RxSwift -import RxCocoa -import PhotosUI -import Alamofire -import GoogleMaps -import CoreLocation - -final class PopUpStoreRegisterViewController: BaseViewController { - - // MARK: - Navigation/Header - var completionHandler: (() -> Void)? - private var selectedImages: [UIImage] = [] - private var selectedMainImageIndex: Int? - private var imageFileNames: [String] = [] - private var images: [ExtendedImage] = [] - private var pickerViewController: PHPickerViewController? - private let adminUseCase: AdminUseCase - private var nameField: UITextField? - private var addressField: UITextField? - private var latField: UITextField? - private var lonField: UITextField? - private var descTV: UITextView? - - - private let popupName: String = "" - private let editingStore: GetAdminPopUpStoreListResponseDTO.PopUpStore? - let presignedService = PreSignedService() - - var disposeBag = DisposeBag() - private let nickname: String - private let navContainer = UIView() - - init(nickname: String, adminUseCase: AdminUseCase, editingStore: GetAdminPopUpStoreListResponseDTO.PopUpStore? = nil) { - self.nickname = nickname - self.adminUseCase = adminUseCase - self.editingStore = editingStore - super.init() - self.accountIdLabel.text = nickname + "님" - if editingStore != nil { - pageTitleLabel.text = "팝업스토어 수정" - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private lazy var imagesCollectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.itemSize = CGSize(width: 80, height: 120) - layout.minimumLineSpacing = 8 - - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.identifier) - cv.dataSource = self - cv.delegate = self - return cv - }() - - private let logoImageView: UIImageView = { - let iv = UIImageView() - iv.image = UIImage(named: "image_login_logo") - iv.contentMode = .scaleAspectFit - return iv - }() - - private let accountIdLabel: UILabel = { - let lbl = UILabel() - lbl.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - lbl.textColor = .black - return lbl - }() - - - private let menuButton: UIButton = { - let btn = UIButton(type: .system) - btn.setImage(UIImage(systemName: "adminlist"), for: .normal) - btn.tintColor = .black - return btn - }() - - // MARK: - Title (Back button + label) - private let titleContainer = UIView() - private let backButton: UIButton = { - let btn = UIButton(type: .system) - btn.setImage(UIImage(systemName: "chevron.left"), for: .normal) - btn.tintColor = .black - return btn - }() - - private let pageTitleLabel: UILabel = { - let lbl = UILabel() - lbl.text = "팝업스토어 등록" - lbl.font = UIFont.boldSystemFont(ofSize: 18) - lbl.textColor = .black - return lbl - }() - - - private let addImageButton = UIButton(type: .system).then { - $0.setTitle("이미지 추가", for: .normal) - $0.setTitleColor(.systemBlue, for: .normal) - } - - private let removeAllButton = UIButton(type: .system).then { - $0.setTitle("전체 삭제", for: .normal) - $0.setTitleColor(.red, for: .normal) - } - - // MARK: - Scroll - private let scrollView = UIScrollView() - private let contentView = UIView() - - // MARK: - Form Background - private let formBackgroundView: UIView = { - let v = UIView() - v.backgroundColor = .white - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.lightGray.cgColor - v.layer.cornerRadius = 8 - return v - }() - private let verticalStack = UIStackView() - - // MARK: - Bottom Save Button - private let saveButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("저장", for: .normal) - btn.setTitleColor(.white, for: .normal) - btn.backgroundColor = .lightGray - btn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) - btn.layer.cornerRadius = 8 - btn.isEnabled = false - return btn - }() - - // MARK: - DateTimePicker - private var selectedStartDate: Date? - private var selectedEndDate: Date? - private var selectedStartTime: Date? - private var selectedEndTime: Date? - - // MARK: - Categories - private var categories: [String] = ["게임", "라이프스타일", "반려동물", "뷰티", "스포츠", "애니메이션", "엔터테인먼트", "여행", "예술", "음식/요리", "키즈", "패션"] - - // MARK: - UI Elements - private let categoryButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("카테고리 선택 ▾", for: .normal) - btn.setTitleColor(.darkGray, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize:14) - btn.layer.cornerRadius = 8 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.lightGray.cgColor - btn.contentHorizontalAlignment = .left - btn.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) - return btn - }() - - private let periodButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("기간 선택 ▾", for: .normal) - btn.setTitleColor(.darkGray, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize:14) - btn.layer.cornerRadius = 8 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.lightGray.cgColor - btn.contentHorizontalAlignment = .left - btn.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) - return btn - }() - - private let timeButton: UIButton = { - let btn = UIButton(type: .system) - btn.setTitle("시간 선택 ▾", for: .normal) - btn.setTitleColor(.darkGray, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize:14) - btn.layer.cornerRadius = 8 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.lightGray.cgColor - btn.contentHorizontalAlignment = .left - btn.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) - return btn - }() - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = UIColor(white:0.95, alpha:1) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGesture.cancelsTouchesInView = false - view.addGestureRecognizer(tapGesture) - if let store = editingStore { - loadStoreDetail(for: store.id) - } - - setupNavigation() - setupLayout() - setupRows() - setupImageCollectionUI() - setupImageCollectionActions() - setupKeyboardHandling() - setupAddressField() - - - } - - - // MARK: - Navigation - private func setupNavigation() { - backButton.addTarget(self, action: #selector(onBack), for: .touchUpInside) - } - - @objc private func handleTap() { - view.endEditing(true) - } - @objc private func onBack() { - navigationController?.popViewController(animated: true) - } - @objc private func fieldDidChange(_ textField: UITextField) { - if textField == addressField { - Logger.log(message: "주소 값 변경: \(textField.text ?? "nil")", category: .debug) - } else if textField == latField { - Logger.log(message: "위도 값 변경: \(textField.text ?? "nil")", category: .debug) - } else if textField == lonField { - Logger.log(message: "경도 값 변경: \(textField.text ?? "nil")", category: .debug) - updateSaveButtonState() - - } - } - private func fillFormWithExistingData(_ storeDetail: GetAdminPopUpStoreDetailResponseDTO) { - nameField?.text = storeDetail.name - categoryButton.setTitle("\(storeDetail.categoryName) ▾", for: .normal) - addressField?.text = storeDetail.address - latField?.text = String(storeDetail.latitude) - lonField?.text = String(storeDetail.longitude) - descTV?.text = storeDetail.desc - - let isoFormatter = ISO8601DateFormatter() - - if let startDate = isoFormatter.date(from: storeDetail.startDate), - let endDate = isoFormatter.date(from: storeDetail.endDate) { - self.selectedStartDate = startDate - self.selectedEndDate = endDate - self.updatePeriodButtonTitle() // 버튼에 날짜 텍스트 업데이트 - } - - // 대표 이미지 로드 (생략된 부분은 기존 코드 참고) - if let mainImageURL = presignedService.fullImageURL(from: storeDetail.mainImageUrl) { - URLSession.shared.dataTask(with: mainImageURL) { [weak self] data, response, error in - guard let self = self, - let data = data, - let image = UIImage(data: data) else { return } - let extendedImage = ExtendedImage(filePath: storeDetail.mainImageUrl, image: image, isMain: true) - DispatchQueue.main.async { - self.images.append(extendedImage) - self.imagesCollectionView.reloadData() - self.updateSaveButtonState() - } - }.resume() - } - } - func loadStoreDetail(for storeId: Int64) { - Logger.log(message: "상세 정보 요청 시작 - Store ID: \(storeId)", category: .debug) - - adminUseCase.fetchStoreDetail(id: storeId) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] storeDetail in - Logger.log(message: "상세 정보 요청 성공", category: .info) - self?.fillFormWithExistingData(storeDetail) - }, onError: { error in - Logger.log(message: "상세 정보 요청 실패: \(error.localizedDescription)", category: .error) - }) - .disposed(by: disposeBag) - } - - private func setupKeyboardHandling() { - // 키보드 Notification 등록 - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillShow), - name: UIResponder.keyboardWillShowNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillHide), - name: UIResponder.keyboardWillHideNotification, - object: nil - ) - - // 스크롤뷰 키보드 처리 설정 - scrollView.keyboardDismissMode = .interactive - } - - @objc private func keyboardWillShow(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { - return - } - - let keyboardHeight = keyboardFrame.height - let contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: keyboardHeight, - right: 0 - ) - - scrollView.contentInset = contentInset - scrollView.scrollIndicatorInsets = contentInset - - // 현재 활성화된 필드가 키보드에 가려지는지 확인 - if let activeField = view.findFirstResponder() { - let activeRect = activeField.convert(activeField.bounds, to: scrollView) - let bottomOffset = activeRect.maxY + 20 // 여유 공간 - - if bottomOffset > (scrollView.frame.height - keyboardHeight) { - let scrollPoint = CGPoint( - x: 0, - y: bottomOffset - (scrollView.frame.height - keyboardHeight) - ) - scrollView.setContentOffset(scrollPoint, animated: true) - } - } - } - - @objc private func keyboardWillHide(_ notification: Notification) { - UIView.animate(withDuration: 0.3) { - self.scrollView.contentInset = .zero - self.scrollView.scrollIndicatorInsets = .zero - } - } - - - - // MARK: - Layout - private func setupLayout() { - // (1) 상단 컨테이너 - view.addSubview(navContainer) - navContainer.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide) - make.left.right.equalToSuperview() - make.height.equalTo(44) - } - - navContainer.addSubview(logoImageView) - logoImageView.snp.makeConstraints { make in - make.left.equalToSuperview().offset(8) - make.centerY.equalToSuperview() - make.width.equalTo(22) - make.height.equalTo(35) - } - - navContainer.addSubview(accountIdLabel) - accountIdLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalTo(logoImageView.snp.right).offset(8) - } - - navContainer.addSubview(menuButton) - menuButton.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.right.equalToSuperview().inset(16) - make.width.height.equalTo(32) - } - - // (2) 타이틀 컨테이너 - view.addSubview(titleContainer) - titleContainer.snp.makeConstraints { make in - make.top.equalTo(navContainer.snp.bottom) - make.left.right.equalToSuperview() - make.height.equalTo(44) - } - - titleContainer.addSubview(backButton) - backButton.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalToSuperview().offset(8) - make.width.height.equalTo(32) - } - - titleContainer.addSubview(pageTitleLabel) - pageTitleLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.equalTo(backButton.snp.right).offset(4) - } - - // (3) 스크롤뷰 - view.addSubview(scrollView) - scrollView.snp.makeConstraints { make in - make.top.equalTo(titleContainer.snp.bottom) - make.left.right.equalToSuperview() - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-74) - } - - scrollView.addSubview(contentView) - contentView.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.width.equalTo(scrollView.snp.width) - } - - // (4) 이미지 영역 추가 - let buttonStack = UIStackView(arrangedSubviews: [addImageButton, removeAllButton]) - buttonStack.axis = .horizontal - buttonStack.distribution = .fillEqually - buttonStack.spacing = 16 - - contentView.addSubview(buttonStack) - buttonStack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(40) - } - - contentView.addSubview(imagesCollectionView) - imagesCollectionView.snp.makeConstraints { make in - make.top.equalTo(buttonStack.snp.bottom).offset(8) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(130) - } - - // (5) 폼 배경 - contentView.addSubview(formBackgroundView) - formBackgroundView.snp.makeConstraints { make in - make.top.equalTo(imagesCollectionView.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - make.bottom.equalToSuperview().offset(-16) - } - - formBackgroundView.addSubview(verticalStack) - verticalStack.axis = .vertical - verticalStack.spacing = 0 - verticalStack.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - // (6) 저장 버튼 - view.addSubview(saveButton) - saveButton.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(16) - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16) - make.height.equalTo(44) - } - } - // MARK: - Setup Rows - private func setupRows() { - addRowTextField(leftTitle: "이름", placeholder: "팝업스토어 이름을 입력해 주세요.") - addRowTextField(leftTitle: "이미지", placeholder: "팝업스토어 대표 이미지를 업로드 해주세요.") - - categoryButton.addTarget(self, action: #selector(didTapCategoryButton), for: .touchUpInside) - addRowCustom(leftTitle: "카테고리", rightView: categoryButton) - - // (위치) => 2줄 - // 1) 주소 (TextField) - let addressField = makeRoundedTextField("팝업스토어 주소를 입력해 주세요.") - self.addressField = addressField - addressField.snp.makeConstraints { make in - addressField.addTarget(self, action: #selector(fieldDidChange(_:)), for: .editingChanged) - - } - - // 2) (위도 Label + TF) + (경도 Label + TF) - let latLabel = makePlainLabel("위도") - let latField = makeRoundedTextField("") - latField.textAlignment = .center - self.latField = latField // latField와 연결 - latField.addTarget(self, action: #selector(fieldDidChange(_:)), for: .editingChanged) - - - let lonLabel = makePlainLabel("경도") - let lonField = makeRoundedTextField("") - self.lonField = lonField // lonField와 연결 - lonField.textAlignment = .center - lonField.addTarget(self, action: #selector(fieldDidChange(_:)), for: .editingChanged) - - - let latStack = UIStackView(arrangedSubviews: [latLabel, latField]) - latStack.axis = .horizontal - latStack.spacing = 8 - latStack.distribution = .fillProportionally - - let lonStack = UIStackView(arrangedSubviews: [lonLabel, lonField]) - lonStack.axis = .horizontal - lonStack.spacing = 8 - lonStack.distribution = .fillProportionally - - let latLonRow = UIStackView(arrangedSubviews: [latStack, lonStack]) - latLonRow.axis = .horizontal - latLonRow.spacing = 16 - latLonRow.distribution = .fillEqually - - // 수직 스택(주소, latLonRow) - let locationVStack = UIStackView(arrangedSubviews: [addressField, latLonRow]) - locationVStack.axis = .vertical - locationVStack.spacing = 8 - locationVStack.distribution = .fillEqually - - - // 한 행에 왼쪽 "위치", 오른쪽 2줄(주소 / 위도경도) - addRowCustom(leftTitle: "위치", rightView: locationVStack, rowHeight: nil, totalHeight: 80) - - // (마커) => 2줄 - // 1) (마커명 Label + TF) - let markerLabel = makePlainLabel("마커명") - - let markerField = makeRoundedTextField("") - - let markerStackH = UIStackView(arrangedSubviews: [markerLabel, markerField]) - markerStackH.axis = .horizontal - markerStackH.spacing = 8 - markerStackH.distribution = .fillProportionally - - // 2) (스니펫 Label + TF) - let snippetLabel = makePlainLabel("스니펫") - let snippetField = makeRoundedTextField("") - let snippetStackH = UIStackView(arrangedSubviews: [snippetLabel, snippetField]) - snippetStackH.axis = .horizontal - snippetStackH.spacing = 8 - snippetStackH.distribution = .fillProportionally - - // 수직 - let markerVStack = UIStackView(arrangedSubviews: [markerStackH, snippetStackH]) - markerVStack.axis = .vertical - markerVStack.spacing = 8 - markerVStack.distribution = .fillEqually - - - // 한 행 => "마커" 라벨, 오른쪽 2줄 (마커명, 스니펫) - addRowCustom(leftTitle: "마커", rightView: markerVStack, rowHeight: nil, totalHeight: 80) - - // (10) 기간 - periodButton.addTarget(self, action: #selector(didTapPeriodButton), for: .touchUpInside) - addRowCustom(leftTitle: "기간", rightView: periodButton) - - // (11) 시간 - timeButton.addTarget(self, action: #selector(didTapTimeButton), for: .touchUpInside) - addRowCustom(leftTitle: "시간", rightView: timeButton) - - // (12) 작성자 - let writerLbl = makeSimpleLabel(nickname) - addRowCustom(leftTitle: "작성자", rightView: writerLbl) - - // (13) 작성시간 - let timeLbl = makeSimpleLabel("2025.01.06 10:30") - addRowCustom(leftTitle: "작성시간", rightView: timeLbl) - - // (14) 상태값 - let statusLbl = makeSimpleLabel("진행") - addRowCustom(leftTitle: "상태값", rightView: statusLbl) - - // (15) 설명 - let descTV = makeRoundedTextView() - self.descTV = descTV // 설명 필드 연결 - addRowCustom(leftTitle: "설명", rightView: descTV, rowHeight: nil, totalHeight: 120) - - } - - - // MARK: - Row - - private func addRowTextField(leftTitle: String, placeholder: String) { - let tf = makeRoundedTextField(placeholder) - if leftTitle == "이름" { - nameField = tf // 이름 필드 연결 - } else if leftTitle == "주소" { - addressField = tf // 주소 필드 연결 - } - addRowCustom(leftTitle: leftTitle, rightView: tf) - } - - private func setupImageCollectionUI() { - // 1) 상단 버튼들 (Add / RemoveAll) - let buttonStack = UIStackView(arrangedSubviews: [addImageButton, removeAllButton]) - buttonStack.axis = .horizontal - buttonStack.distribution = .fillEqually - buttonStack.spacing = 16 - - contentView.addSubview(buttonStack) - buttonStack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(40) - } - - // 2) CollectionView - contentView.addSubview(imagesCollectionView) - imagesCollectionView.snp.makeConstraints { make in - make.top.equalTo(buttonStack.snp.bottom).offset(8) - make.left.right.equalToSuperview().inset(16) - make.height.equalTo(130) // 셀 높이(120) + 패딩 - } - - // formBackgroundView를 아래로 조금 내려야 한다면? - formBackgroundView.snp.remakeConstraints { make in - make.top.equalTo(imagesCollectionView.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - make.bottom.equalToSuperview() - } - } - private func setupImageCollectionActions() { - // (1) 이미지 추가 버튼 -> 앨범 열기 - addImageButton.rx.tap - .bind { [weak self] in - self?.showImagePicker() - } - .disposed(by: disposeBag) - - // (2) 전체 삭제 버튼 - removeAllButton.rx.tap - .bind { [weak self] in - self?.images.removeAll() - self?.imagesCollectionView.reloadData() - self?.updateSaveButtonState() - } - .disposed(by: disposeBag) - - saveButton.rx.tap - .bind { [weak self] in - guard let self = self else { return } - // 1) 유효성 검사 - if self.validateForm() { - // 2) OK -> 등록 로직 - self.doRegister() - } else { - // 3) 실패 -> Alert/toast - let alert = UIAlertController( - title: "필수값 미입력", - message: "필수 항목을 모두 입력해 주세요.", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "확인", style: .default)) - self.present(alert, animated: true, completion: nil) - } - } - .disposed(by: disposeBag) - } - // 저장 버튼 활성화 여부 갱신 - private func updateSaveButtonState() { - let isFormValid = validateForm() // 폼 유효성 검사 결과 - saveButton.isEnabled = isFormValid - saveButton.backgroundColor = isFormValid ? .systemBlue : .lightGray - } - - /** - rowHeight: 기본(41) - totalHeight: 2줄 필요한 경우(90~100), 3줄 등 필요 시 더 크게 - */ - private func addRowCustom(leftTitle: String, - rightView: UIView, - rowHeight: CGFloat? = 36, - totalHeight: CGFloat? = nil) { - let row = UIView() - row.backgroundColor = .white - - let leftBG = UIView() - leftBG.backgroundColor = UIColor(white: 0.94, alpha: 1) - row.addSubview(leftBG) - leftBG.snp.makeConstraints { make in - make.top.bottom.left.equalToSuperview() - make.width.equalTo(80) - } - - let leftLabel = UILabel() - leftLabel.text = leftTitle - leftLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold) - leftLabel.textColor = .black - leftLabel.textAlignment = .center - leftBG.addSubview(leftLabel) - leftLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.left.right.equalToSuperview().inset(8) - } - - let rightBG = UIView() - rightBG.backgroundColor = .white - row.addSubview(rightBG) - rightBG.snp.makeConstraints { make in - make.top.bottom.right.equalToSuperview() - make.left.equalTo(leftBG.snp.right) - } - - rightBG.addSubview(rightView) - rightView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(7) - make.bottom.equalToSuperview().offset(-7) - make.left.equalToSuperview().offset(8) - make.right.equalToSuperview().offset(-8) - if let fixH = rowHeight { - make.height.equalTo(fixH).priority(.medium) - } - } - - if let totalH = totalHeight { - row.snp.makeConstraints { make in - make.height.equalTo(totalH).priority(.high) - } - } else { - row.snp.makeConstraints { make in - make.height.greaterThanOrEqualTo(41) - } - } - - let separator = UIView() - separator.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) - row.addSubview(separator) - separator.snp.makeConstraints { make in - make.left.right.bottom.equalToSuperview() - make.height.equalTo(1) - } - - verticalStack.addArrangedSubview(row) - } - - @objc private func didTapPeriodButton() { - DateTimePickerManager.shared.showDateRange(on: self) { [weak self] start, end in - guard let self = self else { return } - self.selectedStartDate = start - self.selectedEndDate = end - self.updatePeriodButtonTitle() - self.updateSaveButtonState() // 날짜 선택 후 저장 버튼 상태 업데이트 - } - } - - @objc private func didTapTimeButton() { - DateTimePickerManager.shared.showTimeRange(on: self) { [weak self] st, et in - guard let self = self else { return } - self.selectedStartTime = st - self.selectedEndTime = et - - // 디버깅을 위한 로그 추가 - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss" - Logger.log(message: "시간 선택 완료 - 시작: \(formatter.string(from: st)), 종료: \(formatter.string(from: et))", category: .debug) - - self.updateTimeButtonTitle() - self.updateSaveButtonState() - } - } - - - private func updatePeriodButtonTitle() { - guard let s = selectedStartDate, let e = selectedEndDate else { return } - let df = DateFormatter() - df.dateFormat = "yyyy.MM.dd" - let sStr = df.string(from: s) - let eStr = df.string(from: e) - - periodButton.setTitle("\(sStr) ~ \(eStr)", for: .normal) - } - - private func updateTimeButtonTitle() { - guard let st = selectedStartTime, let et = selectedEndTime else { return } - let df = DateFormatter() - df.dateFormat = "HH:mm" - let stStr = df.string(from: st) - let etStr = df.string(from: et) - timeButton.setTitle("\(stStr) ~ \(etStr)", for: .normal) - } - - // MARK: - Category Selection - - @objc private func didTapCategoryButton() { - let alertController = UIAlertController(title: "카테고리 선택", message: nil, preferredStyle: .actionSheet) - - // 기존 카테고리 목록 추가 - for category in categories { - let action = UIAlertAction(title: category, style: .default) { [weak self] _ in - self?.updateCategoryButtonTitle(with: category) - } - alertController.addAction(action) - } - - // '카테고리 추가' 옵션 추가 - let addAction = UIAlertAction(title: "카테고리 추가", style: .default) { [weak self] _ in - self?.presentAddCategoryAlert() - } - alertController.addAction(addAction) - - // 취소 버튼 추가 - let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - - // iPad에서 액션 시트가 크래시되지 않도록 설정 - if let popoverController = alertController.popoverPresentationController { - popoverController.sourceView = categoryButton - popoverController.sourceRect = categoryButton.bounds - } - - present(alertController, animated: true, completion: nil) - } - - private func presentAddCategoryAlert() { - let alert = UIAlertController(title: "새 카테고리 추가", message: "추가할 카테고리 이름을 입력하세요.", preferredStyle: .alert) - - alert.addTextField { textField in - textField.placeholder = "카테고리 이름" - } - - let addAction = UIAlertAction(title: "추가", style: .default) { [weak self] _ in - guard let self = self else { return } - if let newCategory = alert.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !newCategory.isEmpty { - // 중복 체크 - if self.categories.contains(newCategory) { - self.presentDuplicateCategoryAlert() - } else { - self.categories.append(newCategory) - self.updateCategoryButtonTitle(with: newCategory) - } - } - } - - let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - - alert.addAction(addAction) - alert.addAction(cancelAction) - - present(alert, animated: true, completion: nil) - } - private func showImagePicker() { - // 1) PHPicker 설정 - var configuration = PHPickerConfiguration() - configuration.filter = .images // 이미지만 - configuration.selectionLimit = 0 // 0이면 무제한, 혹은 10, 5 등 제한 가능 - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - self.pickerViewController = picker - - // 2) 모달 표시 - present(picker, animated: true, completion: nil) - } - - private func presentDuplicateCategoryAlert() { - let alert = UIAlertController(title: "중복", message: "이미 존재하는 카테고리입니다.", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "확인", style: .default, handler: nil)) - present(alert, animated: true, completion: nil) - } - - private func updateCategoryButtonTitle(with category: String) { - categoryButton.setTitle("\(category) ▾", for: .normal) - } - - // MARK: - UI Helpers - private func makeRoundedTextField(_ placeholder: String) -> UITextField { - let tf = UITextField() - tf.placeholder = placeholder - tf.font = UIFont.systemFont(ofSize:14) - tf.textColor = .darkGray - tf.borderStyle = .none - tf.layer.cornerRadius = 8 - tf.layer.borderWidth = 1 - tf.layer.borderColor = UIColor.lightGray.cgColor - tf.setLeftPaddingPoints(8) - return tf - } - - private func makeRoundedButton(_ title: String) -> UIButton { - let btn = UIButton(type: .system) - btn.setTitle(title, for: .normal) - btn.setTitleColor(.darkGray, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize:14) - btn.layer.cornerRadius = 8 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.lightGray.cgColor - btn.contentHorizontalAlignment = .left - btn.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) - return btn - } - - private func makeIconButton(_ title: String, iconName: String) -> UIButton { - let btn = makeRoundedButton(title) - if let icon = UIImage(named: iconName) { - btn.setImage(icon, for: .normal) - btn.imageView?.contentMode = .scaleAspectFit - btn.titleEdgeInsets = UIEdgeInsets(top:0, left:6, bottom:0, right:0) - } - return btn - } - - private func makeSimpleLabel(_ text: String) -> UILabel { - let lbl = UILabel() - lbl.text = text - lbl.font = UIFont.systemFont(ofSize:14) - lbl.textColor = .darkGray - return lbl - } - - private func makePlainLabel(_ text: String) -> UILabel { - // 작은 라벨(위도/경도/마커명/스니펫 등) - let lbl = UILabel() - lbl.text = text - lbl.font = UIFont.systemFont(ofSize:14) - lbl.textColor = .darkGray - lbl.textAlignment = .right - lbl.setContentHuggingPriority(.required, for: .horizontal) - return lbl - } - - private func makeRoundedTextView() -> UITextView { - let tv = UITextView() - tv.font = UIFont.systemFont(ofSize:14) - tv.textColor = .darkGray - tv.layer.cornerRadius = 8 - tv.layer.borderWidth = 1 - tv.layer.borderColor = UIColor.lightGray.cgColor - tv.textContainerInset = UIEdgeInsets(top:7, left:7, bottom:7, right:7) - tv.isScrollEnabled = true - return tv - } -} - -// MARK: - Padding -private extension UITextField { - func setLeftPaddingPoints(_ amount: CGFloat){ - let paddingView = UIView(frame: CGRect(x:0, y:0, width:amount, height: frame.size.height)) - leftView = paddingView - leftViewMode = .always - } -} -extension PopUpStoreRegisterViewController: UICollectionViewDataSource, UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return images.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: ImageCell.identifier, - for: indexPath - ) as? ImageCell else { - return UICollectionViewCell() - } - - let item = images[indexPath.item] - cell.configure(with: item) - - // 대표이미지 변경 - cell.onMainCheckToggled = { [weak self] in - self?.toggleMainImage(index: indexPath.item) - } - // 개별 삭제 - cell.onDeleteTapped = { [weak self] in - self?.deleteImage(index: indexPath.item) - } - - return cell - } -} - -// 헬퍼 메서드들 -private extension PopUpStoreRegisterViewController { - /// 대표이미지를 단 하나만 허용 -> 누른 index만 isMain = true - func toggleMainImage(index: Int) { - for i in 0.. Bool { - // (1) 팝업스토어 이름 - Logger.log(message: "nameField.text = \(nameField?.text ?? "nil")", category: .debug) - guard let nameField = nameField, !(nameField.text ?? "").isEmpty else { - Logger.log( - message: "이름 필드가 비어 있습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - // (2) 카테고리 선택 - if categoryButton.title(for: .normal) == "카테고리 선택 ▾" { - Logger.log( - message: "카테고리가 선택되지 않았습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - // (3) 주소 - Logger.log(message: "addressField = \(addressField != nil ? "초기화됨" : "nil")", category: .debug) - Logger.log(message: "addressField.text = \(addressField?.text ?? "nil")", category: .debug) - guard let addressField = addressField, !(addressField.text ?? "").isEmpty else { - Logger.log(message: "주소 필드가 비어 있습니다.", category: .debug) - return false - } - - - // (4) 위도/경도 - Logger.log(message: "latField.text = \(latField?.text ?? "nil")", category: .debug) - Logger.log(message: "lonField.text = \(lonField?.text ?? "nil")", category: .debug) - guard let latField = latField, - let lonField = lonField, - let latText = latField.text, !latText.isEmpty, - let lonText = lonField.text, !lonText.isEmpty, - let latVal = Double(latText), let lonVal = Double(lonText), - latVal != 0 || lonVal != 0 else { - Logger.log( - message: "위도/경도 값이 잘못되었습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - // (5) 설명 - guard let descTV = descTV, !(descTV.text ?? "").isEmpty else { - Logger.log( - message: "설명 필드가 비어 있습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - // (6) 이미지 ≥ 1장 - if images.isEmpty { - Logger.log( - message: "이미지가 추가되지 않았습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - // (7) 대표 이미지 설정 여부 - if !images.contains(where: { $0.isMain }) { - Logger.log( - message: "대표 이미지가 설정되지 않았습니다.", - category: .debug, - fileName: #file, - line: #line - ) - return false - } - - Logger.log( - message: "모든 조건이 충족되었습니다.", - category: .info, - fileName: #file, - line: #line - ) - return true - } - private func doRegister() { - Logger.log(message: "doRegister() 호출됨", category: .debug) - - // 1. 폼 데이터 검증 - guard validateFormData() else { return } - - if let editingStore = editingStore { - // 수정 모드 - updateStore(editingStore) - } else { - // 새로 등록 모드 - // 2. 이미지 업로드 실행 - uploadImages() - } - } - - - // 폼 데이터 검증 - private func validateFormData() -> Bool { - guard let name = nameField?.text, - let address = addressField?.text, - let latitude = latField?.text, Double(latitude) != nil, - let longitude = lonField?.text, Double(longitude) != nil, - let description = descTV?.text, - !images.isEmpty else { - Logger.log(message: "폼 데이터 검증 실패", category: .error) - return false - } - Logger.log(message: "폼 데이터 검증 성공", category: .debug) - return true - } - private func updateStore(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { - // 이미지가 수정되었다면 먼저 이미지 업로드 - if !images.isEmpty { - uploadImagesForUpdate(store) - } else { - // 이미지 수정이 없다면 바로 스토어 정보 업데이트 - updateStoreInfo(store, updatedImagePaths: nil) - } - } - private func uploadImagesForUpdate(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { - let uuid = UUID().uuidString - let updatedImages = images.enumerated().map { index, image in - let filePath = "PopUpImage/\(nameField?.text ?? "")/\(uuid)/\(index).jpg" - return ExtendedImage( - filePath: filePath, - image: image.image, - isMain: image.isMain) - } - - presignedService.tryUpload(datas: updatedImages.map { - PreSignedService.PresignedURLRequest(filePath: $0.filePath, image: $0.image) - }) - .subscribe( - onSuccess: { [weak self] _ in - guard let self = self else { return } - Logger.log(message: "이미지 업로드 성공", category: .info) - let imagePaths = updatedImages.map { $0.filePath } - self.updateStoreInfo(store, updatedImagePaths: imagePaths) - }, - onError: { [weak self] error in - Logger.log(message: "이미지 업로드 실패: \(error.localizedDescription)", category: .error) - self?.showErrorAlert(message: "이미지 업로드 실패: \(error.localizedDescription)") - } - ) - .disposed(by: disposeBag) - } - - private func updateStoreInfo(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore, updatedImagePaths: [String]?) { - guard let name = nameField?.text, - let address = addressField?.text, - let latitude = Double(latField?.text ?? ""), - let longitude = Double(lonField?.text ?? ""), - let description = descTV?.text, - let categoryTitle = categoryButton.title(for: .normal)?.replacingOccurrences(of: " ▾", with: "") else { - return - } - - // 업데이트할 이미지가 있다면 첫 번째 값을 대표 이미지로, 없으면 기존 스토어의 mainImageUrl 사용 - let mainImage = updatedImagePaths?.first ?? store.mainImageUrl - - let request = UpdatePopUpStoreRequestDTO( - popUpStore: .init( - id: store.id, - name: name, - categoryId: Int64(getCategoryId(from: categoryTitle)), - desc: description, - address: address, - startDate: getFormattedDate(from: selectedStartDate), - endDate: getFormattedDate(from: selectedEndDate), - mainImageUrl: mainImage, - bannerYn: !mainImage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, - imageUrl: updatedImagePaths ?? [store.mainImageUrl], - startDateBeforeEndDate: true - ), - location: .init( - latitude: latitude, - longitude: longitude, - markerTitle: "마커 제목", - markerSnippet: "마커 설명" - ), - imagesToAdd: updatedImagePaths ?? [], - imagesToDelete: [] // 필요한 경우 기존 이미지 삭제 로직 추가 - ) - - - - adminUseCase.updateStore(request: request) - .subscribe( - onNext: { [weak self] _ in - Logger.log(message: "updateStore API 호출 성공", category: .info) - self?.showSuccessAlert(isUpdate: true) - }, - onError: { [weak self] error in - Logger.log(message: "updateStore API 호출 실패: \(error.localizedDescription)", category: .error) - self?.showErrorAlert(message: error.localizedDescription) - } - ) - .disposed(by: disposeBag) - } - - private func showSuccessAlert(isUpdate: Bool = false) { - let message = isUpdate ? "팝업스토어가 성공적으로 수정되었습니다." : "팝업스토어가 성공적으로 등록되었습니다." - let alert = UIAlertController( - title: isUpdate ? "수정 성공" : "등록 성공", - message: message, - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "확인", style: .default) { [weak self] _ in - self?.completionHandler?() // 목록 새로고침 - self?.navigationController?.popViewController(animated: true) - }) - present(alert, animated: true) - } - - // 이미지 업로드 - private func uploadImages() { - let uuid = UUID().uuidString - // let baseS3URL = Secrets.popPoolS3BaseURL.rawValue - let updatedImages = images.enumerated().map { index, image in - let filePath = "PopUpImage/\(nameField?.text ?? "")/\(uuid)/\(index).jpg" - return ExtendedImage( - filePath: filePath, - image: image.image, - isMain: image.isMain) - } - - // let presignedService = PreSignedService() - presignedService.tryUpload(datas: updatedImages.map { - PreSignedService.PresignedURLRequest(filePath: $0.filePath, image: $0.image) - }) - // .observe(on: MainScheduler.instance) - .subscribe( - onSuccess: { [weak self] _ in - guard let self = self else { return } - Logger.log(message: "이미지 업로드 성공", category: .info) - - let imagePaths = updatedImages.map { $0.filePath } - let mainImage = updatedImages.first { $0.isMain }?.filePath ?? "" - self.callCreateStoreAPI(mainImage: mainImage, imagePaths: imagePaths) // baseURL 제거 - }, - onError: { error in - Logger.log(message: "이미지 업로드 실패: \(error.localizedDescription)", category: .error) - self.showErrorAlert(message: "이미지 업로드 실패: \(error.localizedDescription)") - } - ) - .disposed(by: disposeBag) - } - - // createStore API 호출 - private func callCreateStoreAPI(mainImage: String, imagePaths: [String]) { - guard let name = nameField?.text, - let address = addressField?.text, - let latitude = Double(latField?.text ?? ""), - let longitude = Double(lonField?.text ?? ""), - let description = descTV?.text, - let categoryTitle = categoryButton.title(for: .normal)?.replacingOccurrences(of: " ▾", with: "") else { - Logger.log(message: "필수 입력값이 비어 있음", category: .error) - return - } - - let categoryId = getCategoryId(from: categoryTitle) - - Logger.log( - message: """ - 팝업스토어 등록 요청: - - 이름: \(name) - - 카테고리: \(categoryTitle) (ID: \(categoryId)) - - 주소: \(address) - - 위도/경도: (\(latitude), \(longitude)) - - 설명: \(description) - - 시작일: \(getFormattedDate(from: selectedStartDate)) - - 종료일: \(getFormattedDate(from: selectedEndDate)) - - 메인이미지: \(mainImage) - - 전체이미지: \(imagePaths) - """, - category: .network - ) - - let bannerYn = !mainImage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - let dates = prepareDateTime() - let isValidDateOrder = validateDates(start: selectedStartDate, end: selectedEndDate) - - let request = CreatePopUpStoreRequestDTO( - name: name, - categoryId: Int64(categoryId), - desc: description, - address: address, - startDate: dates.startDate, - endDate: dates.endDate, - mainImageUrl: mainImage, - imageUrlList: imagePaths, - latitude: latitude, - longitude: longitude, - markerTitle: "마커 제목", - markerSnippet: "마커 설명", - startDateBeforeEndDate: isValidDateOrder - ) - - - adminUseCase.createStore(request: request) - .subscribe( - onNext: { [weak self] _ in - Logger.log(message: "createStore API 호출 성공", category: .info) - self?.showSuccessAlert() - }, - onError: { [weak self] error in - Logger.log(message: "createStore API 호출 실패: \(error.localizedDescription)", category: .error) - self?.showErrorAlert(message: error.localizedDescription) - } - ) - .disposed(by: disposeBag) - } - private func getCategoryId(from title: String) -> Int { - Logger.log(message: "카테고리 매핑 시작 - 타이틀: \(title)", category: .debug) - - let categoryMap: [String: Int64] = [ - "패션": 1, - "라이프스타일": 2, - "뷰티": 3, - "음식/요리": 4, - "예술": 5, - "반려동물": 6, - "여행": 7, - "엔터테인먼트": 8, - "애니메이션": 9, - "키즈": 10, - "스포츠": 11, - "게임": 12 - ] - - if let id = categoryMap[title] { - Logger.log(message: "카테고리 매핑 성공: \(title) -> \(id)", category: .debug) - return Int(id) - } else { - Logger.log(message: "카테고리 매핑 실패: \(title)에 해당하는 ID를 찾을 수 없음", category: .error) - return 1 // 기본값 - } - } - - - private func createDateTime(date: Date?, time: Date?) -> Date? { - guard let date = date else { return nil } - - if let time = time { - let calendar = Calendar.current - - // 날짜 부분 추출 - var components = calendar.dateComponents([.year, .month, .day], from: date) - - // 시간 부분 추출 - let timeComponents = calendar.dateComponents([.hour, .minute], from: time) - - // 날짜와 시간 결합 - components.hour = timeComponents.hour - components.minute = timeComponents.minute - components.second = 0 - - // 명시적으로 한국 시간대 지정 - components.timeZone = TimeZone(identifier: "Asia/Seoul") - - return calendar.date(from: components) - } - - return date - } - - private func getFormattedDate(from date: Date?) -> String { - guard let date = date else { return "" } - - // 한국 시간대를 명시적으로 지정하고 ISO 8601 형식으로 변환 - let formatter = DateFormatter() - formatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 한국 시간대로 명시 - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - - // Z 표기를 추가하지 않음 (시간대 정보 없음) - return formatter.string(from: date) - } - - - - - private func prepareDateTime() -> (startDate: String, endDate: String) { - // 시작일/시간 결합 - let startDateTime = createDateTime(date: selectedStartDate, time: selectedStartTime) - - // 종료일/시간 결합 - let endDateTime = createDateTime(date: selectedEndDate, time: selectedEndTime) - - // 디버그용 로그 - if let startTime = selectedStartTime, let endTime = selectedEndTime { - let timeFormatter = DateFormatter() - timeFormatter.dateFormat = "HH:mm" - Logger.log(message: "선택된 시작 시간: \(timeFormatter.string(from: startTime))", category: .debug) - Logger.log(message: "선택된 종료 시간: \(timeFormatter.string(from: endTime))", category: .debug) - } - - // 결합된 날짜/시간 로깅 - if let start = startDateTime, let end = endDateTime { - let dateTimeFormatter = DateFormatter() - dateTimeFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - Logger.log(message: "결합된 시작 일시: \(dateTimeFormatter.string(from: start))", category: .debug) - Logger.log(message: "결합된 종료 일시: \(dateTimeFormatter.string(from: end))", category: .debug) - } - - let startDate = getFormattedDate(from: startDateTime) - let endDate = getFormattedDate(from: endDateTime) - - Logger.log(message: "서버로 전송될 시작 일시: \(startDate)", category: .debug) - Logger.log(message: "서버로 전송될 종료 일시: \(endDate)", category: .debug) - - return (startDate: startDate, endDate: endDate) - } - - // 새로운 검증 함수 추가 (prepareDateTime 함수 아래에 추가) - private func validateDates(start: Date?, end: Date?) -> Bool { - guard let start = start, let end = end else { return false } - return start < end - } - - - - - - private func showSuccessAlert() { - let alert = UIAlertController( - title: "등록 성공", - message: "팝업스토어가 성공적으로 등록되었습니다.", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "확인", style: .default, handler: { [weak self] _ in - // 성공 후 닫기와 핸들러 호출 - self?.completionHandler?() - self?.navigationController?.popViewController(animated: true) - })) - present(alert, animated: true) - } - private func showErrorAlert(message: String) { - let alert = UIAlertController( - title: "등록 실패", - message: message, - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "확인", style: .default)) - present(alert, animated: true) - } -} -extension UIView { - func findFirstResponder() -> UIView? { - if isFirstResponder { - return self - } - - for subview in subviews { - if let firstResponder = subview.findFirstResponder() { - return firstResponder - } - } - - return nil - } -} -extension PopUpStoreRegisterViewController: UITextFieldDelegate { - private func setupAddressField() { - // RxCocoa를 사용한 텍스트 필드 바인딩 - addressField?.rx.text.orEmpty - .distinctUntilChanged() - .debounce(.milliseconds(500), scheduler: MainScheduler.instance) - .filter { !$0.isEmpty } - .flatMapLatest { [weak self] address -> Observable in - return Observable.create { observer in - let geocoder = CLGeocoder() - let fullAddress = "\(address), Korea" - - geocoder.geocodeAddressString( - fullAddress, - in: nil, - preferredLocale: Locale(identifier: "ko_KR") - ) { placemarks, error in - if let error = error { - print("Geocoding error: \(error.localizedDescription)") - observer.onNext(nil) - observer.onCompleted() - return - } - - if let location = placemarks?.first?.location { - observer.onNext(location) - } else { - observer.onNext(nil) - } - observer.onCompleted() - } - - return Disposables.create() - } - } - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] location in - guard let location = location else { return } - self?.latField?.text = String(format: "%.6f", location.coordinate.latitude) - self?.lonField?.text = String(format: "%.6f", location.coordinate.longitude) - self?.updateSaveButtonState() - }) - .disposed(by: disposeBag) - } - - - @objc private func addressFieldDidChange(_ textField: UITextField) { - guard let address = textField.text, !address.isEmpty else { return } - - // 한국 주소임을 명시 - let geocoder = CLGeocoder() - let addressWithCountry = address + ", South Korea" - - geocoder.geocodeAddressString(addressWithCountry) { [weak self] placemarks, error in - if let error = error { - print("Geocoding error: \(error.localizedDescription)") - return - } - - guard let location = placemarks?.first?.location else { return } - - DispatchQueue.main.async { - self?.latField?.text = String(format: "%.6f", location.coordinate.latitude) - self?.lonField?.text = String(format: "%.6f", location.coordinate.longitude) - self?.updateSaveButtonState() - } - } - } - -} diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift deleted file mode 100644 index bcc448f4..00000000 --- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// MapPopUpStore.swift -// Poppool -// -// Created by 김기현 on 12/3/24. -// -import Foundation -import CoreLocation - -struct MapPopUpStore: Equatable { - let id: Int64 - let category: String - let name: String - let address: String - let startDate: String - let endDate: String - let latitude: Double - let longitude: Double - let markerId: Int64 - let markerTitle: String - let markerSnippet: String - let mainImageUrl: String? // 이미지 URL 추가 - - - var coordinate: CLLocationCoordinate2D { - CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - } -} - - extension MapPopUpStore { - func toMarkerInput() -> MapMarker.Input { - return MapMarker.Input( - isSelected: false, - isCluster: false, - regionName: self.markerTitle, // 또는 name이나 다른 적절한 필드 - count: 0 - ) - } - - - - func toStoreItem() -> StoreItem { - return StoreItem( - id: id, - thumbnailURL: mainImageUrl ?? "", // 이미지 URL 매핑 - category: category, - title: name, - location: address, - dateRange: "\(startDate) ~ \(endDate)", - isBookmarked: false // 기본값 - ) - } -} diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift deleted file mode 100644 index 53164882..00000000 --- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// MapRepository.swift -// Poppool -// -// Created by 김기현 on 12/3/24. -// - -import Foundation -import RxSwift - -protocol MapRepository { - func fetchStoresInBounds( - northEastLat: Double, - northEastLon: Double, - southWestLat: Double, - southWestLon: Double, - categories: [Int64] - ) -> Observable<[MapPopUpStoreDTO]> - - func searchStores( - query: String, - categories: [Int64] - ) -> Observable<[MapPopUpStoreDTO]> - - func fetchCategories() -> Observable<[Category]> -} - -// MARK: - Implementation -class DefaultMapRepository: MapRepository { - private let provider: Provider - - init(provider: Provider) { - self.provider = provider - } - - - func fetchStoresInBounds( - northEastLat: Double, - northEastLon: Double, - southWestLat: Double, - southWestLon: Double, - categories: [Int64] - ) -> Observable<[MapPopUpStoreDTO]> { - return provider.requestData( - with: MapAPIEndpoint.locations_fetchStoresInBounds( - northEastLat: northEastLat, - northEastLon: northEastLon, - southWestLat: southWestLat, - southWestLon: southWestLon, - categories: categories - ), - interceptor: TokenInterceptor() // ← 토큰 누락 해결 - ) - .map { $0.popUpStoreList } - } - - func searchStores( - query: String, - categories: [Int64] - ) -> Observable<[MapPopUpStoreDTO]> { - return provider.requestData( - with: MapAPIEndpoint.locations_searchStores( - query: query, - categories: categories - ), - interceptor: TokenInterceptor() // ← 토큰 누락 해결 - ) - .map { $0.popUpStoreList } - } - - - func fetchCategories() -> Observable<[Category]> { - Logger.log(message: "카테고리 매핑 요청을 시작합니다.", category: .network) - - return provider.requestData( - with: SignUpAPIEndpoint.signUp_getCategoryList(), - interceptor: TokenInterceptor() - ) - .do(onNext: { responseDTO in - Logger.log( - message: """ - 카테고리 매핑 응답: - - Response: \(responseDTO) - - categoryResponseList: \(responseDTO.categoryResponseList) - """, - category: .debug - ) - }) - .map { responseDTO in - let categories = responseDTO.categoryResponseList.map { $0.toDomain() } - Logger.log(message: "매핑된 카테고리 데이터: \(categories)", category: .debug) - return categories - } - .catch { error in - Logger.log( - message: "카테고리 매핑 요청 실패: \(error.localizedDescription)", - category: .error - ) - throw error - } - } - -} diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift deleted file mode 100644 index 1d68c933..00000000 --- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import RxSwift - -protocol MapUseCase { - func fetchCategories() -> Observable<[Category]> - func fetchStoresInBounds( - northEastLat: Double, - northEastLon: Double, - southWestLat: Double, - southWestLon: Double, - categories: [Int64] - ) -> Observable<[MapPopUpStore]> - - func searchStores( - query: String, - categories: [Int64] - ) -> Observable<[MapPopUpStore]> - - func filterStoresByLocation(_ stores: [MapPopUpStore], selectedRegions: [String]) -> [MapPopUpStore] - -} - -class DefaultMapUseCase: MapUseCase { - private let repository: MapRepository - - init(repository: MapRepository) { - self.repository = repository - } - - func fetchCategories() -> Observable<[Category]> { - return repository.fetchCategories() - } - - func fetchStoresInBounds( - northEastLat: Double, - northEastLon: Double, - southWestLat: Double, - southWestLon: Double, - categories: [Int64] - ) -> Observable<[MapPopUpStore]> { - - return repository.fetchStoresInBounds( - northEastLat: northEastLat, - northEastLon: northEastLon, - southWestLat: southWestLat, - southWestLon: southWestLon, - categories: categories // ← 그대로 넘긴다 - ) - .map { $0.map { $0.toDomain() } } - } - - - func searchStores( - query: String, - categories: [Int64] - ) -> Observable<[MapPopUpStore]> { - return repository.searchStores( - query: query, - categories: categories.map { Int64($0) ?? 0 } - ) - .map { $0.map { $0.toDomain() } } - } - func filterStoresByLocation(_ stores: [MapPopUpStore], selectedRegions: [String]) -> [MapPopUpStore] { - guard !selectedRegions.isEmpty else { return stores } - - return stores.filter { store in - let components = store.address.components(separatedBy: " ") - guard components.count >= 2 else { return false } - - let mainRegion = components[0].replacingOccurrences(of: "특별시", with: "") - .replacingOccurrences(of: "광역시", with: "") - let subRegion = components[1] - - return selectedRegions.contains("\(mainRegion)전체") || - selectedRegions.contains(subRegion) - } - } - } diff --git a/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift b/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift deleted file mode 100644 index c11757bb..00000000 --- a/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift +++ /dev/null @@ -1,192 +0,0 @@ -import Foundation -import RxSwift -import UIKit -import Alamofire - -protocol AdminRepository { - func fetchStoreList(query: String?, page: Int, size: Int) -> Observable - func fetchStoreDetail(id: Int64) -> Observable - func createStore(request: CreatePopUpStoreRequestDTO) -> Observable - func updateStore(request: UpdatePopUpStoreRequestDTO) -> Observable - func deleteStore(id: Int64) -> Observable - - func createNotice(request: CreateNoticeRequestDTO) -> Observable - func updateNotice(id: Int64, request: UpdateNoticeRequestDTO) -> Observable - func deleteNotice(id: Int64) -> Observable -} - -final class DefaultAdminRepository: AdminRepository { - - // MARK: - Properties - private let provider: Provider - private let tokenInterceptor = TokenInterceptor() - - // MARK: - Init - init(provider: Provider) { - self.provider = provider - } - - // MARK: - Store Methods - func fetchStoreList(query: String?, page: Int, size: Int) -> Observable { - let endpoint = AdminAPIEndpoint.fetchStoreList( - query: query, - page: page, - size: size - ) - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - } - - func fetchStoreDetail(id: Int64) -> Observable { - let endpoint = AdminAPIEndpoint.fetchStoreDetail(id: id) - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - .catch { error in - if case .responseSerializationFailed = error as? AFError { - // 빈 데이터 응답시 기본값 반환 - return Observable.just(GetAdminPopUpStoreDetailResponseDTO.empty) - } - throw error - } - } - - - func createStore(request: CreatePopUpStoreRequestDTO) -> Observable { - Logger.log(message: "createStore API 호출 시작", category: .info) - let endpoint = AdminAPIEndpoint.createStore(request: request) - Logger.log(message: "Request URL: \(endpoint.baseURL + endpoint.path)", category: .info) - Logger.log(message: "Request Body: \(request)", category: .info) - - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - .catch { error -> Observable in - if case .responseSerializationFailed(let reason) = error as? AFError, - case .inputDataNilOrZeroLength = reason { - // 빈 응답 데이터일 경우 성공으로 간주 - Logger.log(message: "빈 응답 데이터 처리: 성공으로 간주", category: .info) - return Observable.just(EmptyResponse()) - } - throw error - } - .do( - onNext: { _ in - Logger.log(message: "createStore API 호출 성공", category: .info) - }, - onError: { error in - Logger.log(message: "createStore API 호출 실패: \(error)", category: .error) - } - ) - } - - - - func updateStore(request: UpdatePopUpStoreRequestDTO) -> Observable { - let endpoint = AdminAPIEndpoint.updateStore(request: request) - - Logger.log(message: """ - Store Update 요청: - URL: \(endpoint.baseURL + endpoint.path) - Method: PUT - Request: \(request) - """, category: .debug) - - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - .catch { error -> Observable in - Logger.log(message: "Update Store Error 발생: \(error)", category: .error) - - if let afError = error as? AFError { - switch afError { - case .responseSerializationFailed(let reason): - Logger.log(message: "Serialization 실패 reason: \(reason)", category: .error) - if case .inputDataNilOrZeroLength = reason { - Logger.log(message: "빈 응답 데이터 - 성공으로 처리", category: .info) - return Observable.just(EmptyResponse()) - } - default: - Logger.log(message: "기타 AFError: \(afError)", category: .error) - } - } - - throw error - } - .do(onNext: { _ in - Logger.log(message: "Store Update 성공", category: .info) - }, onError: { error in - Logger.log(message: "Store Update 최종 실패: \(error)", category: .error) - }) - } - - - func deleteStore(id: Int64) -> Observable { - Logger.log(message: "deleteStore API 호출 시작", category: .info) - let endpoint = AdminAPIEndpoint.deleteStore(id: id) - return provider.request(with: endpoint, interceptor: tokenInterceptor) - .andThen(Observable.just(EmptyResponse())) - .do( - onNext: { _ in - Logger.log(message: "deleteStore API 호출 성공", category: .info) - }, - onError: { error in - Logger.log(message: "deleteStore API 호출 실패: \(error)", category: .error) - } - ) - } - - - // MARK: - Notice Methods - func createNotice(request: CreateNoticeRequestDTO) -> Observable { - let endpoint = AdminAPIEndpoint.createNotice(request: request) - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - } - - func updateNotice(id: Int64, request: UpdateNoticeRequestDTO) -> Observable { - let endpoint = AdminAPIEndpoint.updateNotice(id: id, request: request) - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - } - - func deleteNotice(id: Int64) -> Observable { - let endpoint = AdminAPIEndpoint.deleteNotice(id: id) - return provider.requestData( - with: endpoint, - interceptor: tokenInterceptor - ) - } -} -extension GetAdminPopUpStoreDetailResponseDTO { - static var empty: GetAdminPopUpStoreDetailResponseDTO { - return GetAdminPopUpStoreDetailResponseDTO( - id: 0, - name: "", - categoryId: 0, - categoryName: "", - desc: "", - address: "", - startDate: "", - endDate: "", - createUserId: "", - createDateTime: "", - mainImageUrl: "", - bannerYn: false, - imageList: [], - latitude: 0.0, - longitude: 0.0, - markerTitle: "", - markerSnippet: "" - ) - } -} diff --git a/Poppool/Poppool/Presentation/Admin/Domain/AdminUseCase.swift b/Poppool/Poppool/Presentation/Admin/Domain/AdminUseCase.swift deleted file mode 100644 index 870657d3..00000000 --- a/Poppool/Poppool/Presentation/Admin/Domain/AdminUseCase.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation -import RxSwift - -protocol AdminUseCase { - func fetchStoreList(query: String?, page: Int, size: Int) -> Observable - func fetchStoreDetail(id: Int64) -> Observable - func createStore(request: CreatePopUpStoreRequestDTO) -> Observable - func updateStore(request: UpdatePopUpStoreRequestDTO) -> Observable - func deleteStore(id: Int64) -> Observable - - // Notice - func createNotice(request: CreateNoticeRequestDTO) -> Observable - func updateNotice(id: Int64, request: UpdateNoticeRequestDTO) -> Observable - func deleteNotice(id: Int64) -> Observable -} - -final class DefaultAdminUseCase: AdminUseCase { - - private let repository: AdminRepository - - init(repository: AdminRepository) { - self.repository = repository - } - - func fetchStoreList(query: String?, page: Int, size: Int) -> Observable { - return repository.fetchStoreList(query: query, page: page, size: size) - } - - func fetchStoreDetail(id: Int64) -> Observable { - return repository.fetchStoreDetail(id: id) - } - - func createStore(request: CreatePopUpStoreRequestDTO) -> Observable { - Logger.log(message: "createStore 호출 - 요청 데이터: \(request)", category: .debug) - return repository.createStore(request: request) - .do(onNext: { _ in - Logger.log(message: "createStore 성공", category: .info) - }, onError: { error in - Logger.log(message: "createStore 실패 - Error: \(error)", category: .error) - }) - } - - func updateStore(request: UpdatePopUpStoreRequestDTO) -> Observable { - Logger.log(message: """ - Updating store with location: - Latitude: \(request.location.latitude) - Longitude: \(request.location.longitude) - """, category: .debug) - - return repository.updateStore(request: request) - .do(onNext: { _ in - Logger.log(message: "Store update successful", category: .debug) - }, onError: { error in - Logger.log(message: "Store update failed: \(error)", category: .error) - }) - } - - func deleteStore(id: Int64) -> Observable { - return repository.deleteStore(id: id) - } - - // Notice - func createNotice(request: CreateNoticeRequestDTO) -> Observable { - return repository.createNotice(request: request) - } - - func updateNotice(id: Int64, request: UpdateNoticeRequestDTO) -> Observable { - return repository.updateNotice(id: id, request: request) - } - - func deleteNotice(id: Int64) -> Observable { - return repository.deleteNotice(id: id) - } -} diff --git a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift deleted file mode 100644 index 99d29f89..00000000 --- a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift +++ /dev/null @@ -1,306 +0,0 @@ -//// -//// PopUpStoreRegisterReactor.swift -//// Poppool -//// -//// Created by 김기현 on 1/14/25. -//// -// -//import Foundation -//import ReactorKit -//import RxSwift -//import UIKit -// -//final class PopUpStoreRegisterReactor: Reactor { -// -// // MARK: - Action -// enum Action { -// /// 화면 최초 로드 -// case viewDidLoad -// -// /// 사용자 입력값 갱신 -// case updateName(String) -// case updateDesc(String) -// case updateCategory(String) -// case updateAddress(String) -// case updateLatitude(String) // 문자열 -> Double 변환 -// case updateLongitude(String) -// case updateMarkerTitle(String) -// case updateMarkerSnippet(String) -// case updateStartDate(Date) -// case updateEndDate(Date) -// case updateStartTime(Date) -// case updateEndTime(Date) -// -// /// 이미지 관련 -// case addImage(ExtendedImage) // 개별 이미지 추가 -// case removeImage(Int) // 특정 인덱스 이미지 삭제 -// case toggleMainImage(Int) // 대표이미지 토글 -// -// /// "저장/등록" 버튼 탭 -// case tapRegister -// } -// -// // MARK: - Mutation -// enum Mutation { -// /// 폼 데이터 갱신 -// case setName(String) -// case setDesc(String) -// case setCategory(String) -// case setAddress(String) -// case setLatitude(Double) -// case setLongitude(Double) -// case setMarkerTitle(String) -// case setMarkerSnippet(String) -// case setStartDate(Date?) -// case setEndDate(Date?) -// case setStartTime(Date?) -// case setEndTime(Date?) -// -// /// 이미지 변경 -// case addImage(ExtendedImage) -// case removeImageAt(Int) -// case toggleMain(Int) -// -// /// 등록 성공 여부 -// case setRegistered(Bool) -// } -// -// // MARK: - State -// struct State { -// // 폼 입력값 -// var name: String = "" -// var desc: String = "" -// var category: String = "게임" -// var address: String = "" -// var latitude: Double = 0 -// var longitude: Double = 0 -// var markerTitle: String = "" -// var markerSnippet: String = "" -// var startDate: Date? -// var endDate: Date? -// var startTime: Date? -// var endTime: Date? -// -// -// // 이미지 목록 -// var images: [ExtendedImage] = [] -// -// // 최종 등록 여부 -// var isRegistered: Bool = false -// } -// -// // ReactorKit 필수 -// let initialState: State = State() -// -// // 주입받는 의존성 -// private let adminUseCase: AdminUseCase -// -// // disposeBag (mutate 안에서는 ReactorKit이 관리) -// private let disposeBagInternal = DisposeBag() -// -// // MARK: - Init -// init(adminUseCase: AdminUseCase) { -// self.adminUseCase = adminUseCase -// } -// -// // MARK: - mutate -// func mutate(action: Action) -> Observable { -// switch action { -// -// case .viewDidLoad: -// return .empty() -// -// // 텍스트 입력 업데이트 -// case let .updateName(name): -// return .just(.setName(name)) -// -// case let .updateDesc(desc): -// return .just(.setDesc(desc)) -// -// case let .updateCategory(cat): -// return .just(.setCategory(cat)) -// -// case let .updateAddress(addr): -// return .just(.setAddress(addr)) -// -// case let .updateLatitude(latString): -// // 문자 -> Double 변환 -// let lat = Double(latString) ?? 0 -// return .just(.setLatitude(lat)) -// -// case let .updateLongitude(lonString): -// let lon = Double(lonString) ?? 0 -// return .just(.setLongitude(lon)) -// -// case let .updateMarkerTitle(title): -// return .just(.setMarkerTitle(title)) -// -// case let .updateMarkerSnippet(snippet): -// return .just(.setMarkerSnippet(snippet)) -// -// case let .updateStartDate(date): -// return .just(.setStartDate(date)) -// -// case let .updateEndDate(date): -// return .just(.setEndDate(date)) -// -// case let .updateStartTime(time): -// return .just(.setStartTime(time)) -// -// case let .updateEndTime(time): -// return .just(.setEndTime(time)) -// -// // 이미지 관련 -// case let .addImage(img): -// return .just(.addImage(img)) -// -// case let .removeImage(index): -// return .just(.removeImageAt(index)) -// -// case let .toggleMainImage(index): -// return .just(.toggleMain(index)) -// -// // "저장" 액션 -// case .tapRegister: -// return doRegister() -// } -// } -// -// // MARK: - reduce -// func reduce(state: State, mutation: Mutation) -> State { -// var newState = state -// -// switch mutation { -// case let .setName(name): -// newState.name = name -// -// case let .setDesc(desc): -// newState.desc = desc -// -// case let .setCategory(cat): -// newState.category = cat -// -// case let .setAddress(addr): -// newState.address = addr -// -// case let .setLatitude(lat): -// newState.latitude = lat -// -// case let .setLongitude(lon): -// newState.longitude = lon -// -// case let .setMarkerTitle(title): -// newState.markerTitle = title -// -// case let .setMarkerSnippet(snippet): -// newState.markerSnippet = snippet -// -// case let .setStartDate(date): -// newState.startDate = date -// -// case let .setEndDate(date): -// newState.endDate = date -// -// case let .setStartTime(time): -// newState.startTime = time -// -// case let .setEndTime(time): -// newState.endTime = time -// -// // 이미지 -// case let .addImage(img): -// newState.images.append(img) -// -// case let .removeImageAt(index): -// if index >= 0 && index < newState.images.count { -// newState.images.remove(at: index) -// } -// -// case let .toggleMain(idx): -// // 모든 이미지 isMain=false 후 idx만 true -// for i in 0.. Observable { -// // 1) 폼 유효성 검사 -// guard validateForm() else { -// // 유효성 실패시엔 Mutation 없이 .empty() (혹은 에러 Mutation) -// return .empty() -// } -// -// // 2) 대표 vs 서브 이미지 -// let mainImg = currentState.images.first(where: { $0.isMain }) -// ?? currentState.images.first! -// let mainUrl = mainImg.filePath -// let subImages = currentState.images -// .filter { $0.filePath != mainUrl } -// .map { $0.filePath } -// -// // 3) 날짜/시간 -> 문자열 변환 -// let dateFormatter = DateFormatter() -// dateFormatter.dateFormat = "yyyy-MM-dd" -// let startDateStr = currentState.startDate.map { dateFormatter.string(from: $0) } ?? "2025-01-01" -// let endDateStr = currentState.endDate.map { dateFormatter.string(from: $0) } ?? "2025-12-31" -// -// // 4) DTO -// let request = CreatePopUpStoreRequestDTO( -// name: currentState.name, -// categoryId: convertCategoryToId(currentState.category), -// desc: currentState.desc, -// address: currentState.address, -// startDate: startDateStr, -// endDate: endDateStr, -// mainImageUrl: mainUrl, -// bannerYn: false, -// imageUrlList: subImages, -// latitude: currentState.latitude, -// longitude: currentState.longitude, -// markerTitle: currentState.markerTitle, -// markerSnippet: currentState.markerSnippet, -// startDateBeforeEndDate: true -// ) -// -// // 5) 서버 호출 -> 결과에 따라 Mutation -// return adminUseCase.createStore(request: request) -// .map { _ in Mutation.setRegistered(true) } -// .catch { error in -// // 에러 시 로깅/별도 처리 -// return .empty() -// } -// .asObservable() -// } -// -// // MARK: - validateForm() -// private func validateForm() -> Bool { -// // 간단 예시 -// if currentState.name.isEmpty { return false } -// if currentState.desc.isEmpty { return false } -// if currentState.address.isEmpty { return false } -// if currentState.latitude == 0 && currentState.longitude == 0 { return false } -// if currentState.markerTitle.isEmpty || currentState.markerSnippet.isEmpty { return false } -// // 이미지 >=1, 대표 1장 -// if currentState.images.isEmpty { return false } -// if !currentState.images.contains(where: { $0.isMain }) { return false } -// return true -// } -// -// /// 예시: 카테고리 문자열 -> ID 변환 (임의 로직) -// private func convertCategoryToId(_ cat: String) -> Int64 { -// switch cat { -// case "게임": return 101 -// case "라이프스타일": return 102 -// default: return 100 -// } -// } -//} diff --git a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift deleted file mode 100644 index 55c20d1c..00000000 --- a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift +++ /dev/null @@ -1,424 +0,0 @@ -//// -//// PopUpStoreRegisterView.swift -//// Poppool -//// -//// Created by 김기현 on 1/14/25. -//// -// -//import UIKit -//import SnapKit -//import Then -// -//final class PopUpStoreRegisterView: UIView { -// -// // MARK: - Callbacks (Closure) -// /// "이미지 추가" 버튼 탭 -// var onAddImageTapped: (() -> Void)? -// /// "전체삭제" 버튼 탭 -// var onRemoveAllTapped: (() -> Void)? -// /// 대표이미지 체크 토글 (콜렉션셀에서 index 전달) -// var onToggleMainImage: ((Int) -> Void)? -// /// 개별 이미지 삭제(index) -// var onDeleteImage: ((Int) -> Void)? -// -// /// "카테고리 선택" 버튼 탭 -// var onCategoryButtonTapped: (() -> Void)? -// /// "기간 선택" 버튼 탭 -// var onPeriodButtonTapped: (() -> Void)? -// /// "시간 선택" 버튼 탭 -// var onTimeButtonTapped: (() -> Void)? -// /// "저장" 버튼 탭 -// var onSaveTapped: (() -> Void)? -// -// // MARK: - Subviews -// // (1) 상단 "이름" 입력 필드 -// private let nameTextField = UITextField().then { -// $0.placeholder = "팝업스토어 이름을 입력해 주세요." -// $0.font = .systemFont(ofSize: 14) -// $0.textColor = .darkGray -// $0.borderStyle = .roundedRect -// } -// -// // (2) 이미지 버튼들 -// private let addImageButton = UIButton(type: .system).then { -// $0.setTitle("이미지 추가", for: .normal) -// $0.setTitleColor(.systemBlue, for: .normal) -// } -// private let removeAllButton = UIButton(type: .system).then { -// $0.setTitle("전체 삭제", for: .normal) -// $0.setTitleColor(.red, for: .normal) -// } -// -// // (3) 이미지 콜렉션뷰 -// private let imagesCollectionView: UICollectionView = { -// let layout = UICollectionViewFlowLayout() -// layout.scrollDirection = .horizontal -// layout.itemSize = CGSize(width: 80, height: 100) -// layout.minimumLineSpacing = 8 -// -// let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) -// cv.backgroundColor = .clear -// cv.register(PopUpImageCell.self, forCellWithReuseIdentifier: PopUpImageCell.identifier) -// return cv -// }() -// -// // (4) 카테고리/기간/시간 버튼 -// private let categoryButton = UIButton(type: .system).then { -// $0.setTitle("카테고리 선택 ▾", for: .normal) -// $0.setTitleColor(.darkGray, for: .normal) -// $0.titleLabel?.font = .systemFont(ofSize:14) -// $0.layer.cornerRadius = 8 -// $0.layer.borderWidth = 1 -// $0.layer.borderColor = UIColor.lightGray.cgColor -// $0.contentHorizontalAlignment = .left -// $0.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) -// } -// private let periodButton = UIButton(type: .system).then { -// $0.setTitle("기간 선택 ▾", for: .normal) -// $0.setTitleColor(.darkGray, for: .normal) -// $0.titleLabel?.font = UIFont.systemFont(ofSize:14) -// $0.layer.cornerRadius = 8 -// $0.layer.borderWidth = 1 -// $0.layer.borderColor = UIColor.lightGray.cgColor -// $0.contentHorizontalAlignment = .left -// $0.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) -// } -// private let timeButton = UIButton(type: .system).then { -// $0.setTitle("시간 선택 ▾", for: .normal) -// $0.setTitleColor(.darkGray, for: .normal) -// $0.titleLabel?.font = UIFont.systemFont(ofSize:14) -// $0.layer.cornerRadius = 8 -// $0.layer.borderWidth = 1 -// $0.layer.borderColor = UIColor.lightGray.cgColor -// $0.contentHorizontalAlignment = .left -// $0.contentEdgeInsets = UIEdgeInsets(top:7, left:8, bottom:7, right:8) -// } -// -// // (5) "저장" 버튼 -// private let saveButton = UIButton(type: .system).then { -// $0.setTitle("저장", for: .normal) -// $0.setTitleColor(.white, for: .normal) -// $0.backgroundColor = .lightGray -// $0.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) -// $0.layer.cornerRadius = 8 -// $0.isEnabled = false -// } -// -// // (6) 스크롤/스택 -// private let scrollView = UIScrollView() -// private let contentView = UIView() -// private let verticalStack = UIStackView().then { -// $0.axis = .vertical -// $0.spacing = 8 -// $0.distribution = .fill -// } -// -// // MARK: - Internal Data -// /// 외부(뷰컨)에서 주입할 "이미지 목록" -// private var images: [ExtendedImage] = [] -// -// // MARK: - Public computed properties -// /// 입력한 이름 get/set -// var storeName: String { -// get { nameTextField.text ?? "" } -// set { nameTextField.text = newValue } -// } -// -// /// 현재 카테고리 버튼 타이틀 -// var categoryText: String { -// get { categoryButton.title(for: .normal) ?? "" } -// set { categoryButton.setTitle(newValue, for: .normal) } -// } -// -// // MARK: - Init -// override init(frame: CGRect) { -// super.init(frame: frame) -// setupLayout() -// setupActions() -// setupCollectionView() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// // MARK: - Setup -// private func setupLayout() { -// backgroundColor = UIColor(white:0.95, alpha:1) -// -// // 1) 스크롤+컨텐츠 -// addSubview(scrollView) -// scrollView.snp.makeConstraints { make in -// make.top.left.right.equalToSuperview() -// make.bottom.equalToSuperview().offset(-64) // 아래 "저장"버튼을 띄우기 위해 예시 -// } -// scrollView.addSubview(contentView) -// contentView.snp.makeConstraints { make in -// make.edges.equalToSuperview() -// make.width.equalTo(scrollView.snp.width) -// } -// -// // 2) 수직스택 -// contentView.addSubview(verticalStack) -// verticalStack.snp.makeConstraints { make in -// make.top.equalToSuperview().offset(16) -// make.left.right.equalToSuperview().inset(16) -// make.bottom.equalToSuperview() -// } -// -// // (A) 이름 필드 -// let nameRow = makeRow(title: "이름", rightView: nameTextField) -// verticalStack.addArrangedSubview(nameRow) -// -// // (B) 이미지 버튼 (add/remove) -// let buttonStack = UIStackView(arrangedSubviews: [addImageButton, removeAllButton]) -// buttonStack.axis = .horizontal -// buttonStack.distribution = .fillEqually -// buttonStack.spacing = 8 -// verticalStack.addArrangedSubview(buttonStack) -// buttonStack.snp.makeConstraints { make in -// make.height.equalTo(40) -// } -// -// // (C) 콜렉션뷰 -// verticalStack.addArrangedSubview(imagesCollectionView) -// imagesCollectionView.snp.makeConstraints { make in -// make.height.equalTo(100) -// } -// -// // (D) 카테고리 버튼 -// let catRow = makeRow(title: "카테고리", rightView: categoryButton) -// verticalStack.addArrangedSubview(catRow) -// -// // (E) 기간 버튼 -// let periodRow = makeRow(title: "기간", rightView: periodButton) -// verticalStack.addArrangedSubview(periodRow) -// -// // (F) 시간 버튼 -// let timeRow = makeRow(title: "시간", rightView: timeButton) -// verticalStack.addArrangedSubview(timeRow) -// -// // (여기에 위치/마커/설명 등 다른 항목도 같은 방식으로) -// -// // 3) 저장 버튼 (화면 하단 고정) -// addSubview(saveButton) -// saveButton.snp.makeConstraints { make in -// make.left.right.equalToSuperview().inset(16) -// make.bottom.equalTo(safeAreaLayoutGuide).offset(-8) -// make.height.equalTo(44) -// } -// } -// -// private func setupActions() { -// // (1) 이미지추가 -> onAddImageTapped -// addImageButton.addTarget(self, action: #selector(tapAddImage), for: .touchUpInside) -// // (2) 전체삭제 -> onRemoveAllTapped -// removeAllButton.addTarget(self, action: #selector(tapRemoveAll), for: .touchUpInside) -// // (3) 카테고리 -> onCategoryButtonTapped -// categoryButton.addTarget(self, action: #selector(tapCategory), for: .touchUpInside) -// // (4) 기간 -> onPeriodButtonTapped -// periodButton.addTarget(self, action: #selector(tapPeriod), for: .touchUpInside) -// // (5) 시간 -> onTimeButtonTapped -// timeButton.addTarget(self, action: #selector(tapTime), for: .touchUpInside) -// // (6) 저장 -> onSaveTapped -// saveButton.addTarget(self, action: #selector(tapSave), for: .touchUpInside) -// } -// -// private func setupCollectionView() { -// imagesCollectionView.dataSource = self -// imagesCollectionView.delegate = self -// } -// -// // MARK: - Public Methods -// /// 외부에서 "이미지 목록"을 세팅할 때 사용 -// func updateImages(_ newImages: [ExtendedImage]) { -// self.images = newImages -// imagesCollectionView.reloadData() -// updateSaveButtonState() -// } -// -// /// 저장버튼 활성화 업데이트 -// private func updateSaveButtonState() { -// // 예: 이미지가 1장 이상 있을 때만 활성화 -// let hasImages = !images.isEmpty -// saveButton.isEnabled = hasImages -// saveButton.backgroundColor = hasImages ? .systemBlue : .lightGray -// } -// -// // MARK: - Actions -// @objc private func tapAddImage() { -// onAddImageTapped?() -// } -// @objc private func tapRemoveAll() { -// onRemoveAllTapped?() -// } -// @objc private func tapCategory() { -// onCategoryButtonTapped?() -// } -// @objc private func tapPeriod() { -// onPeriodButtonTapped?() -// } -// @objc private func tapTime() { -// onTimeButtonTapped?() -// } -// @objc private func tapSave() { -// onSaveTapped?() -// } -// -// // MARK: - Helpers -// private func makeRow(title: String, rightView: UIView) -> UIView { -// let row = UIView() -// -// // 왼쪽 BG -// let leftBG = UIView() -// leftBG.backgroundColor = UIColor(white:0.94, alpha:1) -// row.addSubview(leftBG) -// leftBG.snp.makeConstraints { make in -// make.top.left.bottom.equalToSuperview() -// make.width.equalTo(80) -// } -// -// // 왼쪽 라벨 -// let label = UILabel() -// label.text = title -// label.font = .systemFont(ofSize:15, weight:.bold) -// label.textColor = .black -// label.textAlignment = .center -// leftBG.addSubview(label) -// label.snp.makeConstraints { make in -// make.centerY.equalToSuperview() -// make.left.right.equalToSuperview().inset(8) -// } -// -// // 오른쪽 BG -// let rightBG = UIView() -// rightBG.backgroundColor = .white -// row.addSubview(rightBG) -// rightBG.snp.makeConstraints { make in -// make.top.bottom.right.equalToSuperview() -// make.left.equalTo(leftBG.snp.right) -// } -// -// // 오른쪽 컨텐츠 (파라미터) -// rightBG.addSubview(rightView) -// rightView.snp.makeConstraints { make in -// make.top.equalToSuperview().offset(8) -// make.bottom.equalToSuperview().offset(-8) -// make.left.equalToSuperview().offset(8) -// make.right.equalToSuperview().offset(-8) -// make.height.equalTo(36).priority(.medium) -// } -// -// // 구분선 -// let sep = UIView() -// sep.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) -// row.addSubview(sep) -// sep.snp.makeConstraints { make in -// make.left.right.bottom.equalToSuperview() -// make.height.equalTo(1) -// } -// -// return row -// } -//} -// -//// MARK: - UICollectionViewDataSource -//extension PopUpStoreRegisterView: UICollectionViewDataSource { -// func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { -// return images.count -// } -// -// func collectionView(_ collectionView: UICollectionView, -// cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { -// guard let cell = collectionView.dequeueReusableCell( -// withReuseIdentifier: PopUpImageCell.identifier, -// for: indexPath -// ) as? PopUpImageCell else { -// return UICollectionViewCell() -// } -// let item = images[indexPath.item] -// cell.configure(with: item) -// -// cell.onMainCheckToggled = { [weak self] in -// self?.onToggleMainImage?(indexPath.item) -// } -// cell.onDeleteTapped = { [weak self] in -// self?.onDeleteImage?(indexPath.item) -// } -// return cell -// } -//} -// -//// MARK: - UICollectionViewDelegateFlowLayout -//extension PopUpStoreRegisterView: UICollectionViewDelegateFlowLayout { -// // 혹시 셀 사이즈/간격을 동적으로 조정하고 싶다면 여기서 -//} -// -//// MARK: - PopUpImageCell (같은 파일) -//final class PopUpImageCell: UICollectionViewCell { -// static let identifier = "PopUpImageCell" -// -// // 콜백 -// var onMainCheckToggled: (() -> Void)? -// var onDeleteTapped: (() -> Void)? -// -// private let thumbImageView = UIImageView().then { -// $0.contentMode = .scaleAspectFill -// $0.layer.cornerRadius = 6 -// $0.clipsToBounds = true -// } -// private let mainCheckButton = UIButton(type: .system).then { -// $0.setTitle("대표", for: .normal) -// $0.setTitleColor(.white, for: .normal) -// $0.backgroundColor = .gray -// $0.titleLabel?.font = .systemFont(ofSize:12, weight:.medium) -// $0.layer.cornerRadius = 4 -// } -// private let deleteButton = UIButton(type: .system).then { -// $0.setTitle("삭제", for: .normal) -// $0.setTitleColor(.red, for: .normal) -// $0.titleLabel?.font = .systemFont(ofSize:12, weight:.medium) -// } -// -// override init(frame: CGRect) { -// super.init(frame: frame) -// contentView.addSubview(thumbImageView) -// contentView.addSubview(mainCheckButton) -// contentView.addSubview(deleteButton) -// -// thumbImageView.snp.makeConstraints { make in -// make.top.left.right.equalToSuperview() -// make.height.equalTo(thumbImageView.snp.width) -// } -// mainCheckButton.snp.makeConstraints { make in -// make.top.equalTo(thumbImageView.snp.bottom).offset(4) -// make.left.equalToSuperview() -// make.width.equalTo(40) -// make.height.equalTo(24) -// } -// deleteButton.snp.makeConstraints { make in -// make.top.equalTo(thumbImageView.snp.bottom).offset(4) -// make.right.equalToSuperview() -// make.width.equalTo(40) -// make.height.equalTo(24) -// } -// -// mainCheckButton.addTarget(self, action: #selector(didTapMainCheck), for: .touchUpInside) -// deleteButton.addTarget(self, action: #selector(didTapDelete), for: .touchUpInside) -// } -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// @objc private func didTapMainCheck() { -// onMainCheckToggled?() -// } -// @objc private func didTapDelete() { -// onDeleteTapped?() -// } -// -// func configure(with item: ExtendedImage) { -// thumbImageView.image = item.image -// mainCheckButton.backgroundColor = item.isMain ? .systemRed : .gray -// } -//} diff --git a/Poppool/Poppool/Presentation/Convention/ControllerConvention.swift b/Poppool/Poppool/Presentation/Convention/ControllerConvention.swift deleted file mode 100644 index bdce512a..00000000 --- a/Poppool/Poppool/Presentation/Convention/ControllerConvention.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ControllerConvention.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/20/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit - -final class ControllerConvention: BaseViewController, View { - - typealias Reactor = ReactorConvention - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = ViewConvention() -} - -// MARK: - Life Cycle -extension ControllerConvention { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } -} - -// MARK: - SetUp -private extension ControllerConvention { - func setUp() { - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension ControllerConvention { - func bind(reactor: Reactor) { - } -} diff --git a/Poppool/Poppool/Presentation/Convention/ConventionCollectionViewCell.swift b/Poppool/Poppool/Presentation/Convention/ConventionCollectionViewCell.swift deleted file mode 100644 index e5eb7c34..00000000 --- a/Poppool/Poppool/Presentation/Convention/ConventionCollectionViewCell.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ConventionCollectionViewCell.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/27/24. -// - -import UIKit - -import SnapKit - -final class ConventionCollectionViewCell: UICollectionViewCell { - - // MARK: - Components - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } -} - -// MARK: - SetUp -private extension ConventionCollectionViewCell { - func setUpConstraints() { - } -} - -extension ConventionCollectionViewCell: Inputable { - struct Input { - } - - func injection(with input: Input) { - } -} diff --git a/Poppool/Poppool/Presentation/Convention/ConventionTableViewCell.swift b/Poppool/Poppool/Presentation/Convention/ConventionTableViewCell.swift deleted file mode 100644 index c80f88aa..00000000 --- a/Poppool/Poppool/Presentation/Convention/ConventionTableViewCell.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ConventionTableViewCell.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/26/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class ConventionTableViewCell: UITableViewCell { - - // MARK: - Components - - let disposeBag = DisposeBag() - - // MARK: - init - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp, Methods -private extension ConventionTableViewCell { - func setUpConstraints() { - } -} - -// MARK: - Inputable -extension ConventionTableViewCell: Inputable { - struct Input { - } - - func injection(with input: Input) { - - } -} diff --git a/Poppool/Poppool/Presentation/Convention/ReactorConvention.swift b/Poppool/Poppool/Presentation/Convention/ReactorConvention.swift deleted file mode 100644 index cc9a1ed9..00000000 --- a/Poppool/Poppool/Presentation/Convention/ReactorConvention.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ReactorConvention.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/20/24. -// - -import ReactorKit -import RxSwift -import RxCocoa - -final class ReactorConvention: Reactor { - - // MARK: - Reactor - enum Action { - } - - enum Mutation { - } - - struct State { - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - - // MARK: - init - init() { - self.initialState = State() - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - } - return newState - } -} diff --git a/Poppool/Poppool/Presentation/Convention/TestDynamicCell.swift b/Poppool/Poppool/Presentation/Convention/TestDynamicCell.swift deleted file mode 100644 index 2f3ae731..00000000 --- a/Poppool/Poppool/Presentation/Convention/TestDynamicCell.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// TestDynamicCell.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/27/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class TestDynamicCell: UICollectionViewCell { - - // MARK: - Components - - let disposeBag = DisposeBag() - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } -} - -// MARK: - SetUp -private extension TestDynamicCell { - func setUpConstraints() { - } -} - -extension TestDynamicCell: Inputable { - struct Input { - - } - - func injection(with input: Input) { - - } -} diff --git a/Poppool/Poppool/Presentation/Convention/TestDynamicSection.swift b/Poppool/Poppool/Presentation/Convention/TestDynamicSection.swift deleted file mode 100644 index 04b245b8..00000000 --- a/Poppool/Poppool/Presentation/Convention/TestDynamicSection.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// TestDynamicSection.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/27/24. -// - -import UIKit - -import RxSwift - -struct TestDynamicSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = TestDynamicCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(1000) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .estimated(1000) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - section.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20) - - return section - } -} diff --git a/Poppool/Poppool/Presentation/Convention/ViewConvention.swift b/Poppool/Poppool/Presentation/Convention/ViewConvention.swift deleted file mode 100644 index 8222a445..00000000 --- a/Poppool/Poppool/Presentation/Convention/ViewConvention.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ViewConvention.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/20/24. -// -import UIKit - -import SnapKit - -final class ViewConvention: UIView { - - // MARK: - Components - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension ViewConvention { - - func setUpConstraints() { - } -} diff --git a/Poppool/Poppool/Presentation/Extension/Optional+.swift b/Poppool/Poppool/Presentation/Extension/Optional+.swift deleted file mode 100644 index 9cb9f785..00000000 --- a/Poppool/Poppool/Presentation/Extension/Optional+.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Optional+.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - -import Foundation - -extension Optional where Wrapped: Collection { - var orEmpty: Wrapped { - return self ?? [] as! Wrapped - } -} diff --git a/Poppool/Poppool/Presentation/Extension/UICollectionReusableView+.swift b/Poppool/Poppool/Presentation/Extension/UICollectionReusableView+.swift deleted file mode 100644 index 384b685b..00000000 --- a/Poppool/Poppool/Presentation/Extension/UICollectionReusableView+.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UICollectionReusableView+.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/29/24. -// - -import UIKit - -extension UICollectionReusableView { - var identifiers: String { - return String(describing: self) - } -} diff --git a/Poppool/Poppool/Presentation/Extension/UICollectionViewCell+.swift b/Poppool/Poppool/Presentation/Extension/UICollectionViewCell+.swift deleted file mode 100644 index 3c10fbac..00000000 --- a/Poppool/Poppool/Presentation/Extension/UICollectionViewCell+.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UICollectionViewCell+.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/20/24. -// - -import UIKit - -extension UICollectionViewCell { - static var identifiers: String { - return String(describing: self) - } -} diff --git a/Poppool/Poppool/Presentation/Extension/UIFont+.swift b/Poppool/Poppool/Presentation/Extension/UIFont+.swift deleted file mode 100644 index 4d131df8..00000000 --- a/Poppool/Poppool/Presentation/Extension/UIFont+.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UIFont+.swift -// PopPool -// -// Created by Porori on 6/20/24. -// - -import Foundation -import UIKit - -extension UIFont { - - static func KorFont(style: FontStyle, size: CGFloat) -> UIFont? { - return UIFont(name: "GothicA1\(style.rawValue)", size: size) - } - - static func EngFont(style: FontStyle, size: CGFloat) -> UIFont? { - return UIFont(name: "Poppins\(style.rawValue)", size: size) - } - - enum FontStyle: String { - case bold = "-Bold" - case medium = "-Medium" - case regular = "-Regular" - case light = "-Light" - } -} - diff --git a/Poppool/Poppool/Presentation/Extension/UIImageView+.swift b/Poppool/Poppool/Presentation/Extension/UIImageView+.swift deleted file mode 100644 index 8040bddb..00000000 --- a/Poppool/Poppool/Presentation/Extension/UIImageView+.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// UIImageView+.swift -// Poppool -// -// Created by SeoJunYoung on 12/3/24. -// - -import UIKit - -import Kingfisher - -extension UIImageView { - func setPPImage(path: String?) { - guard let path = path else { - self.image = UIImage(named: "image_default") - return - } - let imageURLString = Secrets.popPoolS3BaseURL.rawValue + path - if let cenvertimageURL = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - let imageURL = URL(string: cenvertimageURL) - self.kf.setImage(with: imageURL) { result in - switch result { - case .failure(let error): - Logger.log(message: "\(path) image Load Fail: \(error.localizedDescription)", category: .error) - default: - break - } - } - } - } - - func setPPImage(path: String?, completion: @escaping () -> Void) { - guard let path = path else { - self.image = UIImage(named: "image_default") - completion() - return - } - let imageURLString = Secrets.popPoolS3BaseURL.rawValue + path - if let cenvertimageURL = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - let imageURL = URL(string: cenvertimageURL) - self.kf.setImage(with: imageURL) { result in - completion() - switch result { - case .failure(let error): - Logger.log(message: "\(path) image Load Fail: \(error.localizedDescription)", category: .error) - default: - break - } - } - } - } -} diff --git a/Poppool/Poppool/Presentation/Map/Common/GMSMapViewDelegateProxy.swift b/Poppool/Poppool/Presentation/Map/Common/GMSMapViewDelegateProxy.swift deleted file mode 100644 index a3e7064b..00000000 --- a/Poppool/Poppool/Presentation/Map/Common/GMSMapViewDelegateProxy.swift +++ /dev/null @@ -1,39 +0,0 @@ -import RxSwift -import RxCocoa -import GoogleMaps - -class GMSMapViewDelegateProxy: DelegateProxy, DelegateProxyType, GMSMapViewDelegate { - - public private(set) weak var mapView: GMSMapView? - let didChangePositionSubject = PublishSubject() - let idleAtPositionSubject = PublishSubject() - - init(mapView: GMSMapView) { - self.mapView = mapView - super.init(parentObject: mapView, delegateProxy: GMSMapViewDelegateProxy.self) - } - - static func registerKnownImplementations() { - self.register { mapView in - GMSMapViewDelegateProxy(mapView: mapView) - } - } - - static func currentDelegate(for object: GMSMapView) -> GMSMapViewDelegate? { - return object.delegate - } - - static func setCurrentDelegate(_ delegate: GMSMapViewDelegate?, to object: GMSMapView) { - object.delegate = delegate - } - - func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - didChangePositionSubject.onNext(()) - self.forwardToDelegate()?.mapView?(mapView, didChange: position) - } - - func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) { - idleAtPositionSubject.onNext(()) - self.forwardToDelegate()?.mapView?(mapView, idleAt: position) - } -} diff --git a/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift b/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift deleted file mode 100644 index 4f46b43e..00000000 --- a/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift +++ /dev/null @@ -1,266 +0,0 @@ -import CoreLocation - -struct RegionCoordinate { - static let seoul = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780) - static let gyeonggi = CLLocationCoordinate2D(latitude: 37.4138, longitude: 127.5183) - static let incheon = CLLocationCoordinate2D(latitude: 37.4563, longitude: 126.7052) - static let daejeon = CLLocationCoordinate2D(latitude: 36.3504, longitude: 127.3845) - static let gwangju = CLLocationCoordinate2D(latitude: 35.1595, longitude: 126.8526) - static let daegu = CLLocationCoordinate2D(latitude: 35.8714, longitude: 128.6014) - static let busan = CLLocationCoordinate2D(latitude: 35.1796, longitude: 129.0756) - static let ulsan = CLLocationCoordinate2D(latitude: 35.5384, longitude: 129.3114) - static let chungbuk = CLLocationCoordinate2D(latitude: 36.6357, longitude: 127.4914) - static let chungnam = CLLocationCoordinate2D(latitude: 36.6588, longitude: 126.6728) - static let sejong = CLLocationCoordinate2D(latitude: 36.4801, longitude: 127.2892) - static let jeonbuk = CLLocationCoordinate2D(latitude: 35.7175, longitude: 127.1530) - static let jeonnam = CLLocationCoordinate2D(latitude: 34.8679, longitude: 126.9910) - static let gyeongbuk = CLLocationCoordinate2D(latitude: 36.4919, longitude: 128.8889) - static let gyeongnam = CLLocationCoordinate2D(latitude: 35.4606, longitude: 128.2132) - static let gangwon = CLLocationCoordinate2D(latitude: 37.8228, longitude: 128.1555) - static let jeju = CLLocationCoordinate2D(latitude: 33.4890, longitude: 126.4983) -} - -enum RegionType { - case seoul - case gyeonggi - case metropolitan - case province - -} - -struct RegionDefinitions { - // 서울 클러스터 - static let seoulClusters: [RegionCluster] = [ - RegionCluster( - name: "도봉/노원/강북/중랑", - subRegions: ["도봉구", "노원구", "강북구", "중랑구"], - coordinate: CLLocationCoordinate2D(latitude: 37.6494, longitude: 127.0510), - type: .seoul - ), - RegionCluster( - name: "동대문/성북", - subRegions: ["동대문구", "성북구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5894, longitude: 127.0435), - type: .seoul - ), - RegionCluster( - name: "중구/종로", - subRegions: ["중구", "종로구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5738, longitude: 126.9861), - type: .seoul - ), - RegionCluster( - name: "성동/광진", - subRegions: ["성동구", "광진구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5509, longitude: 127.0403), - type: .seoul - ), - RegionCluster( - name: "송파/강동", - subRegions: ["송파구", "강동구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5145, longitude: 127.1058), - type: .seoul - ), - RegionCluster( - name: "동작/관악", - subRegions: ["동작구", "관악구"], - coordinate: CLLocationCoordinate2D(latitude: 37.4959, longitude: 126.9410), - type: .seoul - ), - RegionCluster( - name: "서초/강남", - subRegions: ["서초구", "강남구"], - coordinate: CLLocationCoordinate2D(latitude: 37.4959, longitude: 127.0664), - type: .seoul - ), - RegionCluster( - name: "은평/서대문/마포", - subRegions: ["은평구", "서대문구", "마포구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5744, longitude: 126.9185), - type: .seoul - ), - RegionCluster( - name: "영등포/구로", - subRegions: ["영등포구", "구로구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5162, longitude: 126.8968), - type: .seoul - ), - RegionCluster( - name: "용산", - subRegions: ["용산구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5384, longitude: 126.9654), - type: .seoul - ), - RegionCluster( - name: "양천/강서/금천", - subRegions: ["양천구", "강서구", "금천구"], - coordinate: CLLocationCoordinate2D(latitude: 37.5509, longitude: 126.8553), - type: .seoul - ) - ] - - // 경기도 클러스터 - static let gyeonggiClusters: [RegionCluster] = [ - RegionCluster( - name: "포천/연천/동두천/양주", - subRegions: ["포천시", "연천군", "동두천시", "양주시"], - coordinate: CLLocationCoordinate2D(latitude: 37.8859, longitude: 127.0543), - type: .gyeonggi - ), - RegionCluster( - name: "의정부/구리/남양주", - subRegions: ["의정부시", "구리시", "남양주시"], - coordinate: CLLocationCoordinate2D(latitude: 37.7358, longitude: 127.1422), - type: .gyeonggi - ), - RegionCluster( - name: "파주/고양/가평", - subRegions: ["파주시", "고양시", "가평군"], - coordinate: CLLocationCoordinate2D(latitude: 37.7599, longitude: 126.7762), - type: .gyeonggi - ), - RegionCluster( - name: "용인/화성/수원", - subRegions: ["용인시", "화성시", "수원시"], - coordinate: CLLocationCoordinate2D(latitude: 37.2911, longitude: 127.0876), - type: .gyeonggi - ), - RegionCluster( - name: "군포/의왕/과천/안양", - subRegions: ["군포시", "의왕시", "과천시", "안양시"], - coordinate: CLLocationCoordinate2D(latitude: 37.3956, longitude: 126.9477), - type: .gyeonggi - ), - RegionCluster( - name: "부천/광명/시흥/안산", - subRegions: ["부천시", "광명시", "시흥시", "안산시"], - coordinate: CLLocationCoordinate2D(latitude: 37.4563, longitude: 126.8040), - type: .gyeonggi - ), - RegionCluster( - name: "안성/평택/오산", - subRegions: ["안성시", "평택시", "오산시"], - coordinate: CLLocationCoordinate2D(latitude: 37.0042, longitude: 127.2003), - type: .gyeonggi - ), - RegionCluster( - name: "여주/양평/광주/이천", - subRegions: ["여주시", "양평군", "광주시", "이천시"], - coordinate: CLLocationCoordinate2D(latitude: 37.2958, longitude: 127.5986), - type: .gyeonggi - ), - RegionCluster( - name: "김포", - subRegions: ["김포시"], - coordinate: CLLocationCoordinate2D(latitude: 37.6153, longitude: 126.7164), - type: .gyeonggi - ), - RegionCluster( - name: "성남/하남", - subRegions: ["성남시", "하남시"], - coordinate: CLLocationCoordinate2D(latitude: 37.4517, longitude: 127.1486), - type: .gyeonggi - ) - ] - - // 광역시 및 기타 지역 - static let metropolitanClusters: [RegionCluster] = [ - RegionCluster( - name: "인천", - subRegions: ["인천광역시"], - coordinate: CLLocationCoordinate2D(latitude: 37.4563, longitude: 126.7052), - type: .metropolitan - ), - RegionCluster( - name: "대전", - subRegions: ["대전광역시"], - coordinate: CLLocationCoordinate2D(latitude: 36.3504, longitude: 127.3845), - type: .metropolitan - ), - RegionCluster( - name: "광주", - subRegions: ["광주광역시"], - coordinate: CLLocationCoordinate2D(latitude: 35.1595, longitude: 126.8526), - type: .metropolitan - ), - RegionCluster( - name: "대구", - subRegions: ["대구광역시"], - coordinate: CLLocationCoordinate2D(latitude: 35.8714, longitude: 128.6014), - type: .metropolitan - ), - RegionCluster( - name: "부산", - subRegions: ["부산광역시"], - coordinate: CLLocationCoordinate2D(latitude: 35.1796, longitude: 129.0756), - type: .metropolitan - ), - RegionCluster( - name: "울산", - subRegions: ["울산광역시"], - coordinate: CLLocationCoordinate2D(latitude: 35.5384, longitude: 129.3114), - type: .metropolitan - ) - ] - - static let provinceClusters: [RegionCluster] = [ - RegionCluster( - name: "충북", - subRegions: ["충청북도"], - coordinate: CLLocationCoordinate2D(latitude: 36.6357, longitude: 127.4914), - type: .province - ), - RegionCluster( - name: "충남", - subRegions: ["충청남도"], - coordinate: CLLocationCoordinate2D(latitude: 36.6588, longitude: 126.6728), - type: .province - ), - RegionCluster( - name: "세종", - subRegions: ["세종특별자치시"], - coordinate: CLLocationCoordinate2D(latitude: 36.4801, longitude: 127.2892), - type: .province - ), - RegionCluster( - name: "전북", - subRegions: ["전라북도"], - coordinate: CLLocationCoordinate2D(latitude: 35.7175, longitude: 127.1530), - type: .province - ), - RegionCluster( - name: "전남", - subRegions: ["전라남도"], - coordinate: CLLocationCoordinate2D(latitude: 34.8679, longitude: 126.9910), - type: .province - ), - RegionCluster( - name: "경북", - subRegions: ["경상북도"], - coordinate: CLLocationCoordinate2D(latitude: 36.4919, longitude: 128.8889), - type: .province - ), - RegionCluster( - name: "경남", - subRegions: ["경상남도"], - coordinate: CLLocationCoordinate2D(latitude: 35.4606, longitude: 128.2132), - type: .province - ), - RegionCluster( - name: "강원", - subRegions: ["강원도"], - coordinate: CLLocationCoordinate2D(latitude: 37.8228, longitude: 128.1555), - type: .province - ), - RegionCluster( - name: "제주", - subRegions: ["제주특별자치도"], - coordinate: CLLocationCoordinate2D(latitude: 33.4890, longitude: 126.4983), - type: .province - ) - ] - - static var allClusters: [RegionCluster] { - seoulClusters + gyeonggiClusters + metropolitanClusters + provinceClusters - } -} diff --git a/Poppool/Poppool/Presentation/Map/Common/ViewportBounds.swift b/Poppool/Poppool/Presentation/Map/Common/ViewportBounds.swift deleted file mode 100644 index 9c1fc360..00000000 --- a/Poppool/Poppool/Presentation/Map/Common/ViewportBounds.swift +++ /dev/null @@ -1,6 +0,0 @@ -import CoreLocation - -struct ViewportBounds { - let northEast: CLLocationCoordinate2D - let southWest: CLLocationCoordinate2D -} diff --git a/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift b/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift deleted file mode 100644 index 611d0f3a..00000000 --- a/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift +++ /dev/null @@ -1,15 +0,0 @@ -//import GoogleMaps -// -//class CustomClusterRenderer: GMUDefaultClusterRenderer { -// override func willRenderMarker(_ marker: GMSMarker) { -// super.willRenderMarker(marker) -// // 클러스터일 경우 처리 -// if let cluster = marker.userData as? GMUCluster { -// let customView = MapMarker() -// // 예: 클러스터의 이름과 count를 injection -// customView.injection(with: .init(isSelected: false, isCluster: true, regionName: cluster.clusterIdentifier, count: cluster.count)) -// // marker의 iconView에 적용 -// marker.iconView = customView -// } -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift deleted file mode 100644 index 3919e1bf..00000000 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift +++ /dev/null @@ -1,88 +0,0 @@ -import UIKit -import SnapKit - -final class BalloonChipCell: UICollectionViewCell { - static let identifier = "BalloonChipCell" - - private let button: PPButton = { - let button = PPButton( - style: .secondary, - text: "", - font: .KorFont(style: .medium, size: 11), - cornerRadius: 15 - ) - button.titleLabel?.lineBreakMode = .byTruncatingTail - button.titleLabel?.adjustsFontSizeToFitWidth = false - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - contentView.addSubview(button) - setupLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupLayout() { - button.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } - - func configure(with title: String, isSelected: Bool) { - button.setTitle(title, for: .normal) - if isSelected { - let checkImage = UIImage(named: "icon_check_white")?.withRenderingMode(.alwaysOriginal) - let resizedImage = checkImage?.resize(to: CGSize(width: 16, height: 16)) - button.setImage(resizedImage, for: .normal) - button.semanticContentAttribute = .forceRightToLeft - button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 0) - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 12) - button.setBackgroundColor(.blu500, for: .normal) - button.setTitleColor(.white, for: .normal) - button.layer.borderWidth = 0 - button.titleLabel?.font = .KorFont(style: .bold, size: 11) - - - } else { - button.setImage(nil, for: .normal) - button.semanticContentAttribute = .unspecified - button.imageEdgeInsets = .zero - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) - button.setBackgroundColor(.white, for: .normal) - button.setTitleColor(.g400, for: .normal) - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.g200.cgColor - button.titleLabel?.font = .KorFont(style: .medium, size: 11) - - } - } - - private var currentAction: UIAction? - - var buttonAction: (() -> Void)? { - didSet { - if let oldAction = currentAction { - button.removeAction(oldAction, for: .touchUpInside) - } - - let action = UIAction { [weak self] _ in - self?.buttonAction?() - } - button.addAction(action, for: .touchUpInside) - currentAction = action - } - } -} - -extension UIImage { - func resize(to size: CGSize) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - defer { UIGraphicsEndImageContext() } - draw(in: CGRect(origin: .zero, size: size)) - return UIGraphicsGetImageFromCurrentImageContext() - } -} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift deleted file mode 100644 index b131a1b2..00000000 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift +++ /dev/null @@ -1,35 +0,0 @@ -//import UIKit -//import SnapKit -// -//final class CategoryFilterView: UIView { -// private let stackView = UIStackView() -// private let categories = ["게임", "라이프스타일", "엔터테인먼트", "패션", "음식/요리", "키즈"] -// -// override init(frame: CGRect) { -// super.init(frame: frame) -// setupLayout() -// setupCategories() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// private func setupLayout() { -// addSubview(stackView) -// stackView.axis = .vertical -// stackView.spacing = 12 -// stackView.snp.makeConstraints { make in -// make.top.equalToSuperview().offset(20) -// make.leading.trailing.equalToSuperview().inset(20) -// } -// } -// -// private func setupCategories() { -// for c in categories { -// let chip = FilterChip() -// chip.setTitle(c, style: .inactive) -// stackView.addArrangedSubview(chip) -// } -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift deleted file mode 100644 index 85a3812c..00000000 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift +++ /dev/null @@ -1,39 +0,0 @@ -//import UIKit -//import RxSwift -//import RxCocoa -// -//final class FilterTabsView: UIView { -// private let tabs = ["지역", "카테고리"] -// let segmentedControl = UISegmentedControl() -// -// var rx: Reactive { -// return Reactive(self) -// } -// -// override init(frame: CGRect) { -// super.init(frame: frame) -// setupTabs() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// private func setupTabs() { -// tabs.enumerated().forEach { index, title in -// segmentedControl.insertSegment(withTitle: title, at: index, animated: false) -// } -// segmentedControl.selectedSegmentIndex = 0 -// addSubview(segmentedControl) -// -// segmentedControl.snp.makeConstraints { make in -// make.edges.equalToSuperview() -// } -// } -//} -// -//extension Reactive where Base: FilterTabsView { -// var selectedIndex: ControlProperty { -// return base.segmentedControl.rx.selectedSegmentIndex -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/LocationFilterView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/LocationFilterView.swift deleted file mode 100644 index 4f4bbdf4..00000000 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/LocationFilterView.swift +++ /dev/null @@ -1,40 +0,0 @@ -//import UIKit -//import SnapKit -// -//final class LocationFilterView: UIView { -// private let scrollView = UIScrollView() -// private let contentStack = UIStackView() -// -// private let regions = ["서울", "경기", "인천", "부산", "제주"] -// -// override init(frame: CGRect) { -// super.init(frame: frame) -// setupLayout() -// setupRegions() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// private func setupLayout() { -// addSubview(scrollView) -// scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } -// -// scrollView.addSubview(contentStack) -// contentStack.axis = .horizontal -// contentStack.spacing = 8 -// contentStack.snp.makeConstraints { make in -// make.edges.equalToSuperview().inset(20) -// make.height.equalTo(40) -// } -// } -// -// private func setupRegions() { -// for r in regions { -// let chip = FilterChip() -// chip.setTitle(r, style: .inactive) -// contentStack.addArrangedSubview(chip) -// } -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/FindMap/GuideMapViewController.swift b/Poppool/Poppool/Presentation/Map/FindMap/GuideMapViewController.swift deleted file mode 100644 index 8b137891..00000000 --- a/Poppool/Poppool/Presentation/Map/FindMap/GuideMapViewController.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift deleted file mode 100644 index 690afbd7..00000000 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift +++ /dev/null @@ -1,186 +0,0 @@ -import Foundation -import UIKit -import RxSwift -import ReactorKit -import CoreLocation -import GoogleMaps - -final class FullScreenMapViewController: MapViewController { - var selectedStore: MapPopUpStore? - var shouldAutoSelectNearestStore = false // 일반 모드와 다르게 false로 설정 - - override func viewDidLoad() { - super.viewDidLoad() - - self.navigationController?.navigationBar.isHidden = false - setupNavigation() - - mainView.searchFilterContainer.isHidden = true - mainView.filterChips.isHidden = true - mainView.listButton.isHidden = true - carouselView.isHidden = false - - // 지도 델리게이트 재설정 - mainView.mapView.delegate = self - - // 선택된 스토어가 있다면 즉시 마커 탭 처리 (요구사항 1) - if let store = selectedStore { - updateUI(for: store) - } - } - - - - // MARK: - Binding - override func bind(reactor: Reactor) { - super.bind(reactor: reactor) - - // [변경] 기존 viewportStores 관련 바인딩은 풀스크린에서 marker tap 처리와 충돌할 수 있으므로 주석처리하거나 제거 - /* - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .debounce(.milliseconds(300), scheduler: MainScheduler.instance) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in - self?.currentStores = stores - self?.updateMapWithClustering() - }) - .disposed(by: disposeBag) - */ - - // searchResult나 selectedStore 변경시에만 UI 업데이트 (요구사항 1) - reactor.state - .map { $0.selectedStore ?? $0.searchResult } - .distinctUntilChanged { $0?.id == $1?.id } - .compactMap { $0 } - .filter { [weak self] store in - // 현재 선택된 스토어와 다른 경우에만 업데이트 - self?.selectedStore?.id != store.id - } - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] store in - self?.updateUI(for: store) - }) - .disposed(by: disposeBag) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.navigationBar.isHidden = false - } - - // MARK: - Map Delegate Methods - override func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { - // (1) 구/시 단위 클러스터 - if let clusterData = marker.userData as? ClusterMarkerData { - return handleRegionalClusterTap(marker, clusterData: clusterData) - } - // (2) 동일 좌표 마이크로 클러스터 - else if let storeArray = marker.userData as? [MapPopUpStore] { - if storeArray.count > 1 { - return handleMicroClusterTap(marker, storeArray: storeArray) - } else if let singleStore = storeArray.first { - return handleSingleStoreTap(marker, store: singleStore) - } - } - // (3) 단일 스토어 - else if let singleStore = marker.userData as? MapPopUpStore { - return handleSingleStoreTap(marker, store: singleStore) - } - return false - } - - override func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - // 카메라 이동 중 별도 처리 없음 - } - - override func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { - // 지도 빈 공간 탭은 무시 - } - - private func findMarkerForStore(for store: MapPopUpStore) -> GMSMarker? { - if let marker = self.currentMarker, - let markerStore = marker.userData as? MapPopUpStore, - markerStore.id == store.id { - return marker - } - return nil - } - - /// 선택된 스토어 정보를 기반으로 마커, 카메라, 캐러셀을 업데이트 (요구사항 1) - private func updateUI(for store: MapPopUpStore) { - // 기존 마커 제거 - mainView.mapView.clear() - - // 새 마커 생성 - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - // 마커 뷰 생성 및 선택 상태 주입 - let selectedInput = MapMarker.Input( - isSelected: true, - isCluster: false, - regionName: "", - count: 1, - isMultiMarker: false - ) - let markerView = MapMarker() - markerView.injection(with: selectedInput) - marker.iconView = markerView - - // 마커를 지도에 추가 - marker.map = mainView.mapView - currentMarker = marker - - mainView.mapView.selectedMarker = marker - - // 카메라 이동 - let camera = GMSCameraPosition.camera( - withLatitude: store.latitude, - longitude: store.longitude, - zoom: 16 - ) - mainView.mapView.animate(to: camera) - - // 캐러셀 업데이트 - carouselView.updateCards([store]) - currentCarouselStores = [store] - carouselView.isHidden = false - - // 약간의 딜레이 후 마커 뷰 재갱신 (필요 시) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - markerView.injection(with: selectedInput) - markerView.setNeedsLayout() - markerView.layoutIfNeeded() - } - } - - - @objc private func backButtonTapped() { - dismiss(animated: true) - } - - private func setupNavigation() { - navigationItem.title = "찾아가는 길" - let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.shadowColor = .clear - appearance.backgroundColor = .white - appearance.titleTextAttributes = [ - .foregroundColor: UIColor.black, - .font: UIFont.systemFont(ofSize: 15, weight: .regular) - ] - navigationController?.navigationBar.standardAppearance = appearance - navigationController?.navigationBar.scrollEdgeAppearance = appearance - navigationItem.leftBarButtonItem = UIBarButtonItem( - image: UIImage(named: "bakcbutton")?.withRenderingMode(.alwaysOriginal), - style: .plain, - target: self, - action: #selector(backButtonTapped) - ) - navigationItem.leftBarButtonItem?.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } -} diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift deleted file mode 100644 index 39b05082..00000000 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// MapDirectionRepository.swift -// Poppool -// -// Created by 김기현 on 1/23/25. -// - -import Foundation -import RxSwift - - -protocol MapDirectionRepository { - func getPopUpDirection(popUpStoreId: Int64) -> Observable -} - -final class DefaultMapDirectionRepository: MapDirectionRepository { - private let provider: Provider - private let tokenInterceptor = TokenInterceptor() - - init(provider: Provider) { - self.provider = provider - } - - func getPopUpDirection(popUpStoreId: Int64) -> Observable { - let endpoint = FindDirectionEndPoint.fetchDirection(popUpStoreId: popUpStoreId) -// print("🌎 [Repository]: 요청 생성 - \(endpoint)") - return provider.requestData(with: endpoint, interceptor: TokenInterceptor()) - .do(onNext: { response in -// print("✅ [Repository]: 응답 수신 - \(response)") - }, onError: { error in - print("❌ [Repository]: 요청 실패 - \(error)") - }) - } - -} diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionUseCase.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionUseCase.swift deleted file mode 100644 index 0c7ea395..00000000 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionUseCase.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MapDirectionUseCase.swift -// Poppool -// -// Created by 김기현 on 1/23/25. -// - -import Foundation -import RxSwift - -protocol MapDirectionUseCase { - func getPopUpDirection(popUpStoreId: Int64) -> Observable -} - -final class DefaultMapDirectionUseCase: MapDirectionUseCase { - private let repository: MapDirectionRepository - - init(repository: MapDirectionRepository) { - self.repository = repository - } - - func getPopUpDirection(popUpStoreId: Int64) -> Observable { - return repository.getPopUpDirection(popUpStoreId: popUpStoreId) - .map { $0.toDomain() } - } -} diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift deleted file mode 100644 index c07ed2b8..00000000 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift +++ /dev/null @@ -1,336 +0,0 @@ -import UIKit -import SnapKit -import GoogleMaps -import ReactorKit -import RxSwift -import CoreLocation - -final class MapGuideViewController: UIViewController, View { - // MARK: - Properties - var disposeBag = DisposeBag() - private let popUpStoreId: Int64 - private var currentCarouselStores: [MapPopUpStore] = [] // 현재 선택된 스토어 목록 - - init(popUpStoreId: Int64) { - self.popUpStoreId = popUpStoreId - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private let dimmingView: UIView = { - let v = UIView() - v.backgroundColor = UIColor.gray.withAlphaComponent(0.3) - v.alpha = 0 - return v - }() - - private let modalCardView: UIView = { - let v = UIView() - v.backgroundColor = .white - v.layer.cornerRadius = 16 - v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - v.layer.shadowColor = UIColor.black.cgColor - v.layer.shadowOpacity = 0.1 - v.layer.shadowOffset = .zero - v.layer.shadowRadius = 8 - return v - }() - - private let titleLabel: UILabel = { - let lb = UILabel() - lb.text = "찾아가는 길" - lb.font = UIFont.boldSystemFont(ofSize: 17) - lb.textColor = .black - return lb - }() - - private let closeButton: UIButton = { - let btn = UIButton(type: .system) - let image = UIImage(named: "icon_xmark")?.withRenderingMode(.alwaysOriginal) - btn.setImage(image, for: .normal) - return btn - }() - - private let mapView: GMSMapView = { - let map = GMSMapView() - map.isMyLocationEnabled = false - map.layer.borderWidth = 1 - map.layer.borderColor = UIColor.g100.cgColor - map.layer.cornerRadius = 12 - return map - }() - - /// 지도 우상단 "Expandable" 버튼 - private let expandButton: UIButton = { - let btn = UIButton() - btn.setImage(UIImage(named: "Expandable"), for: .normal) - btn.backgroundColor = UIColor.white - btn.layer.cornerRadius = 16 - btn.clipsToBounds = true - return btn - }() - - private let promptLabel: UILabel = { - let lb = UILabel() - lb.text = "지도 앱으로\n바로 찾아볼까요?" - lb.font = UIFont.systemFont(ofSize: 15, weight: .medium) - lb.textColor = .darkGray - lb.numberOfLines = 2 - return lb - }() - - private let naverButton: UIButton = { - let btn = UIButton() - btn.setImage(UIImage(named: "naver"), for: .normal) - btn.layer.cornerRadius = 24 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.g100.cgColor - btn.clipsToBounds = true - return btn - }() - - private let kakaoButton: UIButton = { - let btn = UIButton() - btn.setImage(UIImage(named: "kakao"), for: .normal) - btn.layer.cornerRadius = 24 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.g100.cgColor - btn.clipsToBounds = true - return btn - }() - - private let appleButton: UIButton = { - let btn = UIButton() - btn.setImage(UIImage(named: "AppleMap"), for: .normal) - btn.layer.cornerRadius = 24 - btn.layer.borderWidth = 1 - btn.layer.borderColor = UIColor.g100.cgColor - btn.clipsToBounds = true - return btn - }() - - private var modalCardBottomConstraint: Constraint? - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - presentModalCard() - } - - func bind(reactor: MapGuideReactor) { - reactor.action.onNext(.viewDidLoad(self.popUpStoreId)) - - // 닫기 버튼 - closeButton.rx.tap - .subscribe(onNext: { [weak self] in - self?.dismiss(animated: true, completion: nil) - }) - .disposed(by: disposeBag) - - // 지도 앱 열기 - naverButton.rx.tap - .map { Reactor.Action.openMapApp("naver") } - .bind(to: reactor.action) - .disposed(by: disposeBag) - kakaoButton.rx.tap - .map { Reactor.Action.openMapApp("kakao") } - .bind(to: reactor.action) - .disposed(by: disposeBag) - appleButton.rx.tap - .map { Reactor.Action.openMapApp("apple") } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - expandButton.rx.tap - .subscribe(onNext: { [weak self] in - guard let self = self else { return } - let provider = ProviderImpl() - let useCase = DefaultMapUseCase(repository: DefaultMapRepository(provider: provider)) - let directionRepository = DefaultMapDirectionRepository(provider: provider) - let reactor = MapReactor(useCase: useCase, directionRepository: directionRepository) - - if let selectedStore = self.currentCarouselStores.first { - reactor.action.onNext(.didSelectItem(selectedStore)) - reactor.action.onNext(.viewDidLoad(self.popUpStoreId)) - - let fullScreenMapVC = FullScreenMapViewController() - fullScreenMapVC.selectedStore = selectedStore // 직접 주입 - fullScreenMapVC.reactor = reactor - - let nav = UINavigationController(rootViewController: fullScreenMapVC) - nav.modalPresentationStyle = .fullScreen - self.present(nav, animated: true) - } else { - reactor.action.onNext(.viewDidLoad(self.popUpStoreId)) - reactor.state - .map { $0.searchResult } - .distinctUntilChanged() - .compactMap { $0 } - .take(1) - .subscribe(onNext: { [weak self] store in - let fullScreenMapVC = FullScreenMapViewController() - fullScreenMapVC.reactor = reactor - - let nav = UINavigationController(rootViewController: fullScreenMapVC) - nav.modalPresentationStyle = .fullScreen - self?.present(nav, animated: true) - }) - .disposed(by: self.disposeBag) - } - }) - .disposed(by: disposeBag) - - reactor.state - .map { $0.destinationCoordinate } - .compactMap { $0 } - .subscribe(onNext: { [weak self] coordinate in - self?.setupMarker(at: coordinate) - }) - .disposed(by: disposeBag) - - reactor.state - .map { $0.shouldDismiss } - .distinctUntilChanged() - .filter { $0 } - .subscribe(onNext: { [weak self] _ in - self?.dismissModalCard() - }) - .disposed(by: disposeBag) - } - - // MARK: - UI Setup - private func setupUI() { - view.backgroundColor = .clear - - view.addSubview(dimmingView) - dimmingView.snp.makeConstraints { $0.edges.equalToSuperview() } - - view.addSubview(modalCardView) - modalCardView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(408) - self.modalCardBottomConstraint = make.bottom.equalToSuperview().offset(408).constraint - } - - let topContainer = UIView() - modalCardView.addSubview(topContainer) - topContainer.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.leading.trailing.equalToSuperview() - make.height.equalTo(44) - } - - topContainer.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(20) - } - - topContainer.addSubview(closeButton) - closeButton.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().inset(16) - make.width.height.equalTo(24) - } - - modalCardView.addSubview(mapView) - mapView.snp.makeConstraints { make in - make.top.equalTo(topContainer.snp.bottom).offset(20) - make.leading.trailing.equalToSuperview().inset(20) - make.height.equalTo(320) - } - - mapView.addSubview(expandButton) - expandButton.snp.makeConstraints { make in - make.bottom.equalToSuperview().inset(10) - make.trailing.equalToSuperview().inset(10) - make.width.height.equalTo(32) - } - - let bottomContainer = UIView() - modalCardView.addSubview(bottomContainer) - bottomContainer.snp.makeConstraints { make in - make.top.equalTo(mapView.snp.bottom).offset(20) - make.leading.trailing.equalToSuperview().inset(20) - make.height.equalTo(44) - make.bottom.equalTo(modalCardView.snp.bottom).inset(60) - } - - bottomContainer.addSubview(promptLabel) - promptLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - - // 티맵 버튼 제거, 네이버/카카오/애플맵만 포함 - let appStack = UIStackView(arrangedSubviews: [naverButton, kakaoButton, appleButton]) - appStack.axis = .horizontal - appStack.alignment = .center - appStack.spacing = 16 // 버튼이 3개로 줄어 간격 다시 늘림 - appStack.distribution = .fillEqually - - bottomContainer.addSubview(appStack) - appStack.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - [naverButton, kakaoButton, appleButton].forEach { button in - button.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 48)) - } - } - } - } - - private func presentModalCard() { - self.dimmingView.alpha = 1 - UIView.animate( - withDuration: 0.3, - delay: 0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0.5, - options: .curveEaseOut - ) { - self.modalCardBottomConstraint?.update(offset: 0) - self.view.layoutIfNeeded() - } - } - - private func setupMarker(at coordinate: CLLocationCoordinate2D) { - // 새 마커 생성 및 설정 - let marker = GMSMarker() - marker.position = coordinate - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - marker.appearAnimation = .none - - let markerView = MapMarker() - markerView.injection(with: .init(isSelected: true)) - marker.iconView = markerView - - // 카메라 위치 설정 - let camera = GMSCameraPosition(target: coordinate, zoom: 16) - - // 애니메이션과 마커 변경을 하나의 트랜잭션으로 처리 - CATransaction.begin() - CATransaction.setDisableActions(true) - - // 카메라 이동과 마커 설정을 동시에 처리 - mapView.animate(to: camera) - marker.map = mapView - - CATransaction.commit() - } - - private func dismissModalCard() { - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { -// self.dimmingView.alpha = 0 - self.modalCardBottomConstraint?.update(offset: 408) - self.view.layoutIfNeeded() - } completion: { _ in - self.navigationController?.popViewController(animated: false) - } - } -} diff --git a/Poppool/Poppool/Presentation/Map/MapStoreCard.swift b/Poppool/Poppool/Presentation/Map/MapStoreCard.swift deleted file mode 100644 index 9647c3d4..00000000 --- a/Poppool/Poppool/Presentation/Map/MapStoreCard.swift +++ /dev/null @@ -1,116 +0,0 @@ -//import UIKit -//import SnapKit -// -//final class MapStoreCard: UIView { -// // MARK: - Components -// private let containerView: UIView = { -// let view = UIView() -// view.backgroundColor = .white -// view.layer.cornerRadius = 16 -// view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] -// view.layer.shadowColor = UIColor.black.cgColor -// view.layer.shadowOpacity = 0.1 -// view.layer.shadowRadius = 4 -// view.layer.shadowOffset = CGSize(width: 0, height: -2) -// return view -// }() -// -// private let thumbnailImageView: UIImageView = { -// let iv = UIImageView() -// iv.contentMode = .scaleAspectFill -// iv.clipsToBounds = true -// iv.layer.cornerRadius = 8 -// return iv -// }() -// -// private let categoryLabel = PPLabel(style: .regular, fontSize: 12) -// private let titleLabel: PPLabel = { -// let label = PPLabel(style: .bold, fontSize: 16) -// label.numberOfLines = 2 // 최대 2줄로 제한 -// return label -// }() -// -// private let locationLabel = PPLabel(style: .regular, fontSize: 12) -// private let dateLabel = PPLabel(style: .regular, fontSize: 12) -// -// // MARK: - Init -// init() { -// super.init(frame: .zero) -// setupLayout() -// configureUI() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -//} -// -//// MARK: - Setup -//private extension MapStoreCard { -// func setupLayout() { -// addSubview(containerView) -// -// [thumbnailImageView, categoryLabel, titleLabel, locationLabel, dateLabel].forEach { -// containerView.addSubview($0) -// } -// -// containerView.snp.makeConstraints { make in -// make.edges.equalToSuperview().inset(16) -// } -// -// -// thumbnailImageView.snp.makeConstraints { make in -// make.leading.equalToSuperview() -// make.top.bottom.equalToSuperview().inset(20) -// make.width.equalTo(80) // 고정된 너비 -// } -// -// categoryLabel.snp.makeConstraints { make in -// make.leading.equalTo(thumbnailImageView.snp.trailing).offset(16) -// make.trailing.equalToSuperview().inset(16) -// make.top.equalTo(thumbnailImageView) -// } -// -// titleLabel.snp.makeConstraints { make in -// make.leading.equalTo(categoryLabel) -// make.trailing.equalToSuperview().inset(16) -// make.top.equalTo(categoryLabel.snp.bottom).offset(8) -// } -// -// locationLabel.snp.makeConstraints { make in -// make.leading.equalTo(categoryLabel) -// make.top.equalTo(titleLabel.snp.bottom).offset(4) -// } -// -// dateLabel.snp.makeConstraints { make in -// make.leading.equalTo(locationLabel.snp.trailing).offset(8) -// make.centerY.equalTo(locationLabel) -// } -// -// } -// -// func configureUI() { -// categoryLabel.textColor = .g700 -// locationLabel.textColor = .g500 -// dateLabel.textColor = .g500 -// } -//} -// -//// MARK: - Inputable -//extension MapStoreCard: Inputable { -// struct Input { -// let image: UIImage? -// let category: String -// let title: String -// let location: String -// let date: String -// } -// -// func injection(with input: Input) { -// thumbnailImageView.image = input.image ?? UIImage(named: "default_thumbnail") -// categoryLabel.text = input.category -// titleLabel.text = input.title -// locationLabel.text = input.location -// dateLabel.text = input.date -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift b/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift deleted file mode 100644 index 52bf64b6..00000000 --- a/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift +++ /dev/null @@ -1,2011 +0,0 @@ -import UIKit -import FloatingPanel -import SnapKit -import RxSwift -import RxCocoa -import ReactorKit -import GoogleMaps -import CoreLocation -import RxGesture - - -class MapViewController: BaseViewController, View { - typealias Reactor = MapReactor - - - fileprivate struct CoordinateKey: Hashable { - let lat: Int - let lng: Int - - init(latitude: Double, longitude: Double) { - self.lat = Int(latitude * 1_000_00) - self.lng = Int(longitude * 1_000_00) - } - } - // 전체 스토어 목록 저장 - var allStores: [MapPopUpStore] = [] - var currentTooltipView: UIView? - var currentTooltipStores: [MapPopUpStore] = [] - var currentTooltipCoordinate: CLLocationCoordinate2D? - - - // MARK: - Properties - private var storeDetailsCache: [Int64: StoreItem] = [:] - private var isMovingToMarker = false - var currentCarouselStores: [MapPopUpStore] = [] - private var markerDictionary: [Int64: GMSMarker] = [:] - private var individualMarkerDictionary: [Int64: GMSMarker] = [:] - private var clusterMarkerDictionary: [String: GMSMarker] = [:] - private let popUpAPIUseCase = PopUpAPIUseCaseImpl( - repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) - private let clusteringManager = ClusteringManager() - var currentStores: [MapPopUpStore] = [] - var disposeBag = DisposeBag() - let mainView = MapView() - let carouselView = MapPopupCarouselView() - private let locationManager = CLLocationManager() - var currentMarker: GMSMarker? - private let storeListReactor = StoreListReactor() - private let storeListViewController = StoreListViewController(reactor: StoreListReactor()) - private var listViewTopConstraint: Constraint? - private var currentFilterBottomSheet: FilterBottomSheetViewController? - private var filterChipsTopY: CGFloat = 0 - private var filterContainerBottomY: CGFloat { - let frameInView = mainView.filterChips.convert(mainView.filterChips.bounds, to: view) - return frameInView.maxY // 필터 컨테이너의 바닥 높이 - } - - enum ModalState { - case top - case middle - case bottom - } - - private var modalState: ModalState = .bottom - - // MARK: - Lifecycle - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - DispatchQueue.main.async { - self.view.layoutIfNeeded() - let frameInView = self.mainView.filterChips.convert(self.mainView.filterChips.bounds, to: self.view) - self.filterChipsTopY = frameInView.minY - } - } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.tabBarController?.tabBar.isHidden = false - } - - override func viewDidLoad() { - super.viewDidLoad() - setUp() - mainView.mapView.padding = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) - - - locationManager.delegate = self - locationManager.requestWhenInUseAuthorization() - locationManager.desiredAccuracy = kCLLocationAccuracyBest - mainView.mapView.isMyLocationEnabled = true - checkLocationAuthorization() - if let reactor = self.reactor { - reactor.action.onNext(.fetchCategories) - - // 한국 전체 영역에 대한 경계값 설정 - let koreaRegion = ( - northEast: CLLocationCoordinate2D(latitude: 38.0, longitude: 132.0), // 한국 북동쪽 끝 - southWest: CLLocationCoordinate2D(latitude: 33.0, longitude: 124.0) // 한국 남서쪽 끝 - ) - - // 전체 스토어 가져오기 - reactor.action.onNext(.viewportChanged( - northEastLat: koreaRegion.northEast.latitude, - northEastLon: koreaRegion.northEast.longitude, - southWestLat: koreaRegion.southWest.latitude, - southWestLon: koreaRegion.southWest.longitude - )) - - // 데이터 로드 후 모든 마커 생성 및 추가 - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .filter { !$0.isEmpty } - .take(1) // 초기 1회만 - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in - guard let self = self else { return } - - // 스토어 정보 저장 - self.allStores = stores - self.currentStores = stores - - // 모든 마커 생성 및 지도에 추가 (즉시 표시) - self.addAllMarkersToMap(stores: stores) - - // 현재 줌 레벨에 맞게 클러스터링 - self.updateMapWithClustering() - - // 가까운 스토어 표시 등 필요한 작업 - if let location = self.locationManager.location { - self.findAndShowNearestStore(from: location) - } - }) - .disposed(by: disposeBag) - } - - - - carouselView.rx.observe(Bool.self, "hidden") - .distinctUntilChanged() - .subscribe(onNext: { [weak self] isHidden in - guard let self = self, let isHidden = isHidden else { return } - self.mainView.setStoreCardHidden(isHidden, animated: true) - }) - .disposed(by: disposeBag) - - carouselView.onCardTapped = { [weak self] store in - let detailController = DetailController() - detailController.reactor = DetailReactor(popUpID: Int64(store.id)) - - self?.navigationController?.isNavigationBarHidden = false - self?.navigationController?.tabBarController?.tabBar.isHidden = false - - self?.navigationController?.pushViewController(detailController, animated: true) - } - - mainView.mapView.rx.idleAtPosition - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] in - guard let self = self else { return } - if let marker = self.currentMarker, - let storeArray = marker.userData as? [MapPopUpStore], - storeArray.count > 1 { - // 툴팁이 없으면 생성, 있으면 위치 업데이트 - if self.currentTooltipView == nil { - self.configureTooltip(for: marker, stores: storeArray) - } else { - self.updateTooltipPosition() - } - } - self.isMovingToMarker = false - }) - .disposed(by: disposeBag) - - - - - carouselView.onCardScrolled = { [weak self] pageIndex in - guard let self = self, - pageIndex >= 0, - pageIndex < self.currentCarouselStores.count else { return } - - let store = self.currentCarouselStores[pageIndex] - - // 이전 선택 마커 상태 초기화 - if let previousMarker = self.currentMarker, - let previousMarkerView = previousMarker.iconView as? MapMarker { - previousMarkerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: (previousMarker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - - // 스와이프한 스토어에 해당하는 마커 찾기 - let markerToFocus = self.findMarkerForStore(for: store) - - if let markerToFocus = markerToFocus { - // 마커 선택 상태로 업데이트 - if let markerView = markerToFocus.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: (markerToFocus.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - - self.currentMarker = markerToFocus - - // 마이크로 클러스터인 경우 툴팁 처리 - if let storeArray = markerToFocus.userData as? [MapPopUpStore], storeArray.count > 1 { - if self.currentTooltipView == nil || self.currentTooltipCoordinate != markerToFocus.position { - self.configureTooltip(for: markerToFocus, stores: storeArray) - } - - // 툴팁에서 선택된 스토어 업데이트 - if let tooltipIndex = storeArray.firstIndex(where: { $0.id == store.id }) { - (self.currentTooltipView as? MarkerTooltipView)?.selectStore(at: tooltipIndex) - } - } else { - // 단일 마커면 기존 툴팁 제거 - self.currentTooltipView?.removeFromSuperview() - self.currentTooltipView = nil - } - } - } - - if let reactor = self.reactor { - bindViewport(reactor: reactor) - reactor.action.onNext(.fetchCategories) - - } - - } - private func addAllMarkersToMap(stores: [MapPopUpStore]) { - // 기존 마커 제거 - clearAllMarkers() - - // 모든 스토어에 대해 마커 생성하고 바로 지도에 추가 - for store in stores { - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: false - )) - marker.iconView = markerView - marker.map = mainView.mapView // 바로 지도에 추가 - - individualMarkerDictionary[store.id] = marker - } - - Logger.log( - message: "🔍 전체 마커 생성 및 지도에 추가 완료: \(stores.count)개", - category: .debug - ) - } - - private func configureTooltip(for marker: GMSMarker, stores: [MapPopUpStore]) { - Logger.log(message: """ - 툴팁 설정: - - 현재 캐러셀 스토어: \(currentCarouselStores.map { $0.name }) - - 마커 스토어: \(stores.map { $0.name }) - """, category: .debug) - - // 기존 툴팁 제거 - self.currentTooltipView?.removeFromSuperview() - - let tooltipView = MarkerTooltipView() - tooltipView.configure(with: stores) - - // 선택된 상태로 표시 - 첫 번째 정보를 기본 선택 상태로 만듦 - tooltipView.selectStore(at: 0) - - // onStoreSelected 클로저 설정 - tooltipView.onStoreSelected = { [weak self] index in - guard let self = self, index < stores.count else { return } - self.currentCarouselStores = stores - self.carouselView.updateCards(stores) - self.carouselView.scrollToCard(index: index) - - // 선택된 상태로 업데이트 - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: stores.count - )) - } - tooltipView.selectStore(at: index) - Logger.log(message: """ - 툴팁 선택: - - 선택된 스토어: \(stores[index].name) - - 툴팁 인덱스: \(index) - """, category: .debug) - } - - // 툴팁 위치 설정 (예시: 마커 우측에 위치) - let markerPoint = self.mainView.mapView.projection.point(for: marker.position) - let markerHeight = (marker.iconView as? MapMarker)?.imageView.frame.height ?? 32 - tooltipView.frame = CGRect( - x: markerPoint.x , // 마커 오른쪽 10포인트 - y: markerPoint.y - markerHeight - tooltipView.frame.height - 14, - width: tooltipView.frame.width, - height: tooltipView.frame.height - ) - - self.mainView.addSubview(tooltipView) - self.currentTooltipView = tooltipView - self.currentTooltipStores = stores - self.currentTooltipCoordinate = marker.position - } - - // MARK: - Setup - private func setUp() { - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - view.addSubview(carouselView) - carouselView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(140) - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24) - } - carouselView.isHidden = true - mainView.mapView.delegate = self - - addChild(storeListViewController) - view.addSubview(storeListViewController.view) - storeListViewController.didMove(toParent: self) - - storeListViewController.view.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - listViewTopConstraint = make.top.equalToSuperview().offset(view.frame.height).constraint - } - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - storeListViewController.mainView.grabberHandle.addGestureRecognizer(panGesture) - storeListViewController.mainView.addGestureRecognizer(panGesture) - setupPanAndSwipeGestures() - - let mapViewTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMapViewTap(_:))) - mainView.mapView.addGestureRecognizer(mapViewTapGesture) - mapViewTapGesture.delegate = self - - } - - private let defaultZoomLevel: Float = 15.0 - private func addMarkersForAllStores(stores: [MapPopUpStore]) { - // 기존 마커 제거 - clearAllMarkers() - - for store in stores { - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: false - )) - marker.iconView = markerView - - // 여기서는 마커를 맵에 바로 추가하지 않고 딕셔너리에만 저장 - individualMarkerDictionary[store.id] = marker - } - - // allStores 도 저장 - self.currentStores = stores - } - private func setupPanAndSwipeGestures() { - storeListViewController.mainView.grabberHandle.rx.swipeGesture(.up) - .skip(1) - .withUnretained(self) - .subscribe { owner, _ in - Logger.log(message: "⬆️ 위로 스와이프 감지", category: .debug) - switch owner.modalState { - case .bottom: - owner.animateToState(.middle) - case .middle: - owner.animateToState(.top) - case .top: - break - } - } - .disposed(by: disposeBag) - - storeListViewController.mainView.grabberHandle.rx.swipeGesture(.down) - .skip(1) - .withUnretained(self) - .subscribe { owner, _ in - Logger.log(message: "⬇️ 아래로 스와이프 감지됨", category: .debug) - switch owner.modalState { - case .top: - owner.animateToState(.middle) - case .middle: - owner.animateToState(.bottom) - case .bottom: - break - } - } - .disposed(by: disposeBag) - } - - // MARK: - Bind - func bind(reactor: Reactor) { - - // 필터 관련 바인딩 - mainView.filterChips.locationChip.rx.tap - .map { Reactor.Action.filterTapped(.location) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.filterChips.categoryChip.rx.tap - .map { Reactor.Action.filterTapped(.category) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - // 리스트 버튼 탭 - mainView.listButton.rx.tap - .withUnretained(self) - .subscribe { owner, _ in - owner.animateToState(.middle) // 버튼 눌렀을 때 상태를 middle로 변경 - } - .disposed(by: disposeBag) - - // 위치 버튼 - mainView.locationButton.rx.tap - .bind { [weak self] _ in - guard let self = self, - let location = self.locationManager.location else { return } - - let camera = GMSCameraPosition.camera( - withLatitude: location.coordinate.latitude, - longitude: location.coordinate.longitude, - zoom: 15 - ) - self.mainView.mapView.animate(to: camera) - } - .disposed(by: disposeBag) - - - - - - mainView.filterChips.onRemoveLocation = { [weak self] in - guard let self = self else { return } - // 필터 제거 액션 - self.reactor?.action.onNext(.clearFilters(.location)) - - // 현재 뷰포트의 바운드로 마커 업데이트 요청 - let bounds = self.mainView.mapView.projection.visibleRegion() - self.reactor?.action.onNext(.viewportChanged( - northEastLat: bounds.farRight.latitude, - northEastLon: bounds.farRight.longitude, - southWestLat: bounds.nearLeft.latitude, - southWestLon: bounds.nearLeft.longitude - )) - - self.clearAllMarkers() - self.clusterMarkerDictionary.values.forEach { $0.map = nil } - self.clusterMarkerDictionary.removeAll() - - // 캐러셀 숨기기 추가 - self.carouselView.isHidden = true - self.carouselView.updateCards([]) - self.currentCarouselStores = [] - self.mainView.setStoreCardHidden(true, animated: true) - - self.updateMapWithClustering() - } - mainView.filterChips.onRemoveCategory = { [weak self] in - guard let self = self else { return } - // 필터 제거 액션 - self.reactor?.action.onNext(.clearFilters(.category)) - - // 현재 뷰포트의 바운드로 마커 업데이트 요청 - let bounds = self.mainView.mapView.projection.visibleRegion() - self.reactor?.action.onNext(.viewportChanged( - northEastLat: bounds.farRight.latitude, - northEastLon: bounds.farRight.longitude, - southWestLat: bounds.nearLeft.latitude, - southWestLon: bounds.nearLeft.longitude - )) - - self.resetSelectedMarker() - - // 만약 지도 위 마커를 전부 제거하고 싶다면 (상황에 따라) - // self.clearAllMarkers() - // self.clusterMarkerDictionary.values.forEach { $0.map = nil } - // self.clusterMarkerDictionary.removeAll() - self.carouselView.isHidden = true - self.carouselView.updateCards([]) - self.currentCarouselStores = [] - self.mainView.setStoreCardHidden(true, animated: true) - } - - Observable.combineLatest( - reactor.state.map { $0.selectedLocationFilters }.distinctUntilChanged(), - reactor.state.map { $0.selectedCategoryFilters }.distinctUntilChanged() - ) { locationFilters, categoryFilters -> (String, String) in - // 지역 필터 텍스트 포맷팅 - let locationText: String - if locationFilters.isEmpty { - locationText = "지역선택" - } else if locationFilters.count > 1 { - locationText = "\(locationFilters[0]) 외 \(locationFilters.count - 1)개" - } else { - locationText = locationFilters[0] - } - - // 카테고리 필터 텍스트 포맷팅 - let categoryText: String - if categoryFilters.isEmpty { - categoryText = "카테고리" - } else if categoryFilters.count > 1 { - categoryText = "\(categoryFilters[0]) 외 \(categoryFilters.count - 1)개" - } else { - categoryText = categoryFilters[0] - } - return (locationText, categoryText) - } - .observe(on: MainScheduler.instance) - .bind { [weak self] locationText, categoryText in - Logger.log( - message: """ - 필터 업데이트: - 📍 위치: \(locationText) - 🏷️ 카테고리: \(categoryText) - """, - category: .debug - ) - self?.mainView.filterChips.update( - locationText: locationText, - categoryText: categoryText - ) - } - .disposed(by: disposeBag) - - - reactor.state.map { $0.activeFilterType } - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] filterType in - guard let self = self else { return } - if let filterType = filterType { - self.presentFilterBottomSheet(for: filterType) - } else { - self.dismissFilterBottomSheet() - } - }) - .disposed(by: disposeBag) - reactor.state.map { $0.searchResult } - .distinctUntilChanged() - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind { [weak self] store in - guard let self = self else { return } - let camera = GMSCameraPosition.camera( - withLatitude: store.latitude, - longitude: store.longitude, - zoom: 15 - ) - self.mainView.mapView.animate(to: camera) - self.addMarker(for: store) - } - .disposed(by: disposeBag) -// mainView.searchInput.onSearch = { [weak self] query in -// self?.reactor?.action.onNext(.searchTapped(query)) -// } -// -// reactor.state.map { $0.isLoading } -// .distinctUntilChanged() -// .observe(on: MainScheduler.instance) -// .bind { [weak self] isLoading in -// self?.mainView.searchInput.searchTextField.isEnabled = !isLoading -//// self?.mainView.searchInput.setLoading(isLoading) -// } -// .disposed(by: disposeBag) -// 보류 - mainView.searchInput.rx.tapGesture() - .when(.recognized) - .throttle(.milliseconds(500), scheduler: MainScheduler.instance) - .withUnretained(self) - .subscribe(onNext: { owner, _ in - print("tapGesture fired - push 시작") - let searchMainVC = SearchMainController() - searchMainVC.reactor = SearchMainReactor() - owner.navigationController?.pushViewController(searchMainVC, animated: true) - print("pushViewController 호출 완료") - }) - .disposed(by: disposeBag) - - - - reactor.state.map { $0.searchResults } - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .bind { [weak self] results in - guard let self = self else { return } - - // 이전 선택된 마커, 툴팁, 캐러셀 초기화 - self.mainView.mapView.clear() - self.storeListViewController.reactor?.action.onNext(.setStores([])) - self.carouselView.updateCards([]) - self.carouselView.isHidden = true - self.resetSelectedMarker() // 추가된 부분 - - // 결과가 없으면 스토어 카드 숨김 후 종료 - if results.isEmpty { - self.mainView.setStoreCardHidden(true, animated: true) - return - } else { - self.mainView.setStoreCardHidden(false, animated: true) - } - - // 새 결과로 마커 추가 및 업데이트 - self.addMarkers(for: results) - - // 스토어 리스트 업데이트 - let storeItems = results.map { $0.toStoreItem() } - self.storeListViewController.reactor?.action.onNext(.setStores(storeItems)) - - // 캐러셀 업데이트 - self.carouselView.updateCards(results) - self.carouselView.isHidden = false - self.currentCarouselStores = results - - // 만약 현재 선택된 마커의 스토어가 새로운 결과에 없다면, 선택 상태 초기화 - if let currentMarker = self.currentMarker, - let selectedStore = currentMarker.userData as? MapPopUpStore, - !results.contains(where: { $0.id == selectedStore.id }) { - self.resetSelectedMarker() - } - - // 첫 번째 검색 결과로 지도 이동 - if let firstStore = results.first { - let camera = GMSCameraPosition.camera( - withLatitude: firstStore.latitude, - longitude: firstStore.longitude, - zoom: 15 - ) - self.mainView.mapView.animate(to: camera) - } - } - .disposed(by: disposeBag) - - - - -// reactor.state.map { $0.searchResults.isEmpty } -// .distinctUntilChanged() -// .skip(1) // 초기값 스킵 -// .observe(on: MainScheduler.instance) -// .bind { [weak self] isEmpty in -// guard let self = self else { return } -// if isEmpty { -// self.showAlert( -// title: "검색 결과 없음", -// message: "검색 결과가 없습니다. 다른 키워드로 검색해보세요." -// ) -// } -// } -// .disposed(by: disposeBag) - } - - - // MARK: - List View Control - private func toggleListView() { - UIView.animate(withDuration: 0.3) { - let middleOffset = -self.view.frame.height * 0.7 - self.listViewTopConstraint?.update(offset: middleOffset) - self.modalState = .middle - self.mainView.searchFilterContainer.backgroundColor = .clear - self.view.layoutIfNeeded() - } - - } - - - func addMarker(for store: MapPopUpStore) { - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: store.toMarkerInput()) - marker.iconView = markerView - marker.map = mainView.mapView - } - - @objc private func handleMapViewTap(_ gesture: UITapGestureRecognizer) { - // 리스트뷰가 현재 보이는 상태(중간 또는 상단)일 때만 내림 - if modalState == .middle || modalState == .top { - Logger.log(message: "맵뷰 탭 감지: 리스트뷰 내림", category: .debug) - animateToState(.bottom) - } - } - - @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { - let translation = gesture.translation(in: view) - let velocity = gesture.velocity(in: view) - - switch gesture.state { - case .changed: - if let constraint = listViewTopConstraint { - let currentOffset = constraint.layoutConstraints.first?.constant ?? 0 - let newOffset = currentOffset + translation.y - - // 오프셋 제한 범위 설정 - let minOffset: CGFloat = filterContainerBottomY // 필터 컨테이너 바닥 제한 - let maxOffset: CGFloat = view.frame.height // 최하단 제한 - let clampedOffset = min(max(newOffset, minOffset), maxOffset) - - constraint.update(offset: clampedOffset) - gesture.setTranslation(.zero, in: view) - - if modalState == .top { - adjustMapViewAlpha(for: clampedOffset, minOffset: minOffset, maxOffset: maxOffset) - } - } - - case .ended: - if let constraint = listViewTopConstraint { - let currentOffset = constraint.layoutConstraints.first?.constant ?? 0 - let middleY = view.frame.height * 0.3 // 중간 지점 기준 높이 - let targetState: ModalState - - // 속도와 위치를 기반으로 상태 결정 - if velocity.y > 500 { // 아래로 빠르게 드래그 - targetState = .bottom - } else if velocity.y < -500 { // 위로 빠르게 드래그 - targetState = .top - } else if currentOffset < middleY * 0.7 { - targetState = .top - } else if currentOffset < view.frame.height * 0.7 { - targetState = .middle - } else { - targetState = .bottom - } - - animateToState(targetState) - } - - default: - break - } - } - - private func adjustMapViewAlpha(for offset: CGFloat, minOffset: CGFloat, maxOffset: CGFloat) { - let middleOffset = view.frame.height * 0.3 - - if offset <= minOffset { - mainView.mapView.alpha = 0 // 탑에서는 완전히 숨김 - } else if offset >= maxOffset { - mainView.mapView.alpha = 1 // 바텀에서는 완전히 보임 - } else if offset <= middleOffset { - let progress = (offset - minOffset) / (middleOffset - minOffset) - mainView.mapView.alpha = progress - } else { - mainView.mapView.alpha = 1 - } - } - - - private func updateMapViewAlpha(for offset: CGFloat, minOffset: CGFloat, maxOffset: CGFloat) { - let progress = (maxOffset - offset) / (maxOffset - minOffset) // 0(탑) ~ 1(바텀) - mainView.mapView.alpha = max(0, min(progress, 1)) // 0(완전히 가림) ~ 1(완전히 보임) - } - private func animateToState(_ state: ModalState) { - guard modalState != state else { return } - self.view.layoutIfNeeded() - - UIView.animate(withDuration: 0.3, animations: { - switch state { - case .top: - let filterChipsFrame = self.mainView.filterChips.convert( - self.mainView.filterChips.bounds, - to: self.view - ) - self.mainView.mapView.alpha = 0 // 탑 상태에서는 숨김 - self.storeListViewController.setGrabberHandleVisible(false) - self.listViewTopConstraint?.update(offset: filterChipsFrame.maxY) - self.mainView.searchInput.setBackgroundColor(.g50) - - - - case .middle: - self.storeListViewController.setGrabberHandleVisible(true) - let offset = max(self.view.frame.height * 0.3, self.filterContainerBottomY) - self.listViewTopConstraint?.update(offset: offset) - self.storeListViewController.mainView.layer.cornerRadius = 20 - self.storeListViewController.mainView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.mainView.mapView.alpha = 1 - self.mainView.mapView.isHidden = false - self.mainView.searchInput.setBackgroundColor(.white) - - // 리스트뷰 표시 시, 이미 가져온 전체 스토어 데이터를 바로 사용 - if !self.allStores.isEmpty { - // 이미 데이터가 있으면 바로 표시 - self.fetchStoreDetails(for: self.allStores) - - Logger.log( - message: "✅ 기존 스토어 목록으로 리스트뷰 업데이트: \(self.allStores.count)개", - category: .debug - ) - } else { - // 데이터가 없으면 전체 영역 요청 (이 부분도 필요하지만, 초기 로드 시 이미 불러왔을 가능성이 높음) - if let reactor = self.reactor { - let koreaRegion = ( - northEast: CLLocationCoordinate2D(latitude: 38.0, longitude: 132.0), - southWest: CLLocationCoordinate2D(latitude: 33.0, longitude: 124.0) - ) - - reactor.action.onNext(.viewportChanged( - northEastLat: koreaRegion.northEast.latitude, - northEastLon: koreaRegion.northEast.longitude, - southWestLat: koreaRegion.southWest.latitude, - southWestLon: koreaRegion.southWest.longitude - )) - - // 즉시 구독하지만 최대 1회만 실행 - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .filter { !$0.isEmpty } - .take(1) - .subscribe(onNext: { [weak self] allStores in - guard let self = self else { return } - - self.allStores = allStores - self.fetchStoreDetails(for: allStores) - }) - .disposed(by: self.disposeBag) - } - } - case .bottom: - self.storeListViewController.setGrabberHandleVisible(true) - self.listViewTopConstraint?.update(offset: self.view.frame.height) - self.mainView.mapView.alpha = 1 - self.mainView.mapView.isHidden = false - self.mainView.searchInput.setBackgroundColor(.white) - - } - - self.view.layoutIfNeeded() - }) { _ in - self.modalState = state - Logger.log(message: ". 현재 상태: \(state)", category: .debug) - } - } - - - // updateMapWithClustering() 메서드 전체 구현 - private func updateMapWithClustering() { - let currentZoom = mainView.mapView.camera.zoom - let level = MapZoomLevel.getLevel(from: currentZoom) - let visibleRegion = mainView.mapView.projection.visibleRegion() - let visibleBoundsRect = GMSCoordinateBounds(region: visibleRegion) - - // 현재 뷰포트에 있는 스토어만 필터링 (allStores가 빈 경우 currentStores 사용) - let visibleStores = !allStores.isEmpty ? - allStores.filter { store in visibleBoundsRect.contains(store.coordinate) } : - currentStores - - // 현재 화면에 보이는 스토어 업데이트 - currentStores = visibleStores - - - CATransaction.begin() - CATransaction.setDisableActions(true) - - switch level { - case .detailed: - let newStoreIds = Set(visibleStores.map { $0.id }) - let groupedDict = groupStoresByExactLocation(visibleStores) - - clusterMarkerDictionary.values.forEach { $0.map = nil } - clusterMarkerDictionary.removeAll() - - for (coordinate, storeGroup) in groupedDict { - if storeGroup.count == 1, let store = storeGroup.first { - if let existingMarker = individualMarkerDictionary[store.id] { - if existingMarker.position != store.coordinate { - existingMarker.position = store.coordinate - } - - existingMarker.map = mainView.mapView - - if let markerView = existingMarker.iconView as? MapMarker, - markerView.currentInput?.isSelected != (existingMarker == currentMarker) { - markerView.injection(with: .init( - isSelected: (existingMarker == currentMarker), - isCluster: false - )) - } - } else { - let marker = GMSMarker(position: store.coordinate) - marker.userData = store - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: false - )) - marker.iconView = markerView - marker.map = mainView.mapView - - individualMarkerDictionary[store.id] = marker - } - } else { - guard let firstStore = storeGroup.first else { continue } - let markerKey = firstStore.id - - if let existingMarker = individualMarkerDictionary[markerKey] { - existingMarker.userData = storeGroup - - existingMarker.map = mainView.mapView - - if let markerView = existingMarker.iconView as? MapMarker, - markerView.currentInput?.count != storeGroup.count || - markerView.currentInput?.isSelected != (existingMarker == currentMarker) { - markerView.injection(with: .init( - isSelected: (existingMarker == currentMarker), - isCluster: false, - count: storeGroup.count - )) - } - } else { - // 새 마커 생성 - let marker = GMSMarker(position: firstStore.coordinate) - marker.userData = storeGroup - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: storeGroup.count - )) - marker.iconView = markerView - marker.map = mainView.mapView - - individualMarkerDictionary[markerKey] = marker - } - } - } - - for (id, marker) in individualMarkerDictionary { - if !newStoreIds.contains(id) { - marker.map = nil - } - } - - case .district, .city, .country: - // 개별 마커 숨기기 - individualMarkerDictionary.values.forEach { $0.map = nil } - - // 클러스터 생성 및 업데이트 - let clusters = clusteringManager.clusterStores(visibleStores, at: currentZoom) - let activeClusterKeys = Set(clusters.map { $0.cluster.name }) - - // 클러스터 마커 업데이트 - for cluster in clusters { - let clusterKey = cluster.cluster.name - - if let existingMarker = clusterMarkerDictionary[clusterKey] { - // 기존 마커 재사용 - if existingMarker.position != cluster.cluster.coordinate { - existingMarker.position = cluster.cluster.coordinate - } - existingMarker.userData = cluster - existingMarker.map = mainView.mapView - - if let markerView = existingMarker.iconView as? MapMarker, - markerView.currentInput?.count != cluster.storeCount { - markerView.injection(with: .init( - isSelected: false, - isCluster: true, - regionName: cluster.cluster.name, - count: cluster.storeCount - )) - } - } else { - // 새 클러스터 마커 생성 - let marker = GMSMarker(position: cluster.cluster.coordinate) - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - marker.userData = cluster - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: true, - regionName: cluster.cluster.name, - count: cluster.storeCount - )) - marker.iconView = markerView - marker.map = mainView.mapView - - clusterMarkerDictionary[clusterKey] = marker - } - } - - for (key, marker) in clusterMarkerDictionary { - if !activeClusterKeys.contains(key) { - marker.map = nil - } - } - } - - CATransaction.commit() - } - private func clearAllMarkers() { - individualMarkerDictionary.values.forEach { $0.map = nil } - individualMarkerDictionary.removeAll() - - clusterMarkerDictionary.values.forEach { $0.map = nil } - clusterMarkerDictionary.removeAll() - - markerDictionary.values.forEach { $0.map = nil } - markerDictionary.removeAll() - } - - private func groupStoresByExactLocation(_ stores: [MapPopUpStore]) -> [CoordinateKey: [MapPopUpStore]] { - var dict = [CoordinateKey: [MapPopUpStore]]() - for store in stores { - let key = CoordinateKey(latitude: store.latitude, longitude: store.longitude) - dict[key, default: []].append(store) - } - return dict - } - - - private func updateIndividualMarkers(_ stores: [MapPopUpStore]) { - var newMarkerIDs = Set() - - for store in stores { - newMarkerIDs.insert(store.id) - if let marker = individualMarkerDictionary[store.id] { - if marker.position.latitude != store.latitude || marker.position.longitude != store.longitude { - marker.position = store.coordinate - } - } else { - // 새 마커 생성 및 추가 - let marker = GMSMarker(position: store.coordinate) - marker.userData = store - - let markerView = MapMarker() - markerView.injection(with: store.toMarkerInput()) - marker.iconView = markerView - marker.map = mainView.mapView - - individualMarkerDictionary[store.id] = marker - } - } - for (id, marker) in individualMarkerDictionary { - if !newMarkerIDs.contains(id) { - marker.map = nil - individualMarkerDictionary.removeValue(forKey: id) - } - } - } - private func updateClusterMarkers(_ clusters: [ClusterMarkerData]) { - for clusterData in clusters { - let clusterKey = clusterData.cluster.name - let fixedCoordinate = clusterData.cluster.coordinate - - if let marker = clusterMarkerDictionary[clusterKey] { - if marker.position.latitude != fixedCoordinate.latitude || - marker.position.longitude != fixedCoordinate.longitude { - marker.position = fixedCoordinate - } - } else { - let marker = GMSMarker() - marker.position = fixedCoordinate - marker.userData = clusterData - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: true, - regionName: clusterData.cluster.name, - count: clusterData.storeCount - )) - marker.iconView = markerView - marker.map = mainView.mapView - - clusterMarkerDictionary[clusterKey] = marker - } - } - } - - - - func presentFilterBottomSheet(for filterType: FilterType) { - guard let reactor = self.reactor else { return } - - let sheetReactor = FilterBottomSheetReactor( - savedSubRegions: reactor.currentState.selectedLocationFilters, - savedCategories: reactor.currentState.selectedCategoryFilters - ) - let viewController = FilterBottomSheetViewController(reactor: sheetReactor) - - let initialIndex = (filterType == .location) ? 0 : 1 - viewController.containerView.segmentedControl.selectedSegmentIndex = initialIndex - sheetReactor.action.onNext(.segmentChanged(initialIndex)) - - viewController.onSave = { [weak self] filterData in - guard let self = self else { return } - self.reactor?.action.onNext(.updateBothFilters( - locations: filterData.locations, - categories: filterData.categories - )) - self.reactor?.action.onNext(.filterTapped(nil)) - - let bounds = self.mainView.mapView.projection.visibleRegion() - self.reactor?.action.onNext(.viewportChanged( - northEastLat: bounds.farRight.latitude, - northEastLon: bounds.farRight.longitude, - southWestLat: bounds.nearLeft.latitude, - southWestLon: bounds.nearLeft.longitude - )) - } - - viewController.onDismiss = { [weak self] in - self?.reactor?.action.onNext(.filterTapped(nil)) - } - - viewController.modalPresentationStyle = .overFullScreen - present(viewController, animated: false) { - viewController.showBottomSheet() - } - - currentFilterBottomSheet = viewController - } - private func dismissFilterBottomSheet() { - if let bottomSheet = currentFilterBottomSheet { - bottomSheet.hideBottomSheet() - } - currentFilterBottomSheet = nil - } - //기본 마커 - private func addMarkers(for stores: [MapPopUpStore]) { - mainView.mapView.clear() - markerDictionary.removeAll() - - for store in stores { - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - - let markerView = MapMarker() - markerView.injection(with: .init( - isSelected: false, - isCluster: false - )) - marker.iconView = markerView - marker.map = mainView.mapView - markerDictionary[store.id] = marker - } - } - private func updateListView(with results: [MapPopUpStore]) { - // MapPopUpStore 배열을 StoreItem 배열로 변환 - let storeItems = results.map { $0.toStoreItem() } - storeListViewController.reactor?.action.onNext(.setStores(storeItems)) - } - - private func showAlert(title: String, message: String) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "확인", style: .default, handler: nil)) - present(alert, animated: true, completion: nil) - } - // 캐러셀 영역을 제외한 실제 가시 영역 계산 함수 - private func getEffectiveViewport() -> GMSCoordinateBounds { - // 기본 가시 영역 가져오기 - let visibleRegion = mainView.mapView.projection.visibleRegion() - - // 캐러셀이 보이지 않으면 전체 영역 반환 - if carouselView.isHidden { - return GMSCoordinateBounds(region: visibleRegion) - } - - // 캐러셀 상단 Y 좌표를 지도 좌표로 변환 - let carouselTopY = carouselView.frame.minY - - // 화면 좌표계에서 캐러셀 상단 라인을 생성 (좌우 전체 폭) - let leftPoint = CGPoint(x: 0, y: carouselTopY) - let rightPoint = CGPoint(x: view.frame.width, y: carouselTopY) - - // 화면 좌표를 지도 좌표로 변환 - let leftCoordinate = mainView.mapView.projection.coordinate(for: leftPoint) - let rightCoordinate = mainView.mapView.projection.coordinate(for: rightPoint) - - // 캐러셀 영역을 제외한 북쪽 경계 결정 (원래 북쪽 경계는 그대로 유지) - let adjustedSouthWest = CLLocationCoordinate2D( - latitude: max(leftCoordinate.latitude, rightCoordinate.latitude), - longitude: visibleRegion.nearLeft.longitude - ) - - // 조정된 경계로 새 영역 생성 - return GMSCoordinateBounds( - coordinate: visibleRegion.farLeft, - coordinate: adjustedSouthWest - ) - } - - - - // MARK: - Location - private func checkLocationAuthorization() { - switch locationManager.authorizationStatus { - case .notDetermined: - locationManager.requestWhenInUseAuthorization() - case .authorizedWhenInUse, .authorizedAlways: - locationManager.startUpdatingLocation() - case .denied, .restricted: - Logger.log( - message: "위치 서비스가 비활성화되었습니다. 설정에서 권한을 확인해주세요.", - category: .error - ) - @unknown default: - break - } - } -} - -// MARK: - CLLocationManagerDelegate -extension MapViewController: CLLocationManagerDelegate { - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { return } - - currentMarker?.map = nil - currentMarker = nil - carouselView.isHidden = true - currentCarouselStores = [] - - let camera = GMSCameraPosition.camera( - withLatitude: location.coordinate.latitude, - longitude: location.coordinate.longitude, - zoom: 15 - ) - mainView.mapView.animate(to: camera) - - // 카메라 이동이 완료된 후 가장 가까운 스토어 찾기 - mainView.mapView.rx.idleAtPosition - .take(1) - .subscribe(onNext: { [weak self] _ in - guard let self = self else { return } - self.findAndShowNearestStore(from: location) - }) - .disposed(by: disposeBag) - - locationManager.stopUpdatingLocation() - } - - - private func findAndShowNearestStore(from location: CLLocation) { - guard !currentStores.isEmpty else { - Logger.log(message: "현재위치 표기할 스토어가 없습니다", category: .debug) - return - } - - resetSelectedMarker() - - let nearestStore = currentStores.min { store1, store2 in - let location1 = CLLocation(latitude: store1.latitude, longitude: store1.longitude) - let location2 = CLLocation(latitude: store2.latitude, longitude: store2.longitude) - return location.distance(from: location1) < location.distance(from: location2) - } - - if let store = nearestStore, let marker = findMarkerForStore(for: store) { - // 카메라 이동 없이 선택된 마커만 업데이트합니다. - _ = handleSingleStoreTap(marker, store: store) - } - // 만약 마커가 없다면, 기존 로직과 같이 마커를 생성하고 선택 상태를 업데이트 - else if let store = nearestStore { - let marker = GMSMarker() - marker.position = store.coordinate - marker.userData = store - marker.groundAnchor = CGPoint(x: 0.5, y: 1.0) - - let markerView = MapMarker() - markerView.injection(with: .init(isSelected: true, isCluster: false, count: 1)) - marker.iconView = markerView - marker.map = mainView.mapView - - individualMarkerDictionary[store.id] = marker - currentMarker = marker - carouselView.updateCards([store]) - currentCarouselStores = [store] - carouselView.scrollToCard(index: 0) - mainView.setStoreCardHidden(false, animated: true) - } - } - } - -// MARK: - GMSMapViewDelegate -extension MapViewController: GMSMapViewDelegate { - func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { - let hitBoxSize: CGFloat = 44 // 터치 영역 크기 - let markerPoint = mapView.projection.point(for: marker.position) - let touchPoint = mapView.projection.point(for: marker.position) - - let distance = sqrt( - pow(markerPoint.x - touchPoint.x, 2) + - pow(markerPoint.y - touchPoint.y, 2) - ) - - // 터치 영역을 벗어난 경우 무시 - if distance > hitBoxSize / 2 { - return false - } - // (1) 구/시 단위 클러스터 - if let clusterData = marker.userData as? ClusterMarkerData { - return handleRegionalClusterTap(marker, clusterData: clusterData) - } - // 동일 좌표 마이크로 클러스터 - else if let storeArray = marker.userData as? [MapPopUpStore] { - if storeArray.count > 1 { - return handleMicroClusterTap(marker, storeArray: storeArray) - } else if let singleStore = storeArray.first { - return handleSingleStoreTap(marker, store: singleStore) - } - } - // 단일 스토어 - else if let singleStore = marker.userData as? MapPopUpStore { - return handleSingleStoreTap(marker, store: singleStore) - } - - return false - } - - - func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - if !isMovingToMarker { - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - currentTooltipStores = [] - updateMapWithClustering() - - // 캐러셀 초기화 - carouselView.isHidden = true - carouselView.updateCards([]) - currentCarouselStores = [] - } - } - - - - func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { - if gesture && !isMovingToMarker { - resetSelectedMarker() - } - } - /// 지도 빈 공간 탭 → 기존 마커/캐러셀 해제 - func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { - guard !isMovingToMarker else { return } - - // 현재 선택된 마커의 상태를 완전히 초기화 - if let currentMarker = currentMarker { - if let markerView = currentMarker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: (currentMarker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } -// currentMarker.map = nil - self.currentMarker = nil - } - - // 툴팁 제거 - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - currentTooltipStores = [] - currentTooltipCoordinate = nil - - // 캐러셀 초기화 - carouselView.isHidden = true - carouselView.updateCards([]) - self.currentCarouselStores = [] - mainView.setStoreCardHidden(true, animated: true) - - // 클러스터링 업데이트 - updateMapWithClustering() - } - - - - - // MARK: - Helper for single marker tap - func handleSingleStoreTap(_ marker: GMSMarker, store: MapPopUpStore) -> Bool { - isMovingToMarker = true - - // 이전 마커 선택 상태 해제 - if let previousMarker = currentMarker, - let previousMarkerView = previousMarker.iconView as? MapMarker { - previousMarkerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: (previousMarker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - - // 새 마커 선택 상태로 설정 - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: (marker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - - currentMarker = marker - - // 캐러셀에 표시할 스토어 확인 - if currentCarouselStores.isEmpty || !currentCarouselStores.contains(where: { $0.id == store.id }) { - // 현재 뷰포트의 모든 스토어를 가져오기 - let visibleRegion = mainView.mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let visibleStores = currentStores.filter { store in - bounds.contains(CLLocationCoordinate2D( - latitude: store.latitude, - longitude: store.longitude - )) - } - - if !visibleStores.isEmpty { - // 뷰포트의 모든 스토어를 캐러셀에 표시 - currentCarouselStores = visibleStores - carouselView.updateCards(visibleStores) - - // 선택한 스토어의 인덱스를 찾아 스크롤 - if let index = visibleStores.firstIndex(where: { $0.id == store.id }) { - carouselView.scrollToCard(index: index) - } - } else { - // 뷰포트에 다른 스토어가 없는 경우, 선택한 스토어만 표시 - currentCarouselStores = [store] - carouselView.updateCards([store]) - } - } else { - if let index = currentCarouselStores.firstIndex(where: { $0.id == store.id }) { - carouselView.scrollToCard(index: index) - } - } - - carouselView.isHidden = false - mainView.setStoreCardHidden(false, animated: true) - - // 툴팁 처리 - if let storeArray = marker.userData as? [MapPopUpStore], storeArray.count > 1 { - // 마이크로 클러스터인 경우 툴팁 표시 - configureTooltip(for: marker, stores: storeArray) - // 해당 스토어의 툴팁 인덱스 선택 - if let index = storeArray.firstIndex(where: { $0.id == store.id }) { - (currentTooltipView as? MarkerTooltipView)?.selectStore(at: index) - } - } else { - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - } - - isMovingToMarker = false - return true - } - - - - - func handleRegionalClusterTap(_ marker: GMSMarker, clusterData: ClusterMarkerData) -> Bool { - let currentZoom = mainView.mapView.camera.zoom - let currentLevel = MapZoomLevel.getLevel(from: currentZoom) - - switch currentLevel { - case .city: // 시 단위 클러스터 - let districtZoomLevel: Float = 10.0 - let camera = GMSCameraPosition(target: marker.position, zoom: districtZoomLevel) - mainView.mapView.animate(to: camera) - case .district: // 구 단위 클러스터 - let detailedZoomLevel: Float = 12.0 - let camera = GMSCameraPosition(target: marker.position, zoom: detailedZoomLevel) - mainView.mapView.animate(to: camera) - default: - break - } - - // 클러스터에 포함된 스토어들만 표시하도록 마커 업데이트 - updateMarkersForCluster(stores: clusterData.cluster.stores) - - // 캐러셀 업데이트 - carouselView.updateCards(clusterData.cluster.stores) - carouselView.isHidden = false - self.currentCarouselStores = clusterData.cluster.stores - - return true - } - - func handleMicroClusterTap(_ marker: GMSMarker, storeArray: [MapPopUpStore]) -> Bool { - // 이미 선택된 마커를 다시 탭할 때 - if currentMarker == marker { - // 툴팁과 캐러셀만 숨기고, 마커의 선택 상태는 유지 - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - currentTooltipStores = [] - currentTooltipCoordinate = nil - - carouselView.isHidden = true - carouselView.updateCards([]) - currentCarouselStores = [] - - // 마커 상태 업데이트 - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: storeArray.count - )) - } - - currentMarker = nil - isMovingToMarker = false - return false - } - - isMovingToMarker = true - - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - - if let previousMarker = currentMarker, - let previousMarkerView = previousMarker.iconView as? MapMarker { - previousMarkerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: (previousMarker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: storeArray.count - )) - } - currentMarker = marker - - currentCarouselStores = storeArray - carouselView.updateCards(storeArray) - carouselView.isHidden = false - carouselView.scrollToCard(index: 0) - - mainView.setStoreCardHidden(false, animated: true) - - // 지도 이동 및 툴팁 생성 - mainView.mapView.animate(toLocation: marker.position) - - // 툴팁 생성을 idleAtPosition 이벤트까지 기다리지 않고 직접 호출 - if storeArray.count > 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in - guard let self = self else { return } - self.configureTooltip(for: marker, stores: storeArray) - self.isMovingToMarker = false - } - } - - return true - } - private func showNoMarkersToast() { - // 디자인 예정이므로 임시 구현 - Logger.log(message: "현재 지도 영역에 표시할 마커가 없습니다", category: .debug) - } - private func updateTooltipPosition() { - guard let marker = currentMarker, let tooltip = currentTooltipView else { return } - - let markerPoint = mainView.mapView.projection.point(for: marker.position) - var markerCenter = markerPoint - if let iconView = marker.iconView { - markerCenter.y = markerPoint.y - iconView.bounds.height / 1.5 - } - - // 오프셋 값 (디자인에 맞게 조정) - let offsetX: CGFloat = -10 - let offsetY: CGFloat = -10 - - tooltip.frame.origin = CGPoint( - x: markerCenter.x + offsetX, - y: markerCenter.y - tooltip.frame.height - offsetY - ) - } - - private func resetSelectedMarker() { - if let currentMarker = currentMarker, - let markerView = currentMarker.iconView as? MapMarker { - // 기존 마커뷰 재사용, 새로 생성하지 않음 - if let storeArray = currentMarker.userData as? [MapPopUpStore] { - markerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: storeArray.count - )) - } else { - markerView.injection(with: .init( - isSelected: false, - isCluster: false - )) - } - } - - // 툴팁 제거 - currentTooltipView?.removeFromSuperview() - currentTooltipView = nil - currentTooltipStores = [] - currentTooltipCoordinate = nil - carouselView.isHidden = true - carouselView.updateCards([]) - currentCarouselStores = [] - - // 현재 마커 참조 제거 - self.currentMarker = nil - } - - -} - - -extension MapViewController { - func bindViewport(reactor: MapReactor) { - let cameraObservable = Observable.merge([ - mainView.mapView.rx.didChangePosition, - mainView.mapView.rx.idleAtPosition - ]) - .throttle(.milliseconds(200), scheduler: MainScheduler.instance) - .map { [unowned self] in - self.mainView.mapView.camera - } - - let distinctCameraObservable = cameraObservable.distinctUntilChanged { (cam1, cam2) -> Bool in - let loc1 = CLLocation(latitude: cam1.target.latitude, longitude: cam1.target.longitude) - let loc2 = CLLocation(latitude: cam2.target.latitude, longitude: cam2.target.longitude) - let distance = loc1.distance(from: loc2) - return distance < 40 - } - - // 뷰포트가 변경될 때마다 액션 전달 - distinctCameraObservable - .map { [unowned self] _ -> MapReactor.Action in - let visibleRegion = self.mainView.mapView.projection.visibleRegion() - return .viewportChanged( - northEastLat: visibleRegion.farRight.latitude, - northEastLon: visibleRegion.farRight.longitude, - southWestLat: visibleRegion.nearLeft.latitude, - southWestLon: visibleRegion.nearLeft.longitude - ) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - // 현재 뷰포트 내의 스토어 업데이트 - 마커만 업데이트 - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .filter { !$0.isEmpty } - .take(1) // 초기 1회만 실행 - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in - guard let self = self else { return } - - // 현재 위치가 있으면 가장 가까운 스토어, 없으면 첫 번째 스토어 표시 - if let location = self.locationManager.location { - self.findAndShowNearestStore(from: location) - } else if let firstStore = stores.first, - let marker = self.findMarkerForStore(for: firstStore) { - _ = self.handleSingleStoreTap(marker, store: firstStore) - } - - // 현재 스토어 목록 업데이트 및 클러스터링 - self.currentStores = stores - self.updateMapWithClustering() - }) - .disposed(by: disposeBag) - - - // 뷰포트 내 마커 업데이트 및 캐러셀 표시 (수정된 부분) - reactor.state - .map { $0.viewportStores } - .distinctUntilChanged() - .throttle(.milliseconds(200), scheduler: MainScheduler.instance) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] stores in - guard let self = self else { return } - - let effectiveViewport = self.getEffectiveViewport() - let visibleRegion = self.mainView.mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - // 화면에 보이는 스토어만 필터링 - let visibleStores = stores.filter { store in - bounds.contains(CLLocationCoordinate2D( - latitude: store.latitude, - longitude: store.longitude - )) - } - - self.currentStores = visibleStores - - // 개별 마커 레벨인지 확인 - let currentZoom = self.mainView.mapView.camera.zoom - let level = MapZoomLevel.getLevel(from: currentZoom) - - if level == .detailed && !visibleStores.isEmpty { - // 캐러셀에 모든 마커 정보 표시 - let effectiveViewport = self.getEffectiveViewport() - let effectiveStores = visibleStores.filter { store in - effectiveViewport.contains(CLLocationCoordinate2D( - latitude: store.latitude, - longitude: store.longitude - )) - } - - self.currentCarouselStores = visibleStores - self.carouselView.updateCards(visibleStores) - self.carouselView.isHidden = false - self.mainView.setStoreCardHidden(false, animated: true) - - // 현재 선택된 마커가 있으면 해당 위치로 스크롤 - if let currentMarker = self.currentMarker { - // 마커의 스토어 정보 체크 - if let currentStore = currentMarker.userData as? MapPopUpStore, - let index = visibleStores.firstIndex(where: { $0.id == currentStore.id }) { - self.carouselView.scrollToCard(index: index) - } else if let storeArray = currentMarker.userData as? [MapPopUpStore], - let firstStore = storeArray.first, - let index = visibleStores.firstIndex(where: { $0.id == firstStore.id }) { - self.carouselView.scrollToCard(index: index) - } else { - // 선택된 마커가 현재 뷰포트에 없는 경우 - if let markerView = currentMarker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: false, - isCluster: false, - count: (currentMarker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - self.currentMarker = nil - - // 첫 번째 스토어의 마커를 선택 상태로 설정 - if let firstStore = visibleStores.first, - let marker = self.findMarkerForStore(for: firstStore) { - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: (marker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - self.currentMarker = marker - } - - self.carouselView.scrollToCard(index: 0) - } - } else { - // 선택된 마커가 없는 경우, 첫 번째 스토어로 설정 - if let firstStore = visibleStores.first, - let marker = self.findMarkerForStore(for: firstStore) { - if let markerView = marker.iconView as? MapMarker { - markerView.injection(with: .init( - isSelected: true, - isCluster: false, - count: (marker.userData as? [MapPopUpStore])?.count ?? 1 - )) - } - self.currentMarker = marker - } - self.carouselView.scrollToCard(index: 0) - } - } else { - // 클러스터 레벨이거나 마커가 없는 경우 - self.carouselView.isHidden = true - self.carouselView.updateCards([]) - self.currentCarouselStores = [] - self.mainView.setStoreCardHidden(true, animated: true) - - if level == .detailed && visibleStores.isEmpty { - // 개별 마커 레벨인데 마커가 없는 경우 토스트 표시 - self.showNoMarkersToast() - } - } - - self.updateMapWithClustering() - }) - .disposed(by: disposeBag) - - } - private func fetchStoreDetails(for stores: [MapPopUpStore]) { - // 빈 목록이면 처리하지 않음 - guard !stores.isEmpty else { return } - - // 먼저 기본 정보로 StoreItem 생성하여 순서 유지 - let initialStoreItems = stores.map { store in - StoreItem( - id: store.id, - thumbnailURL: store.mainImageUrl ?? "", - category: store.category, - title: store.name, - location: store.address, - dateRange: "\(store.startDate ?? "") ~ \(store.endDate ?? "")", - isBookmarked: false - ) - } - - self.storeListViewController.reactor?.action.onNext(.setStores(initialStoreItems)) - - stores.forEach { store in - self.popUpAPIUseCase.getPopUpDetail( - commentType: "NORMAL", - popUpStoredId: store.id - ) - .asObservable() - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] detail in - self?.storeListViewController.reactor?.action.onNext(.updateStoreBookmark( - id: store.id, - isBookmarked: detail.bookmarkYn - )) - }) - .disposed(by: disposeBag) - } - } - - - private func findMarkerForStore(for store: MapPopUpStore) -> GMSMarker? { - // individualMarkerDictionary에 저장된 모든 마커를 순회 - for marker in individualMarkerDictionary.values { - if let singleStore = marker.userData as? MapPopUpStore, singleStore.id == store.id { - return marker - } - if let storeGroup = marker.userData as? [MapPopUpStore], - storeGroup.contains(where: { $0.id == store.id }) { - return marker - } - } - // 상세 레벨이 아닐 경우 clusterMarkerDictionary에도 동일하게 검색 - for marker in clusterMarkerDictionary.values { - if let storeGroup = marker.userData as? [MapPopUpStore], - storeGroup.contains(where: { $0.id == store.id }) { - return marker - } - } - return nil - } - private func updateMarkersForCluster(stores: [MapPopUpStore]) { - for marker in individualMarkerDictionary.values { - marker.map = nil - } - individualMarkerDictionary.removeAll() - - for marker in clusterMarkerDictionary.values { - marker.map = nil - } - clusterMarkerDictionary.removeAll() - - for store in stores { - addMarker(for: store) - } - } - - - -private func handleMarkerTap(_ marker: GMSMarker) -> Bool { - isMovingToMarker = true - - if let clusterData = marker.userData as? ClusterMarkerData { - let clusterToIndividualZoom: Float = 14.0 - let currentZoom = mainView.mapView.camera.zoom - let newZoom: Float = (currentZoom < clusterToIndividualZoom) - ? clusterToIndividualZoom - : min(mainView.mapView.maxZoom, currentZoom + 1) - - let camera = GMSCameraPosition(target: marker.position, zoom: newZoom) - mainView.mapView.animate(to: camera) - - // 여러 스토어 캐러셀 업데이트 - let multiStores = clusterData.cluster.stores - carouselView.updateCards(multiStores) - carouselView.isHidden = multiStores.isEmpty - currentCarouselStores = multiStores - // 클러스터 마커 강조/해제 등 필요시 추가 - - return true - } - - // 2) 일반 마커일 때 - if let previousMarker = currentMarker { - let markerView = MapMarker() - markerView.injection(with: .init(isSelected: false, isCluster: false)) - previousMarker.iconView = markerView - } - - // 새 마커 강조 - let markerView = MapMarker() - markerView.injection(with: .init(isSelected: true, isCluster: false)) - marker.iconView = markerView - currentMarker = marker - - if let store = marker.userData as? MapPopUpStore { - // 캐러셀에 뷰포트 내 스토어들을 모두 표시 - carouselView.updateCards(currentStores) - carouselView.isHidden = currentStores.isEmpty - currentCarouselStores = currentStores - - // 탭한 스토어가 몇 번째인지 찾아서 스크롤 - if let idx = currentStores.firstIndex(where: { $0.id == store.id }) { - carouselView.scrollToCard(index: idx) - } - } - - return true - } - - - private func getCurrentViewportBounds() -> (northEast: CLLocationCoordinate2D, southWest: CLLocationCoordinate2D) { - let region = mainView.mapView.projection.visibleRegion() - return (northEast: region.farRight, southWest: region.nearLeft) - } - // 커스텀 마커 - func updateMarkers(with newStores: [MapPopUpStore]) { - let newStoreIDs = Set(newStores.map { $0.id }) - - for store in newStores { - if let marker = individualMarkerDictionary[store.id] { - if abs(marker.position.latitude - store.latitude) > 0.0001 || - abs(marker.position.longitude - store.longitude) > 0.0001 { - marker.position = store.coordinate - } - } else { - let marker = GMSMarker(position: store.coordinate) - marker.userData = store - - let markerView = MapMarker() - markerView.injection(with: store.toMarkerInput()) - marker.iconView = markerView - marker.map = mainView.mapView - - individualMarkerDictionary[store.id] = marker - } - } - - for (id, marker) in individualMarkerDictionary { - if !newStoreIDs.contains(id) { - marker.map = nil - individualMarkerDictionary.removeValue(forKey: id) - } - } - } -} -// MARK: - Reactive Extensions -extension Reactive where Base: GMSMapView { - var delegate: DelegateProxy { - return GMSMapViewDelegateProxy.proxy(for: base) - } - - var didChangePosition: Observable { - let proxy = GMSMapViewDelegateProxy.proxy(for: base) - return proxy.didChangePositionSubject.asObservable() - } - - var idleAtPosition: Observable { - let proxy = GMSMapViewDelegateProxy.proxy(for: base) - return proxy.idleAtPositionSubject.asObservable() - } -} -extension CLLocationCoordinate2D: Equatable { - public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { - return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude - } -} -extension MapViewController: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - // 리스트뷰가 보일 때만 커스텀 탭 제스처 허용 - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - // 터치가 리스트뷰 영역에 있으면 커스텀 제스처 트리거하지 않음 - let touchPoint = touch.location(in: view) - - // 리스트뷰가 보이고 터치가 리스트뷰 위에 있으면 탭 처리하지 않음 - if modalState != .bottom { - let listViewY = storeListViewController.view.frame.minY - if touchPoint.y > listViewY { - return false - } - } - - return true - } -} diff --git a/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift b/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift deleted file mode 100644 index f41f6569..00000000 --- a/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift +++ /dev/null @@ -1,3 +0,0 @@ - -import Foundation -import UIKit diff --git a/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift b/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift deleted file mode 100644 index 842bc1de..00000000 --- a/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -//import FloatingPanel -//import UIKit -// -//class StoreListPanelLayout: FloatingPanelLayout { -// let position: FloatingPanelPosition = .bottom -// let initialState: FloatingPanelState = .half -// -// var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { -// return [ -// .full: FloatingPanelLayoutAnchor(absoluteInset: 120, edge: .top, referenceGuide: .superview), -// .half: FloatingPanelLayoutAnchor(fractionalInset: 0.6, edge: .bottom, referenceGuide: .safeArea), -// .tip: FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .bottom, referenceGuide: .safeArea) // 완전히 내림 -// ] -// } -// -// func backdropAlpha(for state: FloatingPanelState) -> CGFloat { -// return 0.0 -// } -// -// func shouldMove(for proposedTargetState: FloatingPanelState) -> Bool { -// return true -// } -// -// var cornerRadius: CGFloat { return 0 } -// -// func surfaceLayout(for size: CGSize) -> NSCollectionLayoutDimension { -// return .fractionalWidth(1.0) -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift deleted file mode 100644 index 7911caca..00000000 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -//import UIKit -//import SnapKit -//import RxSwift -// -// -//class StoreListHeaderView: UICollectionReusableView { -// static let identifier = "StoreListHeaderView" -// -// let searchInput = MapSearchInput() -// let filterChips = MapFilterChips() -// -// var disposeBag = DisposeBag() -// override init(frame: CGRect) { -// super.init(frame: frame) -//// print("[DEBUG] StoreListHeaderView 초기화 - frame: \(frame)") -// setupLayout() -// searchInput.setBackgroundColorForList() -// -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// private func setupLayout() { -// -// backgroundColor = .white -// addSubview(searchInput) -// addSubview(filterChips) -// -// -// searchInput.snp.makeConstraints { make in -// make.top.equalToSuperview().offset(16) -// make.left.equalToSuperview().offset(20) -// make.right.equalToSuperview().inset(16) -// make.height.equalTo(37) -// -// -// } -// -// filterChips.snp.makeConstraints { make in -// make.top.equalTo(searchInput.snp.bottom).offset(11) -// make.left.right.equalToSuperview().inset(20) -// make.height.equalTo(36) -// make.bottom.equalToSuperview().offset(-20) -// } -// -// -// layoutIfNeeded() -// -// -// } -//} diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift deleted file mode 100644 index f5eee867..00000000 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// StoreListPanelLayout.swift -// Poppool -// -// Created by 김기현 on 12/20/24. -// - -import FloatingPanel -import UIKit - -class StoreListPanelLayout: FloatingPanelLayout { - let position: FloatingPanelPosition = .bottom - let initialState: FloatingPanelState = .half - - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - // 스택뷰 (검색바 + 필터칩) 바로 아래에 맞춰서 올라오도록 설정 - .full: FloatingPanelLayoutAnchor(absoluteInset: 90, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.6, edge: .bottom, referenceGuide: .safeArea) - ] - } - - - - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return 0.0 - } - - // 스크롤 뷰와의 상호작용 방지 - func shouldMove(for proposedTargetState: FloatingPanelState) -> Bool { - return true - } - - // 패널의 모서리 둥글기 설정 - var cornerRadius: CGFloat { return 0 } // 페이지처럼 보이도록 모서리 둥글기 제거 - - // 화면 전체를 덮도록 surface 레이아웃 설정 - func surfaceLayout(for size: CGSize) -> NSCollectionLayoutDimension { - return .fractionalWidth(1.0) - } -} diff --git a/Poppool/Poppool/Presentation/Map/StoreLocation.swift b/Poppool/Poppool/Presentation/Map/StoreLocation.swift deleted file mode 100644 index e8e7b76c..00000000 --- a/Poppool/Poppool/Presentation/Map/StoreLocation.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// StoreLocation.swift -// Poppool -// -// Created by 김기현 on 2/7/25. -// - -import Foundation diff --git a/Poppool/Poppool/Presentation/Map/TestViewController.swift b/Poppool/Poppool/Presentation/Map/TestViewController.swift deleted file mode 100644 index 8a35c50d..00000000 --- a/Poppool/Poppool/Presentation/Map/TestViewController.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// ViewController.swift -// Poppool -// -// Created by Porori on 11/24/24. -// - -import UIKit - -import SnapKit -import RxSwift -import RxGesture -import RxCocoa - -class TestViewController: UIViewController { - - private let topView: UIView = { - let view = UIView() - view.backgroundColor = .w100 - view.alpha = 0 - return view - }() - - private let topViewLabel: UILabel = { - let label = UILabel() - label.text = "Top View Label" - return label - }() - - private let bottomView: UIView = { - let view = UIView() - view.backgroundColor = .w100 - return view - }() - - private let gestureBar: UIView = { - let view = UIView() - view.backgroundColor = .g200 - return view - }() - - private let listButton: PPButton = { - let button = PPButton(style: .secondary, text: "리스트 버튼") - return button - }() - - private let disposeBag = DisposeBag() - - private var bottomViewTopConstraints: Constraint? - - enum ModalState { - case top - case middle - case bottom - } - - var modalState: ModalState = .bottom - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .blue - setUpConstratins() - bind() - } - - func setUpConstratins() { - view.addSubview(listButton) - listButton.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview().inset(20) - make.height.equalTo(50) - } - - view.addSubview(topView) - topView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(104) - } - - topView.addSubview(topViewLabel) - topViewLabel.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - view.addSubview(bottomView) - bottomView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - bottomViewTopConstraints = make.top.equalTo(topView.snp.bottom).offset(700).constraint - make.height.equalTo(700) - } - - bottomView.addSubview(gestureBar) - gestureBar.snp.makeConstraints { make in - make.width.equalTo(50) - make.height.equalTo(20) - make.top.equalToSuperview().inset(20) - make.centerX.equalToSuperview() - } - } - - func bind() { - listButton.rx.tap - .withUnretained(self) - .subscribe { (owner, _) in - print("listButtonTapped") - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 124) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - } - } - .disposed(by: disposeBag) - - gestureBar.rx.swipeGesture(.up) - .skip(1) - .withUnretained(self) - .subscribe { (owner, gesture) in - print("swipe up") - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 0) - owner.topView.alpha = 1 - owner.view.layoutIfNeeded() - owner.modalState = .top - } - } - .disposed(by: disposeBag) - - gestureBar.rx.swipeGesture(.down) - .skip(1) - .withUnretained(self) - .subscribe { (owner, gesture) in - print("swipe down") - switch owner.modalState { - case .top: - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 124) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - owner.modalState = .middle - } - case .middle: - UIView.animate(withDuration: 0.3) { - owner.bottomViewTopConstraints?.update(offset: 700) - owner.topView.alpha = 0 - owner.view.layoutIfNeeded() - owner.modalState = .bottom - } - case .bottom: - break - } - - } - .disposed(by: disposeBag) - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift deleted file mode 100644 index 7318d844..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// InstaCommentAddController.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit -import SwiftSoup - -final class InstaCommentAddController: BaseViewController, View { - - typealias Reactor = InstaCommentAddReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = InstaCommentAddView() - private var sections: [any Sectionable] = [] -} - -// MARK: - Life Cycle -extension InstaCommentAddController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } -} - -// MARK: - SetUp -private extension InstaCommentAddController { - func setUp() { - if let layout = reactor?.compositionalLayout { - mainView.contentCollectionView.collectionViewLayout = layout - } - mainView.contentCollectionView.delegate = self - mainView.contentCollectionView.dataSource = self - mainView.contentCollectionView.register(InstaGuideSectionCell.self, forCellWithReuseIdentifier: InstaGuideSectionCell.identifiers) - view.backgroundColor = .g50 - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension InstaCommentAddController { - func bind(reactor: Reactor) { - - SceneDelegate.appDidBecomeActive - .subscribe { _ in - if let url = UIPasteboard.general.string { -// guard let url = URL(string: url) else { return } -// self.crawl(url: url) -// self.fetchHTML(url: url) - } else { - print("Clipboard is empty or does not contain text") - } - } - .disposed(by: disposeBag) - - rx.viewWillAppear - .map { Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.instaButton.rx.tap - .map { Reactor.Action.instaButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - owner.sections = state.sections - owner.mainView.contentCollectionView.reloadData() - } - .disposed(by: disposeBag) - } -} - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension InstaCommentAddController: UICollectionViewDelegate, UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return sections[section].dataCount - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - guard let reactor = reactor else { return cell } - - return cell - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift deleted file mode 100644 index 6546f5d2..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// InstaCommentAddReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import ReactorKit -import RxSwift -import RxCocoa - -final class InstaCommentAddReactor: Reactor { - - // MARK: - Reactor - enum Action { - case viewWillAppear - case instaButtonTapped - } - - enum Mutation { - case loadView - case moveToInsta - } - - struct State { - var sections: [any Sectionable] = [] - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - - lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - UICollectionViewCompositionalLayout { [weak self] section, env in - guard let self = self else { - return NSCollectionLayoutSection(group: NSCollectionLayoutGroup( - layoutSize: .init( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - )) - ) - } - return getSection()[section].getSection(section: section, env: env) - } - }() - - private let guideSection = InstaGuideSection(inputDataList: [ - .init( - imageList: [ - UIImage(named: "icon_instaGuide_0"), - UIImage(named: "icon_instaGuide_1"), - UIImage(named: "icon_instaGuide_2"), - UIImage(named: "icon_instaGuide_3") - ], - title: [ - { - let title = "아래 인스타그램 열기\n버튼을 터치해 앱 열기" - let attributedTitle = NSMutableAttributedString(string: title) - let koreanFont = UIFont.KorFont(style: .bold, size: 20)! - attributedTitle.addAttribute(.font, value: koreanFont, range: NSRange(location: 0, length: title.count)) - attributedTitle.addAttribute(.foregroundColor, value: UIColor.blu500.cgColor, range: (title as NSString).range(of: "인스타그램 열기")) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.2 - attributedTitle.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: title.count)) - return attributedTitle - }(), - { - let title = "원하는 피드의 이미지로 이동 후\n공유하기 > 링크복사 터치하기" - let attributedTitle = NSMutableAttributedString(string: title) - let koreanFont = UIFont.KorFont(style: .bold, size: 20)! - attributedTitle.addAttribute(.font, value: koreanFont, range: NSRange(location: 0, length: title.count)) - attributedTitle.addAttribute(.foregroundColor, value: UIColor.blu500.cgColor, range: (title as NSString).range(of: "공유하기 > 링크복사")) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.2 - attributedTitle.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: title.count)) - return attributedTitle - }(), - { - let title = "아래 이미지 영역을 터치해\n팝풀 앱으로 돌아오기" - let attributedTitle = NSMutableAttributedString(string: title) - let koreanFont = UIFont.KorFont(style: .bold, size: 20)! - attributedTitle.addAttribute(.font, value: koreanFont, range: NSRange(location: 0, length: title.count)) - attributedTitle.addAttribute(.foregroundColor, value: UIColor.blu500.cgColor, range: (title as NSString).range(of: "팝풀 앱")) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.2 - attributedTitle.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: title.count)) - return attributedTitle - }(), - { - let title = "복사된 인스타 피드 이미지와\n함께할 글을 입력 후 등록하기" - let attributedTitle = NSMutableAttributedString(string: title) - let koreanFont = UIFont.KorFont(style: .bold, size: 20)! - attributedTitle.addAttribute(.font, value: koreanFont, range: NSRange(location: 0, length: title.count)) - attributedTitle.addAttribute(.foregroundColor, value: UIColor.blu500.cgColor, range: (title as NSString).range(of: "글을 입력 후 등록")) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = 1.2 - attributedTitle.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: title.count)) - return attributedTitle - }() - ] - ) - ]) - - // MARK: - init - init() { - self.initialState = State() - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - case .viewWillAppear: - return Observable.just(.loadView) - case .instaButtonTapped: - return Observable.just(.moveToInsta) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .loadView: - newState.sections = getSection() - case .moveToInsta: - openInstagram() - - } - return newState - } - - func getSection() -> [any Sectionable] { - return [ - guideSection - ] - } - - func openInstagram() { - // Instagram 앱의 URL Scheme - let instagramURL = URL(string: "instagram://app")! - - if UIApplication.shared.canOpenURL(instagramURL) { - // Instagram 앱 열기 - UIApplication.shared.open(instagramURL, options: [:], completionHandler: nil) - } else { - // Instagram 앱이 설치되지 않은 경우 - let appStoreURL = URL(string: "https://apps.apple.com/app/instagram/id389801252")! - UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift deleted file mode 100644 index daf48dfe..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// InstaCommentAddView.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import SnapKit - -final class InstaCommentAddView: UIView { - - // MARK: - Components - let headerView: PPReturnHeaderView = { - let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "코멘트 작성하기", font: .KorFont(style: .regular, size: 15)) - return view - }() - - let instaButton: UIButton = { - let button = UIButton() - button.setTitleColor(.white, for: .normal) - button.backgroundColor = .g900 - button.layer.cornerRadius = 4 - - let title = "Instagram 열기" - let attributedTitle = NSMutableAttributedString(string: title) - - let englishFont = UIFont.EngFont(style: .medium, size: 15)! - attributedTitle.addAttribute(.font, value: englishFont, range: (title as NSString).range(of: "Instagram")) - - let koreanFont = UIFont.KorFont(style: .medium, size: 15)! - attributedTitle.addAttribute(.font, value: koreanFont, range: (title as NSString).range(of: "열기")) - - button.setAttributedTitle(attributedTitle, for: .normal) - - return button - }() - - private let instaImageView: UIImageView = { - let view = UIImageView() - view.image = UIImage(named: "icon_instagram") - return view - }() - - let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - view.backgroundColor = .g50 - return view - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension InstaCommentAddView { - - func setUpConstraints() { - self.addSubview(headerView) - headerView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - - self.addSubview(instaButton) - instaButton.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview().inset(20) - make.height.equalTo(52) - } - - instaButton.addSubview(instaImageView) - instaImageView.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(20) - make.centerY.equalToSuperview() - make.size.equalTo(22) - } - - self.addSubview(contentCollectionView) - contentCollectionView.snp.makeConstraints { make in - make.top.equalTo(headerView.snp.bottom) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(instaButton.snp.top) - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSection.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSection.swift deleted file mode 100644 index 77095f97..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSection.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// InstaGuideChildSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import RxSwift - -struct InstaGuideChildSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = InstaGuideChildSectionCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(500) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(500) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - section.orthogonalScrollingBehavior = .paging - return section - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSectionCell.swift deleted file mode 100644 index 46596746..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSectionCell.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// InstaGuideChildSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class InstaGuideChildSectionCell: UICollectionViewCell { - - // MARK: - Components - - let disposeBag = DisposeBag() - - private let indexTrailgView: UIView = { - let view = UIView() - view.backgroundColor = .g900 - view.clipsToBounds = true - view.layer.cornerRadius = 4 - return view - }() - - private let indexLabel: UILabel = { - let label = UILabel() - label.font = .EngFont(style: .medium, size: 16) - label.textColor = .w100 - label.textAlignment = .center - return label - }() - - private let titleLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 2 - return label - }() - - private let imageView: UIImageView = { - let view = UIImageView() - view.layer.cornerRadius = 8 - view.clipsToBounds = true - return view - }() - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } -} - -// MARK: - SetUp -private extension InstaGuideChildSectionCell { - func setUpConstraints() { - contentView.addSubview(indexTrailgView) - indexTrailgView.snp.makeConstraints { make in - make.top.equalToSuperview().inset(36) - make.leading.equalToSuperview().inset(20) - make.width.equalTo(40) - make.height.equalTo(33) - } - - indexTrailgView.addSubview(indexLabel) - indexLabel.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - contentView.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(20) - make.top.equalTo(indexTrailgView.snp.bottom).offset(16) - } - contentView.addSubview(imageView) - imageView.snp.makeConstraints { make in - make.size.equalTo(335) - make.centerX.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(28) - make.bottom.equalToSuperview() - } - } -} - -extension InstaGuideChildSectionCell: Inputable { - struct Input { - var image: UIImage? - var title: NSMutableAttributedString? - var index: Int - } - - func injection(with input: Input) { - indexLabel.text = "#\(input.index + 1)" - titleLabel.attributedText = input.title - imageView.image = input.image - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSection.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSection.swift deleted file mode 100644 index 98801b83..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSection.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// InstaGuideSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import RxSwift - -struct InstaGuideSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = InstaGuideSectionCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .estimated(600) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .estimated(600) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift deleted file mode 100644 index aed527cb..00000000 --- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// InstaGuideSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class InstaGuideSectionCell: UICollectionViewCell { - - // MARK: - Components - - let disposeBag = DisposeBag() - - private var autoScrollTimer: Timer? - - private lazy var contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) - view.isScrollEnabled = false - view.backgroundColor = .g50 - return view - }() - - var pageControl: UIPageControl = { - let controller = UIPageControl() - controller.currentPage = 0 - controller.preferredIndicatorImage = UIImage(systemName: "circle") - controller.preferredCurrentPageIndicatorImage = UIImage(systemName: "circle.fill") - controller.pageIndicatorTintColor = .pb30 - controller.currentPageIndicatorTintColor = .g600 - controller.isUserInteractionEnabled = false - controller.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - return controller - }() - - let stopButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "icon_banner_stopButton_gray"), for: .normal) - return button - }() - - private var isAutoBannerPlay: Bool = false - - private var imageSection = InstaGuideChildSection(inputDataList: []) - - lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - UICollectionViewCompositionalLayout { [weak self] section, env in - guard let self = self else { - return NSCollectionLayoutSection(group: NSCollectionLayoutGroup( - layoutSize: .init( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - )) - ) - } - return getSection()[section].getSection(section: section, env: env) - } - }() - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUp() - setUpConstraints() - bind() - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func prepareForReuse() { - super.prepareForReuse() - stopAutoScroll() - } - - deinit { - stopAutoScroll() - } -} - -// MARK: - SetUp -private extension InstaGuideSectionCell { - func setUp() { - contentCollectionView.delegate = self - contentCollectionView.dataSource = self - - contentCollectionView.register( - InstaGuideChildSectionCell.self, - forCellWithReuseIdentifier: InstaGuideChildSectionCell.identifiers - ) - imageSection.currentPage - .withUnretained(self) - .subscribe { (owner, page) in - owner.pageControl.currentPage = page - } - .disposed(by: disposeBag) - } - - func setUpConstraints() { - contentView.addSubview(contentCollectionView) - contentCollectionView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(504) - } - - contentView.addSubview(pageControl) - pageControl.snp.makeConstraints { make in - make.top.equalTo(contentCollectionView.snp.bottom) - make.centerX.equalToSuperview() - make.bottom.equalToSuperview() - } - - contentView.addSubview(stopButton) - stopButton.snp.makeConstraints { make in - make.size.equalTo(8) - make.centerY.equalTo(pageControl.snp.centerY) - make.leading.equalTo(pageControl.snp.trailing).offset(-36) - } - } - - func getSection() -> [any Sectionable] { - return [imageSection] - } - - func startAutoScroll(interval: TimeInterval = 3.0) { - stopAutoScroll() // 기존 타이머를 중지 - isAutoBannerPlay = true - autoScrollTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in - self?.scrollToNextItem() - } - } - - // 자동 스크롤 중지 함수 - func stopAutoScroll() { - isAutoBannerPlay = false - autoScrollTimer?.invalidate() - autoScrollTimer = nil - } - - // 다음 배너로 스크롤 - private func scrollToNextItem() { - - let visibleIndexPaths = contentCollectionView.indexPathsForVisibleItems.sorted() - guard let currentIndex = visibleIndexPaths.first else { return } - - let nextIndex = IndexPath( - item: (currentIndex.item + 1) % imageSection.dataCount, - section: currentIndex.section - ) - - contentCollectionView.scrollToItem(at: nextIndex, at: .centeredHorizontally, animated: true) - pageControl.currentPage = nextIndex.item - } - - func bind() { - stopButton.rx.tap - .withUnretained(self) - .subscribe { (owner, _) in - if owner.isAutoBannerPlay { - owner.stopAutoScroll() - } else { - owner.startAutoScroll() - } - } - .disposed(by: disposeBag) - } -} - -extension InstaGuideSectionCell: Inputable { - struct Input { - var imageList: [UIImage?] - var title: [NSMutableAttributedString?] - } - - func injection(with input: Input) { - pageControl.numberOfPages = input.imageList.count - let datas = zip(input.imageList, input.title).enumerated().map { $0 } - imageSection.inputDataList = datas.map { .init(image: $0.element.0, title: $0.element.1, index: $0.offset)} - contentCollectionView.reloadData() - startAutoScroll() - } -} - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension InstaGuideSectionCell: UICollectionViewDelegate, UICollectionViewDataSource { - - func numberOfSections(in collectionView: UICollectionView) -> Int { - return getSection().count - } - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return getSection()[section].dataCount - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift deleted file mode 100644 index 6db3bb92..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// SearchResultController.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit - -final class SearchResultController: BaseViewController, View { - - typealias Reactor = SearchResultReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = SearchResultView() - private var sections: [any Sectionable] = [] - private let cellTapped: PublishSubject = .init() -} - -// MARK: - Life Cycle -extension SearchResultController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tabBarController?.tabBar.isHidden = true - } -} - -// MARK: - SetUp -private extension SearchResultController { - func setUp() { - if let layout = reactor?.compositionalLayout { - mainView.contentCollectionView.collectionViewLayout = layout - } - mainView.contentCollectionView.delegate = self - mainView.contentCollectionView.dataSource = self - - mainView.contentCollectionView.register( - SearchTitleSectionCell.self, - forCellWithReuseIdentifier: SearchTitleSectionCell.identifiers - ) - mainView.contentCollectionView.register( - SpacingSectionCell.self, - forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) - mainView.contentCollectionView.register( - SearchResultCountSectionCell.self, - forCellWithReuseIdentifier: SearchResultCountSectionCell.identifiers - ) - mainView.contentCollectionView.register( - HomeCardSectionCell.self, - forCellWithReuseIdentifier: HomeCardSectionCell.identifiers - ) - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension SearchResultController { - func bind(reactor: Reactor) { - - cellTapped - .withUnretained(self) - .map({ (owner, indexPath) in - Reactor.Action.cellTapped(controller: owner, indexPath: indexPath) - }) - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - if state.isEmptyResult { - owner.mainView.emptyLabel.isHidden = false - owner.mainView.contentCollectionView.isHidden = true - } else { - owner.mainView.emptyLabel.isHidden = true - owner.mainView.contentCollectionView.isHidden = false - owner.sections = state.sections - owner.mainView.contentCollectionView.reloadData() - } - } - .disposed(by: disposeBag) - } -} - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension SearchResultController: UICollectionViewDelegate, UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return sections[section].dataCount - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - guard let reactor = reactor else { return cell } - if let cell = cell as? HomeCardSectionCell { - cell.bookmarkButton.rx.tap - .map { Reactor.Action.bookmarkButtonTapped(indexPath: indexPath)} - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - cellTapped.onNext(indexPath) - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift deleted file mode 100644 index 4dcde029..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// SearchResultReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import ReactorKit -import RxSwift -import RxCocoa - -final class SearchResultReactor: Reactor { - - // MARK: - Reactor - enum Action { - case returnSearch(text: String) - case bookmarkButtonTapped(indexPath: IndexPath) - case cellTapped(controller: BaseViewController, indexPath: IndexPath) - } - - enum Mutation { - case loadView - case emptyView - case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) - } - - struct State { - var sections: [any Sectionable] = [] - var isEmptyResult: Bool = false - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - private var popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - UICollectionViewCompositionalLayout { [weak self] section, env in - guard let self = self else { - return NSCollectionLayoutSection(group: NSCollectionLayoutGroup( - layoutSize: .init( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - )) - ) - } - return getSection()[section].getSection(section: section, env: env) - } - }() - - private var titleSection = SearchTitleSection(inputDataList: [.init(title: "포함된 팝업", buttonTitle: nil)]) - private var searchCountSection = SearchResultCountSection(inputDataList: [.init(count: 65)]) - private var searchListSection = HomeCardGridSection(inputDataList: []) - private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) - private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - - // MARK: - init - init() { - self.initialState = State() - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - case .cellTapped(let controller, let indexPath): - return Observable.just(.moveToDetailScene(controller: controller, indexPath: indexPath)) - case .returnSearch(let text): - if hasFinalConsonant(text) { - titleSection.inputDataList = [.init(title: "\(text)이 포함된 팝업")] - } else { - titleSection.inputDataList = [.init(title: "\(text)가 포함된 팝업")] - } - return popUpAPIUseCase.getSearchPopUpList(query: text) - .withUnretained(self) - .map { (owner, response) in - owner.searchCountSection.inputDataList = [.init(count: response.popUpStoreList.count)] - let isLogin = response.loginYn - owner.searchListSection.inputDataList = response.popUpStoreList.map({ response in - return .init( - imagePath: response.mainImageUrl, - id: response.id, - category: response.category, - title: response.name, - address: response.address, - startDate: response.startDate, - endDate: response.endDate, - isBookmark: response.bookmarkYn, - isLogin: isLogin - ) - }) - return .loadView - } - .catch { _ in - return Observable.just(.emptyView) - } - case .bookmarkButtonTapped(let indexPath): - let data = searchListSection.inputDataList[indexPath.row] - let isBookmark = data.isBookmark - let id = data.id - searchListSection.inputDataList[indexPath.row].isBookmark.toggle() - ToastMaker.createBookMarkToast(isBookMark: !isBookmark) - if isBookmark { - return userAPIUseCase.deleteBookmarkPopUp(popUpID: id) - .andThen(Observable.just(.loadView)) - } else { - return userAPIUseCase.postBookmarkPopUp(popUpID: id) - .andThen(Observable.just(.loadView)) - } - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .loadView: - newState.isEmptyResult = searchListSection.isEmpty - newState.sections = getSection() - case .emptyView: - newState.isEmptyResult = true - case .moveToDetailScene(let controller, let indexPath): - let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: searchListSection.inputDataList[indexPath.row].id) - controller.navigationController?.pushViewController(nextController, animated: true) - } - return newState - } - - - func getSection() -> [any Sectionable] { - return [ - spacing24Section, - titleSection, - searchCountSection, - spacing16Section, - searchListSection, - spacing64Section - ] - } - func hasFinalConsonant(_ text: String) -> Bool { - guard let lastCharacter = text.last else { return false } - - let unicodeValue = Int(lastCharacter.unicodeScalars.first!.value) - - // 한글 유니코드 범위 체크 - let base = 0xAC00 - let last = 0xD7A3 - guard base...last ~= unicodeValue else { return false } - - // 종성 인덱스 계산 (받침이 있으면 1 이상) - let finalConsonantIndex = (unicodeValue - base) % 28 - return finalConsonantIndex != 0 - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift deleted file mode 100644 index 24ac0177..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SearchResultCountSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import RxSwift - -struct SearchResultCountSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = SearchResultCountSectionCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(20) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) -// item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .absolute(20) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - section.contentInsets = .init(top: 5, leading: 20, bottom: 0, trailing: 20) - - return section - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift deleted file mode 100644 index ffcf0156..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SearchResultCountSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class SearchResultCountSectionCell: UICollectionViewCell { - - // MARK: - Components - - var disposeBag = DisposeBag() - - private let countLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13) - label.textColor = .g600 - return label - }() - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func prepareForReuse() { - super.prepareForReuse() - disposeBag = DisposeBag() - } -} - -// MARK: - SetUp -private extension SearchResultCountSectionCell { - func setUpConstraints() { - contentView.addSubview(countLabel) - countLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - } -} - -extension SearchResultCountSectionCell: Inputable { - struct Input { - var count: Int - } - - func injection(with input: Input) { - countLabel.text = "총 \(input.count)개를 찾았어요." - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift deleted file mode 100644 index 74352be4..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SearchResultView.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import SnapKit - -final class SearchResultView: UIView { - - // MARK: - Components - let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view - }() - - let emptyLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 14, text: "검색 결과가 없어요:(\n다른 키워드로 검색해주세요") - label.textAlignment = .center - label.numberOfLines = 2 - label.textColor = .g400 - return label - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension SearchResultView { - - func setUpConstraints() { - self.addSubview(contentCollectionView) - contentCollectionView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(56) - make.leading.trailing.bottom.equalToSuperview() - } - - self.addSubview(emptyLabel) - emptyLabel.snp.makeConstraints { make in - make.top.equalTo(contentCollectionView.snp.top).inset(193) - make.leading.trailing.equalToSuperview() - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift deleted file mode 100644 index 7b00a9b1..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// SearchController.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit -import RxGesture - -final class SearchController: BaseViewController, View { - - typealias Reactor = SearchReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = SearchView() - private var sections: [any Sectionable] = [] - private let cellTapped: PublishSubject = .init() - private let pageChange: PublishSubject = .init() -} - -// MARK: - Life Cycle -extension SearchController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tabBarController?.tabBar.isHidden = true - } -} - -// MARK: - SetUp -private extension SearchController { - func setUp() { - if let layout = reactor?.compositionalLayout { - mainView.contentCollectionView.collectionViewLayout = layout - } - mainView.contentCollectionView.delegate = self - mainView.contentCollectionView.dataSource = self - mainView.contentCollectionView.register( - SearchTitleSectionCell.self, - forCellWithReuseIdentifier: SearchTitleSectionCell.identifiers - ) - mainView.contentCollectionView.register( - SpacingSectionCell.self, - forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) - mainView.contentCollectionView.register( - CancelableTagSectionCell.self, - forCellWithReuseIdentifier: CancelableTagSectionCell.identifiers - ) - mainView.contentCollectionView.register( - SearchCountTitleSectionCell.self, - forCellWithReuseIdentifier: SearchCountTitleSectionCell.identifiers - ) - mainView.contentCollectionView.register( - HomeCardSectionCell.self, - forCellWithReuseIdentifier: HomeCardSectionCell.identifiers - ) - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension SearchController { - func bind(reactor: Reactor) { - rx.viewWillAppear - .map { Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - cellTapped - .withUnretained(self) - .map({ (owner, indexPath) in - Reactor.Action.cellTapped(indexPath: indexPath, controller: owner) - }) - .bind(to: reactor.action) - .disposed(by: disposeBag) - - pageChange - .throttle(.milliseconds(1000), scheduler: MainScheduler.asyncInstance) - .map { Reactor.Action.changePage } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - owner.sections = state.sections - owner.mainView.contentCollectionView.reloadData() - } - .disposed(by: disposeBag) - } -} - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return sections[section].dataCount - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - guard let userDefaultService = reactor?.userDefaultService else { return cell } - var searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - guard let reactor = reactor else { return cell } - if let cell = cell as? SearchTitleSectionCell { - cell.titleButton.rx.tap - .map { Reactor.Action.recentSearchListAllDeleteButtonTapped } - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } - - if let cell = cell as? CancelableTagSectionCell { - if searchList.isEmpty { - cell.cancelButton.rx.tap - .map { Reactor.Action.categoryDelteButtonTapped(indexPath: indexPath)} - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } else { - if indexPath.section == 3 { - cell.cancelButton.rx.tap - .map { Reactor.Action.recentSearchListDeleteButtonTapped(indexPath: indexPath)} - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } else { - cell.cancelButton.rx.tap - .map { Reactor.Action.categoryDelteButtonTapped(indexPath: indexPath)} - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } - } - } - - if let cell = cell as? SearchCountTitleSectionCell { - cell.sortedButton.rx.tap - .withUnretained(self) - .map({ (owner, _) in - Reactor.Action.sortedButtonTapped(controller: owner) - }) - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } - - if let cell = cell as? HomeCardSectionCell { - cell.bookmarkButton.rx.tap - .map { Reactor.Action.bookmarkButtonTapped(indexPath: indexPath)} - .bind(to: reactor.action) - .disposed(by: cell.disposeBag) - } - return cell - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - mainView.endEditing(true) - let contentHeight = scrollView.contentSize.height - let scrollViewHeight = scrollView.frame.size.height - let contentOffsetY = scrollView.contentOffset.y - if contentOffsetY + scrollViewHeight >= contentHeight { - pageChange.onNext(()) - } - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - cellTapped.onNext(indexPath) - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift deleted file mode 100644 index 396090d3..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift +++ /dev/null @@ -1,347 +0,0 @@ -// -// SearchReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - -import UIKit - -import ReactorKit -import RxSwift -import RxCocoa - -final class SearchReactor: Reactor { - - // MARK: - Reactor - enum Action { - case viewWillAppear - case returnSearchKeyword(text: String?) - case recentSearchListDeleteButtonTapped(indexPath: IndexPath) - case recentSearchListAllDeleteButtonTapped - case cellTapped(indexPath: IndexPath, controller: BaseViewController) - case sortedButtonTapped(controller: BaseViewController) - case changeSortedFilterIndex(filterIndex: Int, sortedIndex: Int) - case changeCategory(categoryList: [Int64], categoryTitleList: [String?]) - case categoryDelteButtonTapped(indexPath: IndexPath) - case resetCategory - case changePage - case bookmarkButtonTapped(indexPath: IndexPath) - case resetSearchKeyWord - } - - enum Mutation { - case loadView - case moveToCategoryScene(controller: BaseViewController) - case moveToSortedScene(controller: BaseViewController) - case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) - case setSearchKeyWord(text: String?) - case resetSearchKeyWord - } - - struct State { - var sections: [any Sectionable] = [] - var searchKeyWord: String? - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - - private var sortedIndex: Int = 1 - private var filterIndex: Int = 0 - - private var currentPage: Int32 = 0 - private var lastAppendPage: Int32 = 0 - private var lastPage: Int32 = 0 - private var isLoading: Bool = false - - let userDefaultService = UserDefaultService() - private let popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - - lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - UICollectionViewCompositionalLayout { [weak self] section, env in - guard let self = self else { - return NSCollectionLayoutSection(group: NSCollectionLayoutGroup( - layoutSize: .init( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - )) - ) - } - return getSection()[section].getSection(section: section, env: env) - } - }() - - private let recentKeywordTitleSection = SearchTitleSection(inputDataList: [.init(title: "최근 검색어", buttonTitle: "모두삭제")]) - private var recentKeywordSection = CancelableTagSection(inputDataList: []) - - private let searchTitleSection = SearchTitleSection(inputDataList: [.init(title: "팝업스토어 찾기")]) - private var searchCategorySection = CancelableTagSection(inputDataList: [ - .init(title: "카테고리", isSelected: false, isCancelAble: false) - ]) - private var searchListSection = HomeCardGridSection(inputDataList: []) - private var searchSortedSection = SearchCountTitleSection(inputDataList: []) - private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) - private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - private let spacing18Section = SpacingSection(inputDataList: [.init(spacing: 18)]) - private let spacing48Section = SpacingSection(inputDataList: [.init(spacing: 48)]) - private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - - // MARK: - init - init() { - self.initialState = State() - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - let sort = sortedIndex == 0 ? "NEWEST" : "MOST_VIEWED,MOST_COMMENTED,MOST_BOOKMARKED" - switch action { - case .resetSearchKeyWord: - return Observable.just(.resetSearchKeyWord) - case .changePage: - if isLoading { - return Observable.just(.loadView) - } else { - if currentPage < lastPage { - isLoading = true - currentPage += 1 - return setBottomSearchList(sort: sort) - } else { - return Observable.just(.loadView) - } - } - case .viewWillAppear: - setSearchList() - return setBottomSearchList(sort: sort) - case .returnSearchKeyword(let text): - appendSearchList(text: text) - return Observable.just(.loadView) - case .recentSearchListDeleteButtonTapped(let indexPath): - removeSearchList(indexPath: indexPath) - return Observable.just(.loadView) - case .recentSearchListAllDeleteButtonTapped: - resetSearchList() - return Observable.just(.loadView) - case .cellTapped(let indexPath, let controller): - let searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - let section = searchList.isEmpty ? indexPath.section + 4 : indexPath.section - switch section { - case 3: - let text = recentKeywordSection.inputDataList[indexPath.row].title - appendSearchList(text: text) - return Observable.just(.setSearchKeyWord(text: text)) - case 7: - return Observable.just(.moveToCategoryScene(controller: controller)) - case 11: - return Observable.just(.moveToDetailScene(controller: controller, indexPath: indexPath)) - default: - return Observable.just(.loadView) - } - case .sortedButtonTapped(let controller): - return Observable.just(.moveToSortedScene(controller: controller)) - case .changeSortedFilterIndex(let filterIndex, let sortedIndex): - self.sortedIndex = sortedIndex - self.filterIndex = filterIndex - self.currentPage = 0 - self.lastAppendPage = 0 - return Observable.just(.loadView) - case .changeCategory(let categoryList, let categoryTitleList): - self.currentPage = 0 - self.lastAppendPage = 0 - let datas = zip(categoryList, categoryTitleList) - searchCategorySection.inputDataList = datas.map { return .init(title: $0.1, id: $0.0, isSelected: true, isCancelAble: true)} - return Observable.just(.loadView) - case .resetCategory: - self.currentPage = 0 - self.lastAppendPage = 0 - searchCategorySection.inputDataList = [.init(title: "카테고리", isSelected: false, isCancelAble: false)] - return Observable.just(.loadView) - case .categoryDelteButtonTapped(let indexPath): - self.currentPage = 0 - self.lastAppendPage = 0 - searchCategorySection.inputDataList.remove(at: indexPath.row) - if searchCategorySection.inputDataList.isEmpty { searchCategorySection.inputDataList = [.init(title: "카테고리", isSelected: false, isCancelAble: false)] } - return setBottomSearchList(sort: sort) - case .bookmarkButtonTapped(let indexPath): - let data = searchListSection.inputDataList[indexPath.row] - let isBookmark = data.isBookmark - let id = data.id - searchListSection.inputDataList[indexPath.row].isBookmark.toggle() - if isBookmark { - return userAPIUseCase.deleteBookmarkPopUp(popUpID: id) - .andThen(Observable.just(.loadView)) - } else { - return userAPIUseCase.postBookmarkPopUp(popUpID: id) - .andThen(Observable.just(.loadView)) - } - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .loadView: - newState.sections = getSection() - case .moveToCategoryScene(let controller): - let categoryIDList = searchCategorySection.inputDataList.compactMap { $0.id } - let nextController = SearchCategoryController() - nextController.reactor = SearchCategoryReactor(originCategoryList: categoryIDList) - controller.presentPanModal(nextController) - nextController.reactor?.state - .withUnretained(self) - .subscribe(onNext: { (owner, state) in - if state.isSave { - if state.categoryTitleList.isEmpty { - owner.searchCategorySection.inputDataList = [.init(title: "카테고리", isSelected: false, isCancelAble: false)] - } else { - owner.action.onNext(.changeCategory(categoryList: state.categoryIDList, categoryTitleList: state.categoryTitleList)) - } - - } - if state.isReset { owner.action.onNext(.resetCategory)} - }) - .disposed(by: nextController.disposeBag) - case .moveToSortedScene(let controller): - let nextController = SearchSortedController() - nextController.reactor = SearchSortedReactor(filterIndex: filterIndex, sortedIndex: sortedIndex) - controller.presentPanModal(nextController) - nextController.reactor?.state - .withUnretained(self) - .subscribe(onNext: { (owner, state) in - if state.isSave { - ToastMaker.createToast(message: "선택하신 옵션을 저장했어요") - owner.action.onNext(.changeSortedFilterIndex(filterIndex: state.filterIndex, sortedIndex: state.sortedIndex)) - } - }) - .disposed(by: nextController.disposeBag) - case .moveToDetailScene(let controller, let indexPath): - let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: searchListSection.inputDataList[indexPath.row].id) - controller.navigationController?.pushViewController(nextController, animated: true) - case .setSearchKeyWord(let text): - newState.searchKeyWord = text - case .resetSearchKeyWord: - newState.searchKeyWord = nil - newState.sections = getSection() - } - return newState - } - - func getSection() -> [any Sectionable] { - let searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - if searchList.isEmpty { - return [ - spacing24Section, - searchTitleSection, - spacing16Section, - searchCategorySection, - spacing18Section, - searchSortedSection, - spacing16Section, - searchListSection, - spacing64Section - ] - } else { - return [ - spacing24Section, - recentKeywordTitleSection, - spacing16Section, - recentKeywordSection, - spacing48Section, - searchTitleSection, - spacing16Section, - searchCategorySection, - spacing18Section, - searchSortedSection, - spacing16Section, - searchListSection, - spacing64Section - ] - } - } - - func setSearchList() { - let searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - recentKeywordSection.inputDataList = searchList.map { return .init(title: $0) } - } - - func appendSearchList(text: String?) { - if let text = text { - if !text.isEmpty { - var searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - if searchList.contains(text) { - let targetIndex = searchList.firstIndex(of: text)! - searchList.remove(at: targetIndex) - } - searchList = [text] + searchList - userDefaultService.save(key: "searchList", value: searchList) - recentKeywordSection.inputDataList = searchList.map { return .init(title: $0) } - } - } - } - - func removeSearchList(indexPath: IndexPath) { - var searchList = userDefaultService.fetchArray(key: "searchList") ?? [] - searchList.remove(at: indexPath.row) - userDefaultService.save(key: "searchList", value: searchList) - recentKeywordSection.inputDataList = searchList.map { return .init(title: $0) } - } - - func resetSearchList() { - userDefaultService.save(key: "searchList", value: []) - recentKeywordSection.inputDataList = [] - } - - func setBottomSearchList(sort: String?) -> Observable { - let isOpen = filterIndex == 0 ? true : false - let categorys = searchCategorySection.inputDataList.compactMap { $0.id } - return popUpAPIUseCase.getSearchBottomPopUpList(isOpen: isOpen, categories: categorys, page: currentPage, size: 50, sort: sort) - .withUnretained(self) - .map { (owner, response) in - let isLogin = response.loginYn - if owner.currentPage == 0 { - owner.searchListSection.inputDataList = response.popUpStoreList.map { - return .init( - imagePath: $0.mainImageUrl, - id: $0.id, - category: $0.category, - title: $0.name, - address: $0.address, - startDate: $0.startDate, - endDate: $0.endDate, - isBookmark: $0.bookmarkYn, - isLogin: isLogin - ) - } - } else { - if owner.currentPage != owner.lastAppendPage { - owner.lastAppendPage = owner.currentPage - let newData = response.popUpStoreList.map { - return HomeCardSectionCell.Input( - imagePath: $0.mainImageUrl, - id: $0.id, - category: $0.category, - title: $0.name, - address: $0.address, - startDate: $0.startDate, - endDate: $0.endDate, - isBookmark: $0.bookmarkYn, - isLogin: isLogin - ) - } - owner.searchListSection.inputDataList.append(contentsOf: newData) - } - } - let isOpenString = isOpen ? "오픈・" : "종료・" - let sortedString = owner.sortedIndex == 0 ? "신규순" : "인기순" - let sortedTitle = isOpenString + sortedString - owner.searchSortedSection.inputDataList = [.init(count: response.totalElements, sortedTitle: sortedTitle)] - owner.lastPage = response.totalPages - owner.isLoading = false - return .loadView - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift deleted file mode 100644 index fb80227f..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// CancelableTagSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - -import UIKit - -import RxSwift - -struct CancelableTagSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = CancelableTagSectionCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .estimated(100), - heightDimension: .absolute(31) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .estimated(100), - heightDimension: .absolute(31) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 0) - section.interGroupSpacing = 6 - section.orthogonalScrollingBehavior = .continuous - return section - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift deleted file mode 100644 index 7a3afb15..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// CancelableTagSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class CancelableTagSectionCell: UICollectionViewCell { - - // MARK: - Components - - var disposeBag = DisposeBag() - - private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 11) - return label - }() - - let cancelButton: UIButton = { - let button = UIButton() - return button - }() - - private let contentStackView: UIStackView = { - let view = UIStackView() - view.alignment = .center - view.spacing = 2 - return view - }() - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func prepareForReuse() { - super.prepareForReuse() - disposeBag = DisposeBag() - } -} - -// MARK: - SetUp -private extension CancelableTagSectionCell { - func setUpConstraints() { - contentView.layer.cornerRadius = 15.5 - contentView.clipsToBounds = true - contentView.layer.borderWidth = 1 - - contentView.addSubview(contentStackView) - contentStackView.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.leading.equalToSuperview().inset(12) - make.trailing.equalToSuperview().inset(8) - } - contentStackView.addArrangedSubview(titleLabel) - contentStackView.addArrangedSubview(cancelButton) - - titleLabel.snp.makeConstraints { make in - make.height.equalTo(18) - } - cancelButton.snp.makeConstraints { make in - make.size.equalTo(16) - } - } -} - -extension CancelableTagSectionCell: Inputable { - struct Input { - var title: String? - var id: Int64? = nil - var isSelected: Bool = false - var isCancelAble: Bool = true - } - - func injection(with input: Input) { - let xmarkImage = input.isSelected ? UIImage(named: "icon_xmark_white") : UIImage(named: "icon_xmark_gray") - cancelButton.setImage(xmarkImage, for: .normal) - if input.isSelected { - contentView.backgroundColor = .blu500 - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 11), lineHeight: 1.15) - titleLabel.textColor = .w100 - contentView.layer.borderColor = UIColor.blu500.cgColor - } else { - contentView.backgroundColor = .clear - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 11), lineHeight: 1.15) - titleLabel.textColor = .g400 - contentView.layer.borderColor = UIColor.g200.cgColor - } - cancelButton.isHidden = !input.isCancelAble - - if input.isCancelAble { - contentStackView.snp.updateConstraints { make in - make.trailing.equalToSuperview().inset(8) - } - } else { - contentStackView.snp.updateConstraints { make in - make.trailing.equalToSuperview().inset(12) - } - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift deleted file mode 100644 index 6feaa53b..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SearchCountTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/5/24. -// - -import UIKit - -import RxSwift - -struct SearchCountTitleSection: Sectionable { - - var currentPage: PublishSubject = .init() - - typealias CellType = SearchCountTitleSectionCell - - var inputDataList: [CellType.Input] - - var supplementaryItems: [any SectionSupplementaryItemable]? - - var decorationItems: [any SectionDecorationItemable]? - - func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(22) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) -// item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .absolute(22) - ) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - - return section - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift deleted file mode 100644 index 7ae94994..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// SearchCountTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/5/24. -// - -import UIKit - -import SnapKit -import RxSwift - -final class SearchCountTitleSectionCell: UICollectionViewCell { - - // MARK: - Components - - var disposeBag = DisposeBag() - - private let countLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13) - label.textColor = .g400 - return label - }() - - private let sortedTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13) - return label - }() - - private let downImageView: UIImageView = { - let view = UIImageView() - view.image = UIImage(named: "icon_dropdown") - view.isUserInteractionEnabled = false - return view - }() - - let sortedButton: UIButton = { - let button = UIButton() - return button - }() - - // MARK: - init - - override init(frame: CGRect) { - super.init(frame: frame) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func prepareForReuse() { - super.prepareForReuse() - disposeBag = DisposeBag() - } -} - -// MARK: - SetUp -private extension SearchCountTitleSectionCell { - func setUpConstraints() { - contentView.addSubview(countLabel) - countLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - - contentView.addSubview(sortedButton) - sortedButton.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - - sortedButton.addSubview(sortedTitleLabel) - sortedTitleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - - sortedButton.addSubview(downImageView) - downImageView.snp.makeConstraints { make in - make.size.equalTo(22) - make.centerY.equalToSuperview() - make.leading.equalTo(sortedTitleLabel.snp.trailing).offset(6) - make.trailing.equalToSuperview() - } - } -} - -extension SearchCountTitleSectionCell: Inputable { - struct Input { - var count: Int64 - var sortedTitle: String? - } - - func injection(with input: Input) { - sortedTitleLabel.text = input.sortedTitle - countLabel.text = "총 \(input.count)개" - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift deleted file mode 100644 index f7925427..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SearchView.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - -import UIKit - -import SnapKit - -final class SearchView: UIView { - - // MARK: - Components - let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension SearchView { - - func setUpConstraints() { - self.addSubview(contentCollectionView) - contentCollectionView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(56) - make.leading.trailing.bottom.equalToSuperview() - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift deleted file mode 100644 index da326158..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// SearchCategoryController.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit -import PanModal - -final class SearchCategoryController: BaseViewController, View { - - typealias Reactor = SearchCategoryReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = SearchCategoryView() - private var sections: [any Sectionable] = [] - private let cellTapped: PublishSubject = .init() -} - -// MARK: - Life Cycle -extension SearchCategoryController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } -} - -// MARK: - SetUp -private extension SearchCategoryController { - func setUp() { - if let layout = reactor?.compositionalLayout { - mainView.contentCollectionView.collectionViewLayout = layout - } - mainView.contentCollectionView.delegate = self - mainView.contentCollectionView.dataSource = self - mainView.contentCollectionView.register( - TagSectionCell.self, - forCellWithReuseIdentifier: TagSectionCell.identifiers - ) - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension SearchCategoryController { - func bind(reactor: Reactor) { - rx.viewWillAppear - .map { Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - cellTapped - .map { Reactor.Action.cellTapped(indexPath: $0)} - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.resetButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - Reactor.Action.resetButtonTapped(controller: owner) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.closeButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - Reactor.Action.closeButtonTapped(controller: owner) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.saveButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - Reactor.Action.saveButtonTapped(controller: owner) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - owner.sections = state.sections - owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable - owner.mainView.contentCollectionView.reloadData() - } - .disposed(by: disposeBag) - } -} - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension SearchCategoryController: UICollectionViewDelegate, UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return sections[section].dataCount - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - guard let reactor = reactor else { return cell } - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - cellTapped.onNext(indexPath) - } -} -// MARK: - PanModalPresentable -extension SearchCategoryController: PanModalPresentable { - var panScrollable: UIScrollView? { - return nil - } - var longFormHeight: PanModalHeight { - return .intrinsicHeight - } - var shortFormHeight: PanModalHeight { - return .intrinsicHeight - } - var showDragIndicator: Bool { - return false - } - var cornerRadius: CGFloat { - return 20 - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift deleted file mode 100644 index 8f5e4461..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// SearchCategoryReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import UIKit - -import ReactorKit -import RxSwift -import RxCocoa - -final class SearchCategoryReactor: Reactor { - - // MARK: - Reactor - enum Action { - case viewWillAppear - case closeButtonTapped(controller: BaseViewController) - case saveButtonTapped(controller: BaseViewController) - case resetButtonTapped(controller: BaseViewController) - case cellTapped(indexPath: IndexPath) - } - - enum Mutation { - case moveToRecentScene(controller: BaseViewController) - case loadView - case save(controller: BaseViewController) - case reset(controller: BaseViewController) - } - - struct State { - var sections: [any Sectionable] = [] - var categoryIDList: [Int64] = [] - var categoryTitleList: [String?] = [] - var saveButtonIsEnable: Bool = false - var isSave: Bool = false - var isReset: Bool = false - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - var originCategoryList: [Int64] - private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - private var tagSection = TagSection(inputDataList: []) - lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - UICollectionViewCompositionalLayout { [weak self] section, env in - guard let self = self else { - return NSCollectionLayoutSection(group: NSCollectionLayoutGroup( - layoutSize: .init( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - )) - ) - } - return getSection()[section].getSection(section: section, env: env) - } - }() - - // MARK: - init - init(originCategoryList: [Int64]) { - self.initialState = State() - self.originCategoryList = originCategoryList - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - case .viewWillAppear: - return signUpAPIUseCase.fetchCategoryList() - .withUnretained(self) - .map { (owner, response) in - owner.tagSection.inputDataList = response.map { - let isSelected = owner.originCategoryList.contains($0.categoryId) - return .init(title: $0.category, isSelected: isSelected, id: $0.categoryId) - } - return .loadView - } - case .closeButtonTapped(let controller): - return Observable.just(.moveToRecentScene(controller: controller)) - case .saveButtonTapped(let controller): - return Observable.just(.save(controller: controller)) - case .cellTapped(let indexPath): - tagSection.inputDataList[indexPath.row].isSelected.toggle() - return Observable.just(.loadView) - case .resetButtonTapped(let controller): - return Observable.just(.reset(controller: controller)) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .moveToRecentScene(let controller): - controller.dismiss(animated: true) - case .loadView: - newState.sections = getSection() - let selectedList = tagSection.inputDataList.filter { $0.isSelected }.compactMap { $0.id }.sorted(by: <) - let originList = originCategoryList.sorted(by: <) - newState.saveButtonIsEnable = selectedList != originList - newState.categoryIDList = selectedList - newState.categoryTitleList = tagSection.inputDataList.filter { $0.isSelected }.map { $0.title } - case .save(let controller): - newState.isSave = true - controller.dismiss(animated: true) - case .reset(let controller): - newState.isReset = true - controller.dismiss(animated: true) - } - return newState - } - - func getSection() -> [any Sectionable] { - return [ - tagSection - ] - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift deleted file mode 100644 index 42ebb089..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// SearchCategoryView.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import UIKit - -import SnapKit - -final class SearchCategoryView: UIView { - - // MARK: - Components - private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "카테고리를 선택해주세요") - return label - }() - - let closeButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "icon_xmark"), for: .normal) - return button - }() - - let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - view.isScrollEnabled = false - return view - }() - - let buttonStackView: UIStackView = { - let view = UIStackView() - view.distribution = .fillEqually - view.spacing = 12 - return view - }() - - let resetButton: PPButton = { - let button = PPButton(style: .secondary, text: "초기화") - return button - }() - - let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장") - return button - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension SearchCategoryView { - - func setUpConstraints() { - self.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(20) - make.top.equalToSuperview().inset(12) - } - - self.addSubview(closeButton) - closeButton.snp.makeConstraints { make in - make.size.equalTo(24) - make.trailing.equalToSuperview().inset(20) - make.centerY.equalTo(titleLabel) - } - self.addSubview(contentCollectionView) - contentCollectionView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(10) - make.height.equalTo(195) - } - self.addSubview(buttonStackView) - buttonStackView.addArrangedSubview(resetButton) - buttonStackView.addArrangedSubview(saveButton) - buttonStackView.snp.makeConstraints { make in - make.top.equalTo(contentCollectionView.snp.bottom).offset(32) - make.leading.trailing.equalToSuperview().inset(20) - make.height.equalTo(50) - make.bottom.equalToSuperview() - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift deleted file mode 100644 index 9a16aa29..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// SearchMainController.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit -import Pageboy -import Tabman - -final class SearchMainController: BaseTabmanController, View { - - typealias Reactor = SearchMainReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = SearchMainView() - - var beforeController: SearchController = { - let controller = SearchController() - controller.reactor = SearchReactor() - return controller - }() - - var afterController: SearchResultController = { - let controller = SearchResultController() - controller.reactor = SearchResultReactor() - return controller - }() - - lazy var controllers = [ - beforeController, - afterController - ] - - var isResponseTextField: Bool = false -} - -// MARK: - Life Cycle -extension SearchMainController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if !isResponseTextField { - mainView.searchTextField.becomeFirstResponder() - isResponseTextField = true - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tabBarController?.tabBar.isHidden = true - } -} - -// MARK: - SetUp -private extension SearchMainController { - func setUp() { - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) - make.height.equalTo(56) - } - self.dataSource = self - self.isScrollEnabled = false - } -} - -// MARK: - Methods -extension SearchMainController { - func bind(reactor: Reactor) { - beforeController.reactor?.state - .withUnretained(self) - .subscribe(onNext: { (owner, state) in - owner.view.endEditing(true) - if let text = state.searchKeyWord { - if let index = owner.currentIndex { - if index == 0 { - owner.scrollToPage(.at(index: 1), animated: false) - reactor.action.onNext(.returnSearchKeyWord(text: text)) - owner.mainView.searchTextField.text = state.searchKeyWord - } - } - } - }) - .disposed(by: disposeBag) - -// mainView.searchTextField.rx.controlEvent(.editingDidEndOnExit) -// .withUnretained(self) -// .map { (owner, _) in -// Reactor.Action.returnSearchKeyWord(text: owner.mainView.searchTextField.text ) -// } -// .bind(to: reactor.action) -// .disposed(by: disposeBag) -// -// mainView.searchTextField.rx.controlEvent(.editingDidEndOnExit) -// .withUnretained(self) -// .subscribe(onNext: { (owner, _) in -// if let text = owner.mainView.searchTextField.text { -// if !text.isEmpty { -// owner.scrollToPage(.at(index: 1), animated: false) -// } -// } -// owner.beforeController.reactor?.action.onNext(.returnSearchKeyword(text: owner.mainView.searchTextField.text)) -// }) -// .disposed(by: disposeBag) - mainView.searchTextField.rx.controlEvent(.editingDidEndOnExit) - .withLatestFrom(mainView.searchTextField.rx.text.orEmpty) - .withUnretained(self) - .subscribe(onNext: { (owner, query) in - owner.view.endEditing(true) - // 텍스트가 비어있지 않으면 페이지 전환 - if !query.isEmpty { - owner.scrollToPage(.at(index: 1), animated: false) - } - owner.reactor?.action.onNext(.returnSearchKeyWord(text: query)) - owner.beforeController.reactor?.action.onNext(.returnSearchKeyword(text: query)) - }) - .disposed(by: disposeBag) - - - mainView.searchTextField.rx.text - .withUnretained(self) - .subscribe(onNext: { (owner, text) in - if let text = text { - owner.mainView.clearButton.isHidden = text.isEmpty - } else { - owner.mainView.clearButton.isHidden = true - } - }) - .disposed(by: disposeBag) - - mainView.cancelButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - owner.view.endEditing(true) - if owner.currentIndex == 1 { - owner.mainView.searchTextField.text = nil - owner.beforeController.reactor?.action.onNext(.resetSearchKeyWord) - owner.scrollToPage(.at(index: 0), animated: false) - } else { - owner.navigationController?.popViewController(animated: true) - } - return Reactor.Action.returnSearchKeyWord(text: nil) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.clearButton.rx.tap - .withUnretained(self) - .subscribe { (owner, _) in - owner.mainView.searchTextField.text = nil - owner.mainView.clearButton.isHidden = true - } - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - if let text = state.searchKeyword { - owner.afterController.reactor?.action.onNext(.returnSearch(text: text)) - } - } - .disposed(by: disposeBag) - } -} - -extension SearchMainController: PageboyViewControllerDataSource, TMBarDataSource { - func barItem(for bar: any Tabman.TMBar, at index: Int) -> any Tabman.TMBarItemable { - return TMBarItem(title: "") - } - - func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { - return controllers.count - } - - func viewController( - for pageboyViewController: Pageboy.PageboyViewController, - at index: Pageboy.PageboyViewController.PageIndex - ) -> UIViewController? { - return controllers[index] - } - - func defaultPage( - for pageboyViewController: Pageboy.PageboyViewController - ) -> Pageboy.PageboyViewController.Page? { - if let currentIndex = currentIndex { return .at(index: currentIndex) } - return .at(index: 0) - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift deleted file mode 100644 index 29446caa..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SearchMainReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import ReactorKit -import RxSwift -import RxCocoa - -final class SearchMainReactor: Reactor { - - // MARK: - Reactor - enum Action { - case returnSearchKeyWord(text: String?) - } - - enum Mutation { - case setSearchKeyWord(text: String?) - } - - struct State { - var searchKeyword: String? - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - // MARK: - init - init() { - self.initialState = State() - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - case .returnSearchKeyWord(let text): - return Observable.just(.setSearchKeyWord(text: text)) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .setSearchKeyWord(let text): - newState.searchKeyword = text - } - return newState - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift deleted file mode 100644 index d04eb79d..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// SearchMainView.swift -// Poppool -// -// Created by SeoJunYoung on 12/7/24. -// - -import UIKit - -import SnapKit - -final class SearchMainView: UIView { - - // MARK: - Components - private let searchTrailingView: UIView = { - let view = UIView() - view.backgroundColor = .g50 - view.layer.cornerRadius = 4 - view.clipsToBounds = true - return view - }() - - private let searchIconImageView: UIImageView = { - let view = UIImageView() - view.image = UIImage(named: "icon_search_gray") - return view - }() - - private let searchStackView: UIStackView = { - let view = UIStackView() - view.spacing = 4 - view.alignment = .center - return view - }() - - let cancelButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("취소", for: .normal) - button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 14) - button.imageView?.contentMode = .scaleAspectFit - return button - }() - - let searchTextField: UITextField = { - let view = UITextField() - view.font = .KorFont(style: .regular, size: 14) - view.setPlaceholder(text: "팝업스토어명을 입력해보세요", color: .g400, font: .KorFont(style: .regular, size: 14)!) - return view - }() - - let clearButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "icon_clearButton"), for: .normal) - return button - }() - - private var headerStackView: UIStackView = { - let view = UIStackView() - view.alignment = .center - view.spacing = 16 - return view - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension SearchMainView { - - func setUpConstraints() { - searchTrailingView.snp.makeConstraints { make in - make.height.equalTo(37) - } - headerStackView.addArrangedSubview(searchTrailingView) - headerStackView.addArrangedSubview(cancelButton) - self.addSubview(headerStackView) - headerStackView.snp.makeConstraints { make in - make.top.equalToSuperview().inset(7) - make.leading.equalToSuperview().inset(20) - make.trailing.equalToSuperview().inset(16) - } - searchTrailingView.addSubview(searchStackView) - searchStackView.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.leading.trailing.equalToSuperview().inset(12) - } - searchStackView.addArrangedSubview(searchIconImageView) - searchStackView.addArrangedSubview(searchTextField) - searchStackView.addArrangedSubview(clearButton) - - searchIconImageView.snp.makeConstraints { make in - make.size.equalTo(20) - } - - searchTextField.snp.makeConstraints { make in - make.height.equalTo(21) - } - - clearButton.snp.makeConstraints { make in - make.size.equalTo(16) - } - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift deleted file mode 100644 index 12c816d6..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// SearchSortedController.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import UIKit - -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit -import PanModal - -final class SearchSortedController: BaseViewController, View { - - typealias Reactor = SearchSortedReactor - - // MARK: - Properties - var disposeBag = DisposeBag() - - private var mainView = SearchSortedView() -} - -// MARK: - Life Cycle -extension SearchSortedController { - override func viewDidLoad() { - super.viewDidLoad() - setUp() - } -} - -// MARK: - SetUp -private extension SearchSortedController { - func setUp() { - view.addSubview(mainView) - mainView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) - } - } -} - -// MARK: - Methods -extension SearchSortedController { - func bind(reactor: Reactor) { - mainView.closeButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - Reactor.Action.closeButtonTapped(controller: owner) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.filterSegmentControl.rx.controlEvent(.valueChanged) - .withUnretained(self) - .map({ (owner, _) in - Reactor.Action.changeFilterIndex(index: owner.mainView.filterSegmentControl.selectedSegmentIndex) - }) - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.sortedSegmentControl.rx.controlEvent(.valueChanged) - .withUnretained(self) - .map({ (owner, _) in - Reactor.Action.changeSortedIndex(index: owner.mainView.sortedSegmentControl.selectedSegmentIndex) - }) - .bind(to: reactor.action) - .disposed(by: disposeBag) - - mainView.saveButton.rx.tap - .withUnretained(self) - .map { (owner, _) in - Reactor.Action.saveButtonTapped(controller: owner) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .withUnretained(self) - .subscribe { (owner, state) in - owner.mainView.filterSegmentControl.selectedSegmentIndex = state.filterIndex - owner.mainView.sortedSegmentControl.selectedSegmentIndex = state.sortedIndex - owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable - } - .disposed(by: disposeBag) - } -} - -// MARK: - PanModalPresentable -extension SearchSortedController: PanModalPresentable { - var panScrollable: UIScrollView? { - return nil - } - var longFormHeight: PanModalHeight { - return .intrinsicHeight - } - var shortFormHeight: PanModalHeight { - return .intrinsicHeight - } - var showDragIndicator: Bool { - return false - } - var cornerRadius: CGFloat { - return 20 - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift deleted file mode 100644 index 747b9bfa..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// SearchSortedReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import ReactorKit -import RxSwift -import RxCocoa - -final class SearchSortedReactor: Reactor { - - // MARK: - Reactor - enum Action { - case closeButtonTapped(controller: BaseViewController) - case changeFilterIndex(index: Int) - case changeSortedIndex(index: Int) - case saveButtonTapped(controller: BaseViewController) - } - - enum Mutation { - case moveToRecentScene(controller: BaseViewController) - case loadView - case save(controller: BaseViewController) - } - - struct State { - var filterIndex: Int - var sortedIndex: Int - var saveButtonIsEnable: Bool = false - var isSave: Bool = false - } - - // MARK: - properties - - var initialState: State - var disposeBag = DisposeBag() - var originFilterIndex: Int - var originSortedIndex: Int - private var selectedFilterIndex: Int - private var selectedSortedIndex: Int - - // MARK: - init - init(filterIndex: Int, sortedIndex: Int) { - self.initialState = State(filterIndex: filterIndex, sortedIndex: sortedIndex) - self.originFilterIndex = filterIndex - self.originSortedIndex = sortedIndex - self.selectedFilterIndex = filterIndex - self.selectedSortedIndex = sortedIndex - } - - // MARK: - Reactor Methods - func mutate(action: Action) -> Observable { - switch action { - case .closeButtonTapped(let controller): - return Observable.just(.moveToRecentScene(controller: controller)) - case .changeFilterIndex(let index): - selectedFilterIndex = index - return Observable.just(.loadView) - case .changeSortedIndex(let index): - selectedSortedIndex = index - return Observable.just(.loadView) - case .saveButtonTapped(let controller): - return Observable.just(.save(controller: controller)) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .moveToRecentScene(let controller): - controller.dismiss(animated: true) - case .loadView: - newState.filterIndex = selectedFilterIndex - newState.sortedIndex = selectedSortedIndex - - if selectedFilterIndex != originFilterIndex || selectedSortedIndex != originSortedIndex { - newState.saveButtonIsEnable = true - } else { - newState.saveButtonIsEnable = false - } - case .save(let controller): - newState.isSave = true - controller.dismiss(animated: true) - } - return newState - } -} diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift deleted file mode 100644 index d528513c..00000000 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// SearchSortedView.swift -// Poppool -// -// Created by SeoJunYoung on 12/6/24. -// - -import UIKit - -import SnapKit - -final class SearchSortedView: UIView { - - // MARK: - Components - private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "노출 순서를 선택해주세요") - return label - }() - - let closeButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "icon_xmark"), for: .normal) - return button - }() - - private let filterTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "노출 조건") - return label - }() - - let filterSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["오픈", "종료"], selectedSegmentIndex: 0) - return control - }() - - private let sortedTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "팝업순서") - return label - }() - - let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["신규순", "인기순"], selectedSegmentIndex: 0) - return control - }() - - let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button - }() - - // MARK: - init - init() { - super.init(frame: .zero) - setUpConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension SearchSortedView { - - func setUpConstraints() { - self.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(20) - make.top.equalToSuperview().inset(32) - } - - self.addSubview(closeButton) - closeButton.snp.makeConstraints { make in - make.size.equalTo(24) - make.trailing.equalToSuperview().inset(20) - make.centerY.equalTo(titleLabel) - } - - self.addSubview(filterTitleLabel) - filterTitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(36) - make.leading.equalToSuperview().inset(20) - } - - self.addSubview(filterSegmentControl) - filterSegmentControl.snp.makeConstraints { make in - make.top.equalTo(filterTitleLabel.snp.bottom).offset(8) - make.leading.trailing.equalToSuperview().inset(20) - } - - self.addSubview(sortedTitleLabel) - sortedTitleLabel.snp.makeConstraints { make in - make.top.equalTo(filterSegmentControl.snp.bottom).offset(20) - make.leading.equalToSuperview().inset(20) - } - - self.addSubview(sortedSegmentControl) - sortedSegmentControl.snp.makeConstraints { make in - make.top.equalTo(sortedTitleLabel.snp.bottom).offset(8) - make.leading.trailing.equalToSuperview().inset(20) - } - - self.addSubview(saveButton) - saveButton.snp.makeConstraints { make in - make.top.equalTo(sortedSegmentControl.snp.bottom).offset(32) - make.leading.trailing.equalToSuperview().inset(20) - make.bottom.equalToSuperview() - } - } -} diff --git a/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift b/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift deleted file mode 100644 index 330cad1e..00000000 --- a/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// BaseTabmanController.swift -// MomsVillage -// -// Created by SeoJunYoung on 9/25/24. -// - -import UIKit - -import Tabman -import Pageboy - -class BaseTabmanController: TabmanViewController { - init() { - super.init(nibName: nil, bundle: nil) - Logger.log( - message: "\(self) init", - category: .info, - fileName: #file, - line: #line - ) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - self.view.backgroundColor = .systemBackground - self.navigationController?.navigationBar.isHidden = true - } - - deinit { - Logger.log( - message: "\(self) deinit", - category: .info, - fileName: #file, - line: #line - ) - } -} diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/InOutputable.swift b/Poppool/Poppool/Presentation/Utills/Interfaces/InOutputable.swift deleted file mode 100644 index 4bfbc829..00000000 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/InOutputable.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// InOutputable.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/29/24. -// - -import UIKit - -protocol InOutputable: Inputable, Outputable { } - -protocol Inputable { - associatedtype Input - func injection(with input: Input) -} - -protocol Outputable { - associatedtype Output -} diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/Contents.json index c203dc7e..f64986bb 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon_bookmark_fill.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "solid.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/icon_bookmark_fill.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/icon_bookmark_fill.png deleted file mode 100644 index d52d7e28..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/icon_bookmark_fill.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/solid.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/solid.svg new file mode 100644 index 00000000..1ef76aed --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark.imageset/solid.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/Contents.json index fb8ebfdd..44808aa2 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "solid.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "solid-1.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid-1.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid-1.svg new file mode 100644 index 00000000..6d836093 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid-1.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid.png deleted file mode 100644 index 0dfdebb1..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_bookmark/icon_bookmark_fill.imageset/solid.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/Contents.json deleted file mode 100644 index d67ddcf1..00000000 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon_clearButton.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/icon_clearButton.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/icon_clearButton.png deleted file mode 100644 index b8a1cd0d..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clearButton.imageset/icon_clearButton.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/Contents.json new file mode 100644 index 00000000..f64986bb --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "solid.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/solid.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/solid.svg new file mode 100644 index 00000000..71a10b2e --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_clear_button.imageset/solid.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/Contents.json index fb8ebfdd..f64986bb 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "solid.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "solid.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.png deleted file mode 100644 index 17de8bbf..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.svg new file mode 100644 index 00000000..ec4c794b --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_dropdown.imageset/solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/Contents.json index 25625b1d..d86eb988 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "line.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "line.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.png deleted file mode 100644 index 6cee2193..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.svg new file mode 100644 index 00000000..6dd06be5 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_search/icon_search_gray.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/Contents.json index 0f28cb44..d86eb988 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon_xmark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "line.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/icon_xmark.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/icon_xmark.png deleted file mode 100644 index 0125c188..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/icon_xmark.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/line.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/line.svg new file mode 100644 index 00000000..c95be2c3 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/Contents.json index 3184723c..d86eb988 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon_xmark_gray.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "line.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/icon_xmark_gray.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/icon_xmark_gray.png deleted file mode 100644 index 37daacff..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/icon_xmark_gray.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/line.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/line.svg new file mode 100644 index 00000000..6a318745 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_gray.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/Contents.json index b3d1a689..d86eb988 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon_xmark_white.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "line.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/icon_xmark_white.png b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/icon_xmark_white.png deleted file mode 100644 index 6de5ee2f..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/icon_xmark_white.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/line.svg b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/line.svg new file mode 100644 index 00000000..b55257a1 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Icon/icon_xmark/icon_xmark_white.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/Contents.json index fb8ebfdd..b1cb08ba 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "solid.png", + "filename" : "solid.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.png b/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.png deleted file mode 100644 index 10ce11cd..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.svg b/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.svg new file mode 100644 index 00000000..1cb7b17e --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Marker/Marker.imageset/solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/Contents.json b/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/Contents.json index fb8ebfdd..b1cb08ba 100644 --- a/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/Contents.json +++ b/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "solid.png", + "filename" : "solid.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.png b/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.png deleted file mode 100644 index 39f13b31..00000000 Binary files a/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.png and /dev/null differ diff --git a/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.svg b/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.svg new file mode 100644 index 00000000..1fef9bf7 --- /dev/null +++ b/Poppool/Poppool/Resource/Assets.xcassets/Marker/TapMarker.imageset/solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/Poppool/Resource/Info.plist b/Poppool/Poppool/Resource/Info.plist index 9b1010ac..3f04e5cc 100644 --- a/Poppool/Poppool/Resource/Info.plist +++ b/Poppool/Poppool/Resource/Info.plist @@ -2,6 +2,10 @@ + ITSAppUsesNonExemptEncryption + + CFBundleShortVersionString + $(MARKETING_VERSION) CFBundleURLTypes @@ -13,8 +17,10 @@ - ITSAppUsesNonExemptEncryption - + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + KAKAO_AUTH_APP_KEY + ${KAKAO_AUTH_APP_KEY} LSApplicationQueriesSchemes instagram @@ -22,18 +28,17 @@ kakaomap tmap maps + kakaokompassauth + kakaotalk - UIAppFonts - - GothicA1-Bold.ttf - GothicA1-Light.ttf - GothicA1-Medium.ttf - GothicA1-Regular.ttf - Poppins-Bold.ttf - Poppins-Light.ttf - Poppins-Medium.ttf - Poppins-Regular.ttf - + NAVER_MAP_CLIENT_ID + ${NAVER_MAP_CLIENT_ID} + POPPOOL_API_KEY + ${POPPOOL_API_KEY} + POPPOOL_BASE_URL + ${POPPOOL_BASE_URL} + POPPOOL_S3_BASE_URL + ${POPPOOL_S3_BASE_URL} UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Poppool/Poppool/Poppool.entitlements b/Poppool/Poppool/Resource/Poppool.entitlements similarity index 100% rename from Poppool/Poppool/Poppool.entitlements rename to Poppool/Poppool/Resource/Poppool.entitlements diff --git a/Poppool/PoppoolTests/PoppoolTests.swift b/Poppool/PoppoolTests/PoppoolTests.swift deleted file mode 100644 index 931a598f..00000000 --- a/Poppool/PoppoolTests/PoppoolTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// PoppoolTests.swift -// PoppoolTests -// -// Created by Porori on 11/24/24. -// - -import XCTest -@testable import Poppool - -final class PoppoolTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Poppool/PoppoolUITests/PoppoolUITests.swift b/Poppool/PoppoolUITests/PoppoolUITests.swift deleted file mode 100644 index cf8347f1..00000000 --- a/Poppool/PoppoolUITests/PoppoolUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PoppoolUITests.swift -// PoppoolUITests -// -// Created by Porori on 11/24/24. -// - -import XCTest - -final class PoppoolUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift b/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift deleted file mode 100644 index e80f88dd..00000000 --- a/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PoppoolUITestsLaunchTests.swift -// PoppoolUITests -// -// Created by Porori on 11/24/24. -// - -import XCTest - -final class PoppoolUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem.xcodeproj/project.pbxproj b/Poppool/PresentationLayer/DesignSystem/DesignSystem.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c6b12b47 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem.xcodeproj/project.pbxproj @@ -0,0 +1,478 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0516312C2DC3D1E900A6C0D1 /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0516312B2DC3D1E900A6C0D1 /* Infrastructure.framework */; }; + 051631602DC3D28400A6C0D1 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 0516315F2DC3D28400A6C0D1 /* RxCocoa */; }; + 051631622DC3D28400A6C0D1 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 051631612DC3D28400A6C0D1 /* RxSwift */; }; + 051631652DC3D29400A6C0D1 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 051631642DC3D29400A6C0D1 /* SnapKit */; }; + 051631682DC3D3FA00A6C0D1 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = 051631672DC3D3FA00A6C0D1 /* Tabman */; }; + 0516316B2DC3D50700A6C0D1 /* Pageboy in Frameworks */ = {isa = PBXBuildFile; productRef = 0516316A2DC3D50700A6C0D1 /* Pageboy */; }; + 05CFFCBE2DCCB3810051129F /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 05CFFCBD2DCCB3810051129F /* Then */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 051630B82DC3D1A000A6C0D1 /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0516312B2DC3D1E900A6C0D1 /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 051630BA2DC3D1A000A6C0D1 /* DesignSystem */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DesignSystem; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 051630B52DC3D1A000A6C0D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0516316B2DC3D50700A6C0D1 /* Pageboy in Frameworks */, + 051631682DC3D3FA00A6C0D1 /* Tabman in Frameworks */, + 051631622DC3D28400A6C0D1 /* RxSwift in Frameworks */, + 05CFFCBE2DCCB3810051129F /* Then in Frameworks */, + 051631652DC3D29400A6C0D1 /* SnapKit in Frameworks */, + 051631602DC3D28400A6C0D1 /* RxCocoa in Frameworks */, + 0516312C2DC3D1E900A6C0D1 /* Infrastructure.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 051630AE2DC3D1A000A6C0D1 = { + isa = PBXGroup; + children = ( + 051630BA2DC3D1A000A6C0D1 /* DesignSystem */, + 0516312A2DC3D1E900A6C0D1 /* Frameworks */, + 051630B92DC3D1A000A6C0D1 /* Products */, + ); + sourceTree = ""; + }; + 051630B92DC3D1A000A6C0D1 /* Products */ = { + isa = PBXGroup; + children = ( + 051630B82DC3D1A000A6C0D1 /* DesignSystem.framework */, + ); + name = Products; + sourceTree = ""; + }; + 0516312A2DC3D1E900A6C0D1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0516312B2DC3D1E900A6C0D1 /* Infrastructure.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 051630B32DC3D1A000A6C0D1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 051630B72DC3D1A000A6C0D1 /* DesignSystem */ = { + isa = PBXNativeTarget; + buildConfigurationList = 051630BF2DC3D1A000A6C0D1 /* Build configuration list for PBXNativeTarget "DesignSystem" */; + buildPhases = ( + 051630B32DC3D1A000A6C0D1 /* Headers */, + 051630B42DC3D1A000A6C0D1 /* Sources */, + 051630B52DC3D1A000A6C0D1 /* Frameworks */, + 051630B62DC3D1A000A6C0D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 051630BA2DC3D1A000A6C0D1 /* DesignSystem */, + ); + name = DesignSystem; + packageProductDependencies = ( + 0516315F2DC3D28400A6C0D1 /* RxCocoa */, + 051631612DC3D28400A6C0D1 /* RxSwift */, + 051631642DC3D29400A6C0D1 /* SnapKit */, + 051631672DC3D3FA00A6C0D1 /* Tabman */, + 0516316A2DC3D50700A6C0D1 /* Pageboy */, + 05CFFCBD2DCCB3810051129F /* Then */, + ); + productName = DesignSystem; + productReference = 051630B82DC3D1A000A6C0D1 /* DesignSystem.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 051630AF2DC3D1A000A6C0D1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 051630B72DC3D1A000A6C0D1 = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 051630B22DC3D1A000A6C0D1 /* Build configuration list for PBXProject "DesignSystem" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 051630AE2DC3D1A000A6C0D1; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 0516315E2DC3D28400A6C0D1 /* XCRemoteSwiftPackageReference "RxSwift" */, + 051631632DC3D29400A6C0D1 /* XCRemoteSwiftPackageReference "SnapKit" */, + 051631662DC3D3FA00A6C0D1 /* XCRemoteSwiftPackageReference "Tabman" */, + 051631692DC3D50700A6C0D1 /* XCRemoteSwiftPackageReference "Pageboy" */, + 05CFFCBC2DCCB3810051129F /* XCRemoteSwiftPackageReference "Then" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 051630B92DC3D1A000A6C0D1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 051630B72DC3D1A000A6C0D1 /* DesignSystem */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 051630B62DC3D1A000A6C0D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 051630B42DC3D1A000A6C0D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 051630BD2DC3D1A000A6C0D1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 051630BE2DC3D1A000A6C0D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 051630C02DC3D1A000A6C0D1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.DesignSystem; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 051630C12DC3D1A000A6C0D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.DesignSystem; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 051630B22DC3D1A000A6C0D1 /* Build configuration list for PBXProject "DesignSystem" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 051630BD2DC3D1A000A6C0D1 /* Debug */, + 051630BE2DC3D1A000A6C0D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 051630BF2DC3D1A000A6C0D1 /* Build configuration list for PBXNativeTarget "DesignSystem" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 051630C02DC3D1A000A6C0D1 /* Debug */, + 051630C12DC3D1A000A6C0D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 0516315E2DC3D28400A6C0D1 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; + 051631632DC3D29400A6C0D1 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.7.1; + }; + }; + 051631662DC3D3FA00A6C0D1 /* XCRemoteSwiftPackageReference "Tabman" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uias/Tabman"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.0; + }; + }; + 051631692DC3D50700A6C0D1 /* XCRemoteSwiftPackageReference "Pageboy" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uias/Pageboy"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.0; + }; + }; + 05CFFCBC2DCCB3810051129F /* XCRemoteSwiftPackageReference "Then" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devxoul/Then"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0516315F2DC3D28400A6C0D1 /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 0516315E2DC3D28400A6C0D1 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 051631612DC3D28400A6C0D1 /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 0516315E2DC3D28400A6C0D1 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 051631642DC3D29400A6C0D1 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 051631632DC3D29400A6C0D1 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 051631672DC3D3FA00A6C0D1 /* Tabman */ = { + isa = XCSwiftPackageProductDependency; + package = 051631662DC3D3FA00A6C0D1 /* XCRemoteSwiftPackageReference "Tabman" */; + productName = Tabman; + }; + 0516316A2DC3D50700A6C0D1 /* Pageboy */ = { + isa = XCSwiftPackageProductDependency; + package = 051631692DC3D50700A6C0D1 /* XCRemoteSwiftPackageReference "Pageboy" */; + productName = Pageboy; + }; + 05CFFCBD2DCCB3810051129F /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = 05CFFCBC2DCCB3810051129F /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 051630AF2DC3D1A000A6C0D1 /* Project object */; +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutBuilder.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutBuilder.swift new file mode 100644 index 00000000..82c9a901 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutBuilder.swift @@ -0,0 +1,185 @@ +import UIKit + +public final class CollectionLayoutBuilder { + private var itemSize: NSCollectionLayoutSize? + private var groupSize: NSCollectionLayoutSize? + private var numberOfItemsPerGroup: Int = 1 + private var interItemSpacing: NSCollectionLayoutSpacing? + private var section: NSCollectionLayoutSection? + private var headerItem: NSCollectionLayoutBoundarySupplementaryItem? + + public init() { } + + public init(section existingSection: NSCollectionLayoutSection) { + self.section = existingSection + } + + @discardableResult + public func item( + width: NSCollectionLayoutDimension, + height: NSCollectionLayoutDimension + ) -> Self { + itemSize = NSCollectionLayoutSize( + widthDimension: width, + heightDimension: height + ) + + return self + } + + @discardableResult + public func group( + width: NSCollectionLayoutDimension, + height: NSCollectionLayoutDimension + ) -> Self { + groupSize = NSCollectionLayoutSize( + widthDimension: width, + heightDimension: height + ) + + return self + } + + @discardableResult + public func numberOfItemsPerGroup(_ count: Int) -> Self { + numberOfItemsPerGroup = count + + return self + } + + @discardableResult + public func itemSpacing(_ spacing: CGFloat) -> Self { + interItemSpacing = .fixed(spacing) + + return self + } + + @discardableResult + public func withContentInsets( + top: CGFloat = 0, + leading: CGFloat = 0, + bottom: CGFloat = 0, + trailing: CGFloat = 0 + ) -> Self { + section?.contentInsets = NSDirectionalEdgeInsets( + top: top, + leading: leading, + bottom: bottom, + trailing: trailing + ) + + return self + } + + @discardableResult + public func composeSection(_ axis: UIAxis) -> Self { + guard let itemSize, let groupSize else { + fatalError("Item and Group must be set before creating section") + } + + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + var group: NSCollectionLayoutGroup! + + switch axis { + case .vertical: + group = NSCollectionLayoutGroup.vertical( + layoutSize: groupSize, + subitems: Array(repeating: item, count: numberOfItemsPerGroup) + ) + + case .horizontal: + group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: Array(repeating: item, count: numberOfItemsPerGroup) + ) + + default: fatalError("Can't compose section to selected axis") + } + + if let interItemSpacing { + group.interItemSpacing = interItemSpacing + } + + section = NSCollectionLayoutSection(group: group) + + return self + } + + @discardableResult + public func header( + elementKind: String, + width: NSCollectionLayoutDimension = .fractionalWidth(1.0), + height: NSCollectionLayoutDimension = .fractionalHeight(1.0), + alignment: NSRectAlignment = .top + ) -> Self { + let headerSize = NSCollectionLayoutSize( + widthDimension: width, + heightDimension: height + ) + + headerItem = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: elementKind, + alignment: alignment + ) + + if let headerItem { + section?.boundarySupplementaryItems = [headerItem] + } + + return self + } + + @discardableResult + public func withScrollingBehavior(_ behavior: UICollectionLayoutSectionOrthogonalScrollingBehavior) -> Self { + section?.orthogonalScrollingBehavior = behavior + + return self + } + + @discardableResult + public func groupSpacing(_ spacing: CGFloat) -> Self { + section?.interGroupSpacing = spacing + + return self + } + + @discardableResult + public func modifySection(_ modifier: (NSCollectionLayoutSection) -> Void) -> Self { + if let section = self.section { + modifier(section) + } + return self + } + + @discardableResult + public func withExistingHeader(_ headerItem: NSCollectionLayoutBoundarySupplementaryItem) -> Self { + self.headerItem = headerItem + + if let section = self.section { + section.boundarySupplementaryItems = [headerItem] + } + + return self + } + + @discardableResult + public func header(_ headerItems: [NSCollectionLayoutBoundarySupplementaryItem]) -> Self { + if let section = self.section { + section.boundarySupplementaryItems = headerItems + } + + return self + } + + public func build() -> NSCollectionLayoutSection { + guard let section else { fatalError("Section must be created before building") } + return section + } + + public func buildHeader() -> NSCollectionLayoutBoundarySupplementaryItem { + guard let headerItem else { fatalError("Header must be created before building") } + return headerItem + } +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutProvidable.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutProvidable.swift new file mode 100644 index 00000000..d7c3efec --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/CollectionLayoutProvidable.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol CollectionLayoutProvidable { + func makeLayout() -> NSCollectionLayoutSection +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/GridCollectionLayoutProvider.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/GridCollectionLayoutProvider.swift new file mode 100644 index 00000000..15322073 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/GridCollectionLayoutProvider.swift @@ -0,0 +1,17 @@ +import UIKit + +public struct GridCollectionLayoutProvider: CollectionLayoutProvidable { + public init() { } + + public func makeLayout() -> NSCollectionLayoutSection { + return CollectionLayoutBuilder() + .item(width: .fractionalWidth(0.5), height: .absolute(249)) + .group(width: .fractionalWidth(1.0), height: .absolute(249)) + .numberOfItemsPerGroup(2) + .itemSpacing(16) + .composeSection(.horizontal) + .withContentInsets(top: 16, leading: 20, bottom: 0, trailing: 20) + .groupSpacing(24) + .build() + } +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/HeaderLayoutProvidable.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/HeaderLayoutProvidable.swift new file mode 100644 index 00000000..ad2e25fe --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/HeaderLayoutProvidable.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol HeaderLayoutProvidable { + func makeHeaderLayout(_ elementKind: String) -> NSCollectionLayoutBoundarySupplementaryItem +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/TagCollectionLayoutProvider.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/TagCollectionLayoutProvider.swift new file mode 100644 index 00000000..47fc72c1 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/Layout/TagCollectionLayoutProvider.swift @@ -0,0 +1,21 @@ +import UIKit + +public struct TagCollectionLayoutProvider: CollectionLayoutProvidable, HeaderLayoutProvidable { + public init() { } + + public func makeLayout() -> NSCollectionLayoutSection { + return CollectionLayoutBuilder() + .item(width: .estimated(100), height: .absolute(31)) + .group(width: .estimated(100), height: .estimated(31)) + .composeSection(.vertical) + .withScrollingBehavior(.continuous) + .groupSpacing(6) + .build() + } + + public func makeHeaderLayout(_ elementKind: String) -> NSCollectionLayoutBoundarySupplementaryItem { + return CollectionLayoutBuilder() + .header(elementKind: elementKind, height: .absolute(24)) + .buildHeader() + } +} diff --git a/Poppool/Poppool/Presentation/Components/PPButton.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPButton.swift similarity index 87% rename from Poppool/Poppool/Presentation/Components/PPButton.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPButton.swift index 87063272..c4e2a4cd 100644 --- a/Poppool/Poppool/Presentation/Components/PPButton.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPButton.swift @@ -1,21 +1,14 @@ -// -// PPButton.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -class PPButton: UIButton { - - enum ButtonStyle { +public class PPButton: UIButton { + + public enum ButtonStyle { case primary case secondary case tertiary case kakao case apple - + var backgroundColor: UIColor { switch self { case .primary: @@ -30,7 +23,7 @@ class PPButton: UIButton { return .g900 } } - + var textColor: UIColor { switch self { case .primary: @@ -45,7 +38,7 @@ class PPButton: UIButton { return .w100 } } - + var disabledBackgroundColor: UIColor { switch self { case .primary: @@ -56,7 +49,7 @@ class PPButton: UIButton { return .blu500 } } - + var disabledTextColor: UIColor { switch self { case .primary: @@ -68,40 +61,39 @@ class PPButton: UIButton { } } } - - - init( + + public init( style: ButtonStyle, text: String, disabledText: String = "", - font: UIFont? = .KorFont(style: .medium, size: 16), + font: UIFont? = .korFont(style: .medium, size: 16), cornerRadius: CGFloat = 4 ) { super.init(frame: .zero) - + self.setTitle(text, for: .normal) self.setTitle(disabledText, for: .disabled) - + self.setTitleColor(style.textColor, for: .normal) self.setTitleColor(style.disabledTextColor, for: .disabled) - + self.setBackgroundColor(style.backgroundColor, for: .normal) self.setBackgroundColor(style.disabledBackgroundColor, for: .disabled) - + self.titleLabel?.font = font self.layer.cornerRadius = cornerRadius self.clipsToBounds = true } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + /// 버튼 배경색 설정 /// - Parameters: /// - color: 색상 /// - state: 상태 - func setBackgroundColor(_ color: UIColor, for state: UIControl.State) { + public func setBackgroundColor(_ color: UIColor, for state: UIControl.State) { UIGraphicsBeginImageContext(CGSize(width: 1.0, height: 1.0)) guard let context = UIGraphicsGetCurrentContext() else { return } context.setFillColor(color.cgColor) @@ -109,7 +101,7 @@ class PPButton: UIButton { let backgroundImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + self.setBackgroundImage(backgroundImage, for: state) } } diff --git a/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPCancelHeaderView.swift similarity index 78% rename from Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPCancelHeaderView.swift index a033274a..777f79cf 100644 --- a/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPCancelHeaderView.swift @@ -1,38 +1,31 @@ -// -// PPCancelHeaderView.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit import SnapKit -final class PPCancelHeaderView: UIView { - +public final class PPCancelHeaderView: UIView { + // MARK: - Components - let backButton: UIButton = { + public let backButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_backButton"), for: .normal) button.tintColor = .black return button }() - - let cancelButton: UIButton = { + + public let cancelButton: UIButton = { let button = UIButton(type: .system) button.setTitle("취소", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 14) + button.titleLabel?.font = .korFont(style: .regular, size: 14) button.setTitleColor(.black, for: .normal) return button }() - + // MARK: - init - init() { + public init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -40,7 +33,7 @@ final class PPCancelHeaderView: UIView { // MARK: - SetUp private extension PPCancelHeaderView { - + func setUpConstraints() { self.addSubview(backButton) backButton.snp.makeConstraints { make in @@ -48,7 +41,7 @@ private extension PPCancelHeaderView { make.top.bottom.equalToSuperview().inset(8) make.leading.equalToSuperview().inset(12) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.centerY.equalTo(backButton) diff --git a/Poppool/Poppool/Presentation/Components/PPLabel.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPLabel.swift similarity index 71% rename from Poppool/Poppool/Presentation/Components/PPLabel.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPLabel.swift index 41030705..32ce068f 100644 --- a/Poppool/Poppool/Presentation/Components/PPLabel.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPLabel.swift @@ -1,22 +1,15 @@ -// -// PPLabel.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -class PPLabel: UILabel { - - init( - style: UIFont.FontStyle, +public class PPLabel: UILabel { + + public init( + style: UIFont.FontStyle, fontSize: CGFloat, text: String = "", lineHeight: CGFloat = 1.2 ) { super.init(frame: .zero) - self.font = .KorFont(style: style, size: fontSize) + self.font = .korFont(style: style, size: fontSize) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = lineHeight self.attributedText = NSMutableAttributedString( @@ -24,7 +17,7 @@ class PPLabel: UILabel { attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle] ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Poppool/Poppool/Presentation/Components/PPPicker.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPPicker.swift similarity index 73% rename from Poppool/Poppool/Presentation/Components/PPPicker.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPPicker.swift index e5164c9c..21f007cb 100644 --- a/Poppool/Poppool/Presentation/Components/PPPicker.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPPicker.swift @@ -1,20 +1,14 @@ -// -// PPPicker.swift -// PopPool -// -// Created by SeoJunYoung on 7/3/24. -// - -import SnapKit import UIKit -import RxSwift + import RxCocoa +import RxSwift +import SnapKit + +public final class PPPicker: UIView { -final class PPPicker: UIView { - // MARK: - Components private let components: [String] - let pickerView = UIPickerView() + public let pickerView = UIPickerView() private var selectView: UIView = { let view = UIView() view.backgroundColor = .g50 @@ -24,18 +18,18 @@ final class PPPicker: UIView { private let disposeBag = DisposeBag() /// 항목 선택 이벤트를 전달하는 PublishSubject입니다. let itemSelectObserver: PublishSubject = .init() - + // MARK: - init /// PickerCPNT init /// - Parameter components: UIPickerView에 표시할 문자열 배열입니다. - init(components: [String]) { + public init(components: [String]) { self.components = components super.init(frame: .zero) setUp() setUpConstraints() bind() } - + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -49,7 +43,7 @@ extension PPPicker { pickerView.delegate = self pickerView.dataSource = self } - + func setUpConstraints() { self.addSubview(selectView) self.addSubview(pickerView) @@ -62,7 +56,7 @@ extension PPPicker { make.center.equalToSuperview() } } - + func bind() { pickerView.rx.itemSelected .withUnretained(self) @@ -75,43 +69,43 @@ extension PPPicker { // MARK: - Methods extension PPPicker { - + /// 지정된 인덱스로 UIPickerView를 설정 /// - Parameter index: 설정할 인덱스 값 - func setIndex(index: Int) { + public func setIndex(index: Int) { pickerView.selectRow(index, inComponent: 0, animated: true) } } // MARK: - Delegate extension PPPicker: UIPickerViewDelegate, UIPickerViewDataSource { - func numberOfComponents(in pickerView: UIPickerView) -> Int { + public func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + + public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return components.count } - - func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + + public func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { let label = UILabel() label.text = components[row] label.textAlignment = .center - label.font = .KorFont(style: .medium, size: 16) + label.font = .korFont(style: .medium, size: 16) DispatchQueue.main.async { if let label = pickerView.view(forRow: row, forComponent: component) as? UILabel { - label.font = .KorFont(style: .bold, size: 18) + label.font = .korFont(style: .bold, size: 18) } } pickerView.subviews[1].isHidden = true return label } - func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + public func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { return 48 } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + + public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { pickerView.reloadAllComponents() } } diff --git a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressIndicator.swift similarity index 86% rename from Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressIndicator.swift index 4908fec6..55537a9e 100644 --- a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressIndicator.swift @@ -1,29 +1,21 @@ -// -// PPProgressIndicator.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - -import Foundation import UIKit -import SnapKit -import RxSwift + import RxCocoa +import RxSwift +import SnapKit +public final class PPProgressIndicator: UIStackView { -final class PPProgressIndicator: UIStackView { - // MARK: - Properties private var progressViews: [PPProgressView] private var progressIndex: Int - + // MARK: - init /// 전체 단계 수와 시작 지점을 기반으로 CMPTProgressIndicator를 초기화 /// - Parameters: /// - totalStep: 전체 단계 수 /// - startPoint: 초기 시작 지점 (1부터 시작하는 인덱스) - init(totalStep: Int, startPoint: Int) { + public init(totalStep: Int, startPoint: Int) { self.progressViews = (1...totalStep).map({ index in return PPProgressView(isSelected: index == startPoint) }) @@ -32,7 +24,7 @@ final class PPProgressIndicator: UIStackView { setUp() setUpConstraints() } - + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -40,14 +32,14 @@ final class PPProgressIndicator: UIStackView { // MARK: - SetUp private extension PPProgressIndicator { - + /// 스택 뷰 속성 설정 func setUp() { self.axis = .horizontal self.distribution = .fillEqually self.spacing = 6 } - + /// 진행 뷰의 제약 조건을 설정 func setUpConstraints() { progressViews.forEach { views in @@ -58,18 +50,18 @@ private extension PPProgressIndicator { // MARK: - Methods extension PPProgressIndicator { - + /// 진행 인디케이터를 한 단계 앞으로 이동 - func increaseIndicator() { + public func increaseIndicator() { if progressIndex < progressViews.count { progressViews[progressIndex - 1].disappearAnimation(option: .fromLeft) progressIndex += 1 progressViews[progressIndex - 1].fillAnimation(option: .fromLeft) } } - + /// 진행 인디케이터를 한 단계 뒤로 이동 - func decreaseIndicator() { + public func decreaseIndicator() { if progressIndex - 1 > 0 { progressViews[progressIndex - 1].disappearAnimation(option: .fromRight) progressIndex -= 1 diff --git a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressView.swift similarity index 91% rename from Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressView.swift index 46cf39cc..b456b27e 100644 --- a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPProgressIndicator/PPProgressView.swift @@ -1,47 +1,39 @@ -// -// PPProgressView.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - -import Foundation import UIKit + import SnapKit -final class PPProgressView: UIView { - - +public final class PPProgressView: UIView { + /// CMTPProgressView Animation Type - enum ProgressFillAnimation { + public enum ProgressFillAnimation { case fromLeft case fromRight } - + // MARK: - Components private var selectedView: UIView = { let view = UIView() - view.backgroundColor = . systemBlue //수정 필요 + view.backgroundColor = . systemBlue // 수정 필요 view.layer.cornerRadius = 1 return view }() - + private var normalView: UIView = { let view = UIView() view.backgroundColor = .secondarySystemBackground // 수정 필요 view.layer.cornerRadius = 1 return view }() - + /// CMTPProgressView 초기화 /// - Parameter isSelected: 선택 여부 - init(isSelected: Bool) { + public init(isSelected: Bool) { self.selectedView.isHidden = !isSelected super.init(frame: .zero) setUpConstraints() setUp() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -49,17 +41,17 @@ final class PPProgressView: UIView { // MARK: - SetUp private extension PPProgressView { - + /// 뷰 설정 func setUp() { self.clipsToBounds = true } - + /// 제약 조건 설정 func setUpConstraints() { self.addSubview(normalView) self.addSubview(selectedView) - + self.snp.makeConstraints { make in make.height.equalTo(4) } @@ -75,7 +67,7 @@ private extension PPProgressView { } extension PPProgressView { - + /// 선택된 뷰를 채우는 애니메이션 /// - Parameter option: 애니메이션 시작 방향 func fillAnimation(option: ProgressFillAnimation) { @@ -89,7 +81,7 @@ extension PPProgressView { }) self.layoutIfNeeded() self.selectedView.isHidden = false - + UIView.animate(withDuration: 0.2, animations: { self.selectedView.snp.updateConstraints { make in make.leading.equalToSuperview() @@ -104,7 +96,7 @@ extension PPProgressView { }) self.layoutIfNeeded() self.selectedView.isHidden = false - + UIView.animate(withDuration: 0.2, animations: { self.selectedView.snp.updateConstraints { make in make.leading.equalToSuperview() @@ -113,7 +105,7 @@ extension PPProgressView { }) } } - + /// 선택된 뷰를 사라지게 하는 애니메이션 /// - Parameter option: 애니메이션 시작 방향 func disappearAnimation(option: ProgressFillAnimation) { diff --git a/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPReturnHeaderView.swift similarity index 78% rename from Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPReturnHeaderView.swift index f56b0aae..21dd41f9 100644 --- a/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPReturnHeaderView.swift @@ -1,40 +1,34 @@ -// -// PPReturnHeaderView.swift -// Poppool -// -// Created by Porori on 11/27/24. -// - import UIKit + import SnapKit -final class PPReturnHeaderView: UIView { - +public final class PPReturnHeaderView: UIView { + // MARK: - Components - let backButton: UIButton = { + public let backButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_backButton"), for: .normal) button.tintColor = .black return button }() - - let headerLabel: UILabel = { + + public let headerLabel: UILabel = { let label = UILabel() - label.font = .KorFont(style: .regular, size: 15) + label.font = .korFont(style: .regular, size: 15) label.textColor = .g1000 return label }() - + // MARK: - init - init() { + public init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func configure(with text: String) { headerLabel.text = text } @@ -42,7 +36,7 @@ final class PPReturnHeaderView: UIView { // MARK: - SetUp private extension PPReturnHeaderView { - + func setUpConstraints() { self.addSubview(backButton) backButton.snp.makeConstraints { make in @@ -50,7 +44,7 @@ private extension PPReturnHeaderView { make.leading.equalToSuperview().inset(12) make.size.equalTo(28) } - + self.addSubview(headerLabel) headerLabel.snp.makeConstraints { make in make.centerY.equalTo(backButton) diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSearchBarView.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSearchBarView.swift new file mode 100644 index 00000000..0306def6 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSearchBarView.swift @@ -0,0 +1,86 @@ +import UIKit + +import SnapKit +import Then + +public class PPSearchBarView: UIView { + + private let stackView = UIStackView().then { + $0.spacing = 16 + $0.alignment = .center + $0.axis = .horizontal + } + + public let searchBar = UISearchBar().then { + $0.searchTextField.setPlaceholder(text: "팝업스토어명을 입력해보세요", color: .g400, font: .korFont(style: .regular, size: 14)) + $0.tintColor = .g400 + $0.backgroundColor = .g50 + $0.layer.cornerRadius = 4 + $0.setImage(UIImage(named: "icon_search_gray"), for: .search, state: .normal) + $0.searchBarStyle = .minimal + $0.searchTextField.clearButtonMode = .never + $0.searchTextField.subviews.first?.isHidden = true + } + + public let clearButton = UIButton(type: .custom).then { + $0.setImage(UIImage(named: "icon_clear_button"), for: .normal) + $0.isHidden = true + } + + public let cancelButton = UIButton(type: .system).then { + $0.setTitle("취소", for: .normal) + $0.tintColor = .g1000 + $0.titleLabel?.font = .korFont(style: .regular, size: 14) + } + + public init() { + super.init(frame: .zero) + + self.addViews() + self.setupConstraints() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +private extension PPSearchBarView { + func addViews() { + [stackView].forEach { + self.addSubview($0) + } + + [searchBar, cancelButton].forEach { + self.stackView.addArrangedSubview($0) + } + + [clearButton].forEach { + self.searchBar.addSubview($0) + } + } + + func setupConstraints() { + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(12) + make.leading.equalToSuperview().inset(20) + make.trailing.equalToSuperview().inset(16) + make.bottom.equalToSuperview().inset(7) + } + + searchBar.snp.makeConstraints { make in + make.height.equalToSuperview() + } + + clearButton.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(12) + make.centerY.equalToSuperview() + make.size.equalTo(20) + } + + cancelButton.snp.makeConstraints { make in + make.height.equalToSuperview() + } + } +} diff --git a/Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSegmentedControl.swift similarity index 82% rename from Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSegmentedControl.swift index 7699c7fe..1f869e68 100644 --- a/Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPSegmentedControl.swift @@ -1,17 +1,11 @@ -// -// PPSegmentedControl.swift -// PopPool -// -// Created by SeoJunYoung on 6/27/24. -// - import UIKit + import SnapKit -final class PPSegmentedControl: UISegmentedControl { - +public final class PPSegmentedControl: UISegmentedControl { + /// 세그먼트 컨트롤 타입 - enum SegmentedControlType { + public enum SegmentedControlType { case radio case base case tab @@ -31,24 +25,24 @@ final class PPSegmentedControl: UISegmentedControl { bottomLineView.addSubview(view) return view }() - - init(type: SegmentedControlType, segments: [String], selectedSegmentIndex: Int? = nil) { + + public init(type: SegmentedControlType, segments: [String], selectedSegmentIndex: Int? = nil) { super.init(frame: .zero) setUpSegments(type: type, segments: segments) if let selectedSegmentIndex = selectedSegmentIndex { self.selectedSegmentIndex = selectedSegmentIndex } - + } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + /// 서브뷰 레이아웃 설정 - override func layoutSubviews() { + public override func layoutSubviews() { super.layoutSubviews() - //layout이 업데이트 될 때 underbar 업데이트 + // layout이 업데이트 될 때 underbar 업데이트 let underlineFinalXPosition = (self.bounds.width / CGFloat(self.numberOfSegments)) * CGFloat(self.selectedSegmentIndex) self.underlineView.snp.updateConstraints { make in make.leading.equalTo(underlineFinalXPosition) @@ -61,12 +55,12 @@ final class PPSegmentedControl: UISegmentedControl { // MARK: - SetUp private extension PPSegmentedControl { - + /// 세그먼트 설정 메서드 /// - Parameters: /// - type: 세그먼트 컨트롤 타입 /// - segments: 세그먼트 타이틀 배열 - func setUpSegments(type: SegmentedControlType, segments: [String]) { + public func setUpSegments(type: SegmentedControlType, segments: [String]) { let emptyImage = UIImage() for seg in segments.reversed() { self.insertSegment(withTitle: seg, at: 0, animated: true) @@ -85,8 +79,8 @@ private extension PPSegmentedControl { } } self.selectedSegmentTintColor = .blu500 - setFont(color: .w100, font: .KorFont(style: .bold, size: 15), state: .selected) - setFont(color: .g400, font: .KorFont(style: .medium, size: 15), state: .normal) + setFont(color: .w100, font: .korFont(style: .bold, size: 15), state: .selected) + setFont(color: .g400, font: .korFont(style: .medium, size: 15), state: .normal) case .base: // background color 변경이 g50값으로 변경이 되지 않아 subview에 접근하여 layer를 Hidden처리 하고 새로운 뷰를 덮어씌워서 색상을 적용 for (index, view) in self.subviews.enumerated() { @@ -100,8 +94,8 @@ private extension PPSegmentedControl { } } self.selectedSegmentTintColor = .blu500 - setFont(color: .w100, font: .KorFont(style: .bold, size: 15), state: .selected) - setFont(color: .g400, font: .KorFont(style: .medium, size: 14), state: .normal) + setFont(color: .w100, font: .korFont(style: .bold, size: 15), state: .selected) + setFont(color: .g400, font: .korFont(style: .medium, size: 14), state: .normal) case .tab: self.clipsToBounds = false self.setBackgroundImage(emptyImage, for: .normal, barMetrics: .default) @@ -118,17 +112,17 @@ private extension PPSegmentedControl { make.height.equalTo(2) make.bottom.equalTo(bottomLineView.snp.bottom) } - setFont(color: .blu500, font: .KorFont(style: .bold, size: 15), state: .selected) - setFont(color: .g400, font: .KorFont(style: .medium, size: 15), state: .normal) + setFont(color: .blu500, font: .korFont(style: .bold, size: 15), state: .selected) + setFont(color: .g400, font: .korFont(style: .medium, size: 15), state: .normal) } } - + /// 폰트 설정 메서드 /// - Parameters: /// - color: 폰트 색상 /// - font: 폰트 스타일 /// - state: 세그먼트 상태 - func setFont(color: UIColor, font: UIFont?, state: UIControl.State) { + public func setFont(color: UIColor, font: UIFont?, state: UIControl.State) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.2 self.setTitleTextAttributes([ diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPTagCollectionViewCell/PPTagCollectionViewCell.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPTagCollectionViewCell/PPTagCollectionViewCell.swift new file mode 100644 index 00000000..304d3456 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PPTagCollectionViewCell/PPTagCollectionViewCell.swift @@ -0,0 +1,114 @@ +import UIKit + +import RxSwift +import SnapKit + +public final class PPTagCollectionViewCell: UICollectionViewCell { + + // MARK: - Components + + public var disposeBag = DisposeBag() + + public let titleLabel = PPLabel(style: .medium, fontSize: 11) + + public let cancelButton = UIButton() + + private let contentStackView = UIStackView().then { + $0.alignment = .center + $0.spacing = 2 + } + + // MARK: - init + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addViews() + self.setupConstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } + + public override func prepareForReuse() { + super.prepareForReuse() + + configureCell( + title: nil, + id: nil, + isSelected: false, + isCancelable: false, + fontSize: 11, + cornerRadius: 15.5 + ) + + disposeBag = DisposeBag() + } +} + +// MARK: - SetUp +private extension PPTagCollectionViewCell { + func addViews() { + [contentStackView].forEach { + self.contentView.addSubview($0) + } + + [titleLabel, cancelButton].forEach { + contentStackView.addArrangedSubview($0) + } + } + + func setupConstraints() { + contentStackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.equalToSuperview().inset(12) + make.trailing.equalToSuperview().inset(8) + } + + titleLabel.snp.makeConstraints { make in + make.height.equalTo(18) + } + + cancelButton.snp.makeConstraints { make in + make.size.equalTo(16) + } + } + + func configureUI() { + contentView.layer.cornerRadius = 15.5 + contentView.clipsToBounds = true + contentView.layer.borderWidth = 1 + } +} + +extension PPTagCollectionViewCell { + public func configureCell(title: String? = nil, id: Int?, isSelected: Bool = false, isCancelable: Bool = true, fontSize: CGFloat = 11, cornerRadius: CGFloat = 15.5) { + let xmarkImage = isSelected ? UIImage(named: "icon_xmark_white") : UIImage(named: "icon_xmark_gray") + cancelButton.setImage(xmarkImage, for: .normal) + if isSelected { + contentView.backgroundColor = .blu500 + titleLabel.setLineHeightText(text: title, font: .korFont(style: .medium, size: fontSize), lineHeight: 1.15) + titleLabel.textColor = .w100 + contentView.layer.borderColor = UIColor.blu500.cgColor + } else { + contentView.backgroundColor = .clear + titleLabel.setLineHeightText(text: title, font: .korFont(style: .medium, size: fontSize), lineHeight: 1.15) + titleLabel.textColor = .g400 + contentView.layer.borderColor = UIColor.g200.cgColor + } + cancelButton.isHidden = !isCancelable + contentView.layer.cornerRadius = cornerRadius + + if isCancelable { + contentStackView.snp.updateConstraints { make in + make.trailing.equalToSuperview().inset(8) + } + } else { + contentStackView.snp.updateConstraints { make in + make.trailing.equalToSuperview().inset(12) + } + } + } +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PopupGridCollectionViewCell/PPPopupGridCollectionViewCell.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PopupGridCollectionViewCell/PPPopupGridCollectionViewCell.swift new file mode 100644 index 00000000..0e8d1944 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Components/PopupGridCollectionViewCell/PPPopupGridCollectionViewCell.swift @@ -0,0 +1,160 @@ +import UIKit + +import Infrastructure + +import RxSwift +import SnapKit + +public final class PPPopupGridCollectionViewCell: UICollectionViewCell { + + // MARK: - Properties + + public var disposeBag = DisposeBag() + + private let imageView = UIImageView().then { + $0.contentMode = .scaleAspectFill + } + + private let categoryLabel = PPLabel(style: .bold, fontSize: 11).then { + $0.textColor = .blu500 + $0.setLineHeightText(text: "category", font: .korFont(style: .bold, size: 11)) + } + + private let titleLabel = PPLabel(style: .bold, fontSize: 14).then { + $0.numberOfLines = 2 + $0.lineBreakMode = .byTruncatingTail + $0.setLineHeightText(text: "title", font: .korFont(style: .bold, size: 14)) + } + + private let addressLabel = PPLabel(style: .medium, fontSize: 11).then { + $0.numberOfLines = 1 + $0.lineBreakMode = .byTruncatingTail + $0.textColor = .g400 + $0.setLineHeightText(text: "address", font: .korFont(style: .medium, size: 11)) + } + + private let dateLabel = PPLabel(style: .medium, fontSize: 11).then { + $0.lineBreakMode = .byTruncatingTail + $0.textColor = .g400 + $0.setLineHeightText(text: "date", font: .korFont(style: .medium, size: 11)) + } + + public let bookmarkButton = UIButton() + + private let rankLabel = UILabel().then { + $0.backgroundColor = .w10 + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + $0.isHidden = true + $0.textColor = .w100 + $0.textAlignment = .center + $0.setLineHeightText(text: "rank", font: .korFont(style: .medium, size: 11), lineHeight: 1) + } + + // MARK: - init + override init(frame: CGRect) { + super.init(frame: frame) + + self.addViews() + self.setupConstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } + + public override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } +} + +// MARK: - SetUp +private extension PPPopupGridCollectionViewCell { + func addViews() { + [imageView, categoryLabel, titleLabel, dateLabel, addressLabel, bookmarkButton].forEach { + self.contentView.addSubview($0) + } + + [rankLabel].forEach { + imageView.addSubview($0) + } + } + + func setupConstraints() { + imageView.snp.makeConstraints { make in + make.width.equalTo(contentView.bounds.width) + make.height.equalTo(140) + make.top.equalToSuperview() + make.centerX.equalToSuperview() + } + + categoryLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.top.equalTo(imageView.snp.bottom).offset(12) + make.height.equalTo(15) + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(categoryLabel.snp.bottom).offset(4) + make.leading.trailing.equalToSuperview() + } + + dateLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.height.equalTo(15).priority(.high) + make.bottom.equalToSuperview() + } + + addressLabel.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(dateLabel.snp.top) + make.height.equalTo(17).priority(.high) + } + + bookmarkButton.snp.makeConstraints { make in + make.size.equalTo(24) + make.top.trailing.equalToSuperview().inset(8) + } + + rankLabel.snp.makeConstraints { make in + make.height.equalTo(24) + make.width.equalTo(37) + make.leading.bottom.equalToSuperview().inset(12) + } + } + + func configureUI() { + self.contentView.layer.cornerRadius = 4 + self.contentView.clipsToBounds = true + + imageView.layer.cornerRadius = 4 + imageView.clipsToBounds = true + } +} + +extension PPPopupGridCollectionViewCell { + public func configureCell(imagePath: String?, id: Int64, category: String?, title: String?, address: String?, startDate: String?, endDate: String?, isBookmark: Bool, isLogin: Bool, isPopular: Bool = false, row: Int?) { + + categoryLabel.text = "#" + (category ?? "") + titleLabel.text = title + addressLabel.text = address + + let date = startDate.toDate().toPPDateString() + " ~ " + endDate.toDate().toPPDateString() + dateLabel.text = date + + let bookmarkImage = isBookmark ? UIImage(named: "icon_bookmark_fill") : UIImage(named: "icon_bookmark") + bookmarkButton.setImage(bookmarkImage, for: .normal) + + imageView.setPPImage(path: imagePath) + + bookmarkButton.isHidden = !isLogin + rankLabel.isHidden = !isPopular + + if let rank = row { + rankLabel.text = "\(rank)" + rankLabel.isHidden = rank > 2 + } + } +} diff --git a/Poppool/Poppool/Presentation/Extension/UIApplication+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIApplication+.swift similarity index 100% rename from Poppool/Poppool/Presentation/Extension/UIApplication+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIApplication+.swift diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UICollectionReusableView+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UICollectionReusableView+.swift new file mode 100644 index 00000000..1edd36fe --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UICollectionReusableView+.swift @@ -0,0 +1,7 @@ +import UIKit + +public extension UICollectionReusableView { + static var identifiers: String { + return String(describing: self) + } +} diff --git a/Poppool/Poppool/Presentation/Extension/UIColor+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIColor+.swift similarity index 97% rename from Poppool/Poppool/Presentation/Extension/UIColor+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIColor+.swift index 5de4edb1..72662faf 100644 --- a/Poppool/Poppool/Presentation/Extension/UIColor+.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIColor+.swift @@ -7,8 +7,8 @@ import UIKit -extension UIColor { - +public extension UIColor { + // 무채색 컬러 static let g50 = UIColor(hexCode: "F2F5F7") static let g100 = UIColor(hexCode: "DFE2E6") @@ -21,7 +21,7 @@ extension UIColor { static let g800 = UIColor(hexCode: "1F242B") static let g900 = UIColor(hexCode: "17191C") static let g1000 = UIColor(hexCode: "141414") - + // 화이트 톤 static let w4 = UIColor(hexCode: "ffffff", alpha: 0.04) static let w7 = UIColor(hexCode: "ffffff", alpha: 0.07) @@ -31,8 +31,7 @@ extension UIColor { static let w70 = UIColor(hexCode: "ffffff", alpha: 0.7) static let w90 = UIColor(hexCode: "ffffff", alpha: 0.9) static let w100 = UIColor(hexCode: "ffffff", alpha: 1.0) - - + // 퓨어 블랙 static let pb4 = UIColor(hexCode: "141414", alpha: 0.04) static let pb7 = UIColor(hexCode: "141414", alpha: 0.07) @@ -43,7 +42,7 @@ extension UIColor { static let pb70 = UIColor(hexCode: "141414", alpha: 0.7) static let pb90 = UIColor(hexCode: "141414", alpha: 0.9) static let pb100 = UIColor(hexCode: "141414", alpha: 1.0) - + // 블루 static let blu100 = UIColor(hexCode: "E5EEFF") static let blu200 = UIColor(hexCode: "B5CCFE") @@ -54,7 +53,7 @@ extension UIColor { static let blu700 = UIColor(hexCode: "023197") static let blu800 = UIColor(hexCode: "022364") static let blu900 = UIColor(hexCode: "011132") - + // 제이드 static let jd100 = UIColor(hexCode: "E6FFFA") static let jd200 = UIColor(hexCode: "CCFFF6") @@ -66,7 +65,7 @@ extension UIColor { static let jd800 = UIColor(hexCode: "00997D") static let jd900 = UIColor(hexCode: "00997D") static let jd1000 = UIColor(hexCode: "004D3E") - + // 레드 static let re100 = UIColor(hexCode: "FFE6E5") static let re200 = UIColor(hexCode: "FFCCCC") @@ -77,19 +76,19 @@ extension UIColor { static let re700 = UIColor(hexCode: "B30100") static let re800 = UIColor(hexCode: "800000") static let re900 = UIColor(hexCode: "4D0000") - + convenience init(hexCode: String, alpha: CGFloat = 1.0) { var hexFormatted: String = hexCode.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased() - + if hexFormatted.hasPrefix("#") { hexFormatted = String(hexFormatted.dropFirst()) } - + assert(hexFormatted.count == 6, "Invalid hex code used.") - + var rgbValue: UInt64 = 0 Scanner(string: hexFormatted).scanHexInt64(&rgbValue) - + self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, blue: CGFloat(rgbValue & 0x0000FF) / 255.0, diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIFont+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIFont+.swift new file mode 100644 index 00000000..9723e545 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIFont+.swift @@ -0,0 +1,37 @@ +import UIKit + +private final class BundleFinder { + static let module = Bundle(for: BundleFinder.self) +} + +fileprivate extension Bundle { + static let module = BundleFinder.module +} + +public extension UIFont { + static func korFont(style: FontStyle, size: CGFloat) -> UIFont { + let fontName = "GothicA1-\(style.rawValue)" + + if let font = UIFont(name: fontName, size: size) { return font } else { return registerAndGetFont(name: fontName, size: size) } + } + + static func engFont(style: FontStyle, size: CGFloat) -> UIFont { + let fontName = "Poppins-\(style.rawValue)" + + if let font = UIFont(name: fontName, size: size) { return font } else { return registerAndGetFont(name: fontName, size: size) } + } + + private static func registerAndGetFont(name: String, size: CGFloat) -> UIFont { + let url = Bundle.module.url(forResource: name, withExtension: "ttf")! + CTFontManagerRegisterFontURLs([url as CFURL] as CFArray, .process, true, nil) + return UIFont(name: name, size: size)! + + } + + enum FontStyle: String { + case bold = "Bold" + case medium = "Medium" + case regular = "Regular" + case light = "Light" + } +} diff --git a/Poppool/Poppool/Presentation/Extension/UILabel+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UILabel+.swift similarity index 84% rename from Poppool/Poppool/Presentation/Extension/UILabel+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UILabel+.swift index 592fa79a..eb0caec1 100644 --- a/Poppool/Poppool/Presentation/Extension/UILabel+.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UILabel+.swift @@ -1,13 +1,6 @@ -// -// UILabel+.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -extension UILabel { +public extension UILabel { func setLineHeightText(text: String?, font: UIFont?, lineHeight: CGFloat = 1.3) { guard let text = text, let font = font else { return } let paragraphStyle = NSMutableParagraphStyle() diff --git a/Poppool/Poppool/Presentation/Extension/UINavigationController+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UINavigationController+.swift similarity index 68% rename from Poppool/Poppool/Presentation/Extension/UINavigationController+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UINavigationController+.swift index 62923a54..b154ee64 100644 --- a/Poppool/Poppool/Presentation/Extension/UINavigationController+.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UINavigationController+.swift @@ -1,13 +1,6 @@ -// -// UINavigationController+.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit -extension UINavigationController { +public extension UINavigationController { func popViewController(animated: Bool, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock { diff --git a/Poppool/Poppool/Presentation/Extension/UITableViewCell+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UITableViewCell+.swift similarity index 100% rename from Poppool/Poppool/Presentation/Extension/UITableViewCell+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UITableViewCell+.swift diff --git a/Poppool/Poppool/Presentation/Extension/UITextField+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UITextField+.swift similarity index 69% rename from Poppool/Poppool/Presentation/Extension/UITextField+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UITextField+.swift index da25f377..883720b9 100644 --- a/Poppool/Poppool/Presentation/Extension/UITextField+.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UITextField+.swift @@ -1,13 +1,6 @@ -// -// UITextField+.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - import UIKit -extension UITextField { +public extension UITextField { func setPlaceholder(text: String, color: UIColor, font: UIFont) { self.attributedPlaceholder = NSAttributedString( string: text, diff --git a/Poppool/Poppool/Presentation/Extension/UIView+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIView+.swift similarity index 79% rename from Poppool/Poppool/Presentation/Extension/UIView+.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIView+.swift index a24684e7..44394f22 100644 --- a/Poppool/Poppool/Presentation/Extension/UIView+.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIView+.swift @@ -1,13 +1,6 @@ -// -// UIView+.swift -// Poppool -// -// Created by SeoJunYoung on 12/2/24. -// - import UIKit -extension UIView { +public extension UIView { func shake() { let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") animation.timingFunction = CAMediaTimingFunction(name: .linear) diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIViewController+.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIViewController+.swift new file mode 100644 index 00000000..034c7b7e --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Extension/UIViewController+.swift @@ -0,0 +1,191 @@ +import UIKit + +import Infrastructure + +private struct PPModalConstants { + static let animationDuration: TimeInterval = 0.2 + static let presentAnimationOptions: UIView.AnimationOptions = .curveEaseOut + static let dismissAnimationOptions: UIView.AnimationOptions = .curveEaseIn +} + +// modal의 subView와 상태를 들고있는 매니저 +private class PPModalManager { + weak var presentingVC: UIViewController? + weak var presentedViewController: (UIViewController & PPModalPresentable)? + var dimmingView: UIView? + var containerView: UIView? + + init(presenting: UIViewController) { + self.presentingVC = presenting + } +} + +extension UIViewController { + // MARK: - Storage + private struct PPModalStorage { + static var managers: [ObjectIdentifier: PPModalManager] = [:] + } + + private var pp_manager: PPModalManager { + let id = ObjectIdentifier(self) + + if let modalManager = PPModalStorage.managers[id] { + return modalManager + } + + let modalManager = PPModalManager(presenting: self) + PPModalStorage.managers[id] = modalManager + + return modalManager + } + + // MARK: - Present as Bottom Sheet + /// view controller를 bottom-sheet modal처럼 present 해줍니다 + public func PPPresent(_ viewController: UIViewController & PPModalPresentable, animated: Bool = true) { + let manager = pp_manager + manager.presentedViewController = viewController + + // presentingView에 dimming을 조절 + let dimView = UIView(frame: view.bounds) + dimView.backgroundColor = viewController.backgroundColor + dimView.alpha = 0 + dimView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(dimView) + manager.dimmingView = dimView + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handlePPTap(_:))) + dimView.addGestureRecognizer(tapGesture) + + // 높이를 결정. 값이 주워진다면 해당 값을 쓰고, 없다면 view의 기본 frame을 씀 + let height = viewController.modalHeight ?? viewController.view.frame.height + let container = UIView(frame: CGRect( + x: 0, + y: view.bounds.height, + width: view.bounds.width, + height: height + )) + container.backgroundColor = .clear + container.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] + view.addSubview(container) + manager.containerView = container + + // Embed child + addChild(viewController) + viewController.view.frame = container.bounds + viewController.view.layer.cornerRadius = viewController.cornerRadius + viewController.view.clipsToBounds = true + container.addSubview(viewController.view) + viewController.didMove(toParent: self) + + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePPPan(_:))) + container.addGestureRecognizer(pan) + + // Animate in + let animateIn = { + dimView.alpha = 1 + container.frame.origin.y = self.view.bounds.height - height + } + if animated { + UIView.animate( + withDuration: PPModalConstants.animationDuration, + delay: 0, + options: PPModalConstants.presentAnimationOptions, + animations: animateIn, + completion: nil + ) + } else { + animateIn() + } + } + + @objc private func handlePPTap(_ gesture: UITapGestureRecognizer) { + PPDismiss(animated: true) + } + + // MARK: - Dismiss + /// bottom-sheet modal을 Dismiss 합니다. + func PPDismiss(animated: Bool = true) { + guard let manager = PPModalStorage.managers[ObjectIdentifier(self)], + let container = manager.containerView, + let dimView = manager.dimmingView else { return } + + let finish = { + if let vc = manager.presentedViewController { + vc.willMove(toParent: nil) + vc.view.removeFromSuperview() + vc.removeFromParent() + } + dimView.removeFromSuperview() + container.removeFromSuperview() + PPModalStorage.managers.removeValue(forKey: ObjectIdentifier(self)) + } + + let animateOut = { + container.frame.origin.y = self.view.bounds.height + dimView.alpha = 0 + } + if animated { + UIView.animate( + withDuration: PPModalConstants.animationDuration, + delay: 0, + options: PPModalConstants.dismissAnimationOptions, + animations: animateOut, + completion: { _ in finish() } + ) + } else { + animateOut() + finish() + } + } + + // MARK: - Pan Gesture Handler + @objc private func handlePPPan(_ pan: UIPanGestureRecognizer) { + guard let manager = PPModalStorage.managers[ObjectIdentifier(self)], + let container = manager.containerView, + let dimView = manager.dimmingView, + let vc = manager.presentedViewController else { return } + + let translation = pan.translation(in: container) + let velocity = pan.velocity(in: container) + let height = vc.modalHeight ?? vc.view.frame.height + let threshold: CGFloat = 100 + + switch pan.state { + case .changed: + let minY = view.bounds.height - height + let newY = max(minY, container.frame.origin.y + translation.y) + container.frame.origin.y = newY + let progress = 1 - ((newY - minY) / height) + dimView.alpha = progress + pan.setTranslation(.zero, in: container) + + case .ended: + let minY = view.bounds.height - height + let shouldDismiss = velocity.y > threshold || container.frame.origin.y > minY + height / 2 + let animate = { + if shouldDismiss { + container.frame.origin.y = self.view.bounds.height + dimView.alpha = 0 + } else { + container.frame.origin.y = minY + dimView.alpha = 1 + } + } + UIView.animate( + withDuration: PPModalConstants.animationDuration, + delay: 0, + options: PPModalConstants.presentAnimationOptions, + animations: animate, + completion: { _ in if shouldDismiss { self.PPDismiss(animated: false) } } + ) + default: + break + } + } +} + +// Convenience dismiss inside presented VC +public extension PPModalPresentable where Self: UIViewController { + func dismissModal(animated: Bool = true) { + parent?.PPDismiss(animated: animated) + } +} diff --git a/Poppool/Poppool/Resource/Font/GothicA1-Bold.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Bold.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/GothicA1-Bold.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Bold.ttf diff --git a/Poppool/Poppool/Resource/Font/GothicA1-Light.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Light.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/GothicA1-Light.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Light.ttf diff --git a/Poppool/Poppool/Resource/Font/GothicA1-Medium.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Medium.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/GothicA1-Medium.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Medium.ttf diff --git a/Poppool/Poppool/Resource/Font/GothicA1-Regular.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Regular.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/GothicA1-Regular.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/GothicA1-Regular.ttf diff --git a/Poppool/Poppool/Resource/Font/Poppins-Bold.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Bold.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/Poppins-Bold.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Bold.ttf diff --git a/Poppool/Poppool/Resource/Font/Poppins-Light.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Light.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/Poppins-Light.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Light.ttf diff --git a/Poppool/Poppool/Resource/Font/Poppins-Medium.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Medium.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/Poppins-Medium.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Medium.ttf diff --git a/Poppool/Poppool/Resource/Font/Poppins-Regular.ttf b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Regular.ttf similarity index 100% rename from Poppool/Poppool/Resource/Font/Poppins-Regular.ttf rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Font/Poppins-Regular.ttf diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseTabmanController.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseTabmanController.swift new file mode 100644 index 00000000..9947d740 --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseTabmanController.swift @@ -0,0 +1,33 @@ +import UIKit + +import Infrastructure + +import Pageboy +import Tabman + +open class BaseTabmanController: TabmanViewController { + public init() { + super.init(nibName: nil, bundle: nil) + Logger.log( + "\(self) init", + category: .info + ) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .systemBackground + self.navigationController?.navigationBar.isHidden = true + } + + deinit { + Logger.log( + "\(self) deinit", + category: .info + ) + } +} diff --git a/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseViewController.swift similarity index 62% rename from Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseViewController.swift index 8c2b573f..54a4b629 100644 --- a/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Controllers/BaseViewController.swift @@ -1,55 +1,46 @@ -// -// BaseViewController.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/9/24. -// - import UIKit -import RxSwift +import Infrastructure + import RxCocoa +import RxSwift + +open class BaseViewController: UIViewController { -class BaseViewController: UIViewController { - - var systemStatusBarIsDark: BehaviorRelay = .init(value: true) + public var systemStatusBarIsDark: BehaviorRelay = .init(value: true) var systemStatusBarDisposeBag = DisposeBag() - - init() { + + public init() { super.init(nibName: nil, bundle: nil) Logger.log( - message: "\(self) init", - category: .info, - fileName: #file, - line: #line + "\(self) init", + category: .info ) } - - required init?(coder: NSCoder) { + + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { + + open override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white self.navigationController?.navigationBar.isHidden = true systemStatusBarIsDarkBind() } - - override func viewWillAppear(_ animated: Bool) { + + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) systemStatusBarIsDark.accept(systemStatusBarIsDark.value) } - + deinit { Logger.log( - message: "\(self) deinit", - category: .info, - fileName: #file, - line: #line + "\(self) deinit", + category: .info ) } - + func systemStatusBarIsDarkBind() { systemStatusBarIsDark .withUnretained(self) diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/InOutputable.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/InOutputable.swift new file mode 100644 index 00000000..e78b7ffe --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/InOutputable.swift @@ -0,0 +1,12 @@ +import UIKit + +public protocol InOutputable: Inputable, Outputable { } + +public protocol Inputable: Hashable { + associatedtype Input + func injection(with input: Input) +} + +public protocol Outputable { + associatedtype Output +} diff --git a/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/PPModalPresentable.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/PPModalPresentable.swift new file mode 100644 index 00000000..45ddb08f --- /dev/null +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/PPModalPresentable.swift @@ -0,0 +1,11 @@ +import UIKit + +// Protocol that presented view controllers must conform to +public protocol PPModalPresentable: AnyObject { + /// The height of the modal view. If nil, falls back to the view's own height. + var modalHeight: CGFloat? { get } + /// The background dimming color behind the modal view + var backgroundColor: UIColor { get } + /// The corner radius for the modal's top corners + var cornerRadius: CGFloat { get } +} diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionDecorationItem.swift similarity index 69% rename from Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionDecorationItem.swift index 6a2abf42..586255b9 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionDecorationItem.swift @@ -9,32 +9,42 @@ import UIKit /// `SectionDecorationItem` 구조체는 섹션에 추가될 데코레이션 뷰에 대한 정보를 정의합니다. /// 제네릭 타입 `View`는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. -struct SectionDecorationItem: SectionDecorationItemable { - typealias ReusableView = View - +public struct SectionDecorationItem: SectionDecorationItemable { + public init( + elementKind: String, + reusableView: ReusableView, + viewInput: ReusableView.Input + ) { + self.elementKind = elementKind + self.reusableView = reusableView + self.viewInput = viewInput + } + + public typealias ReusableView = View + /// 데코레이션 뷰의 종류를 나타내는 문자열입니다. - var elementKind: String - + public var elementKind: String + /// 데코레이션 뷰의 인스턴스입니다. - var reusableView: ReusableView - + public var reusableView: ReusableView + /// 데코레이션 뷰에 주입될 데이터입니다. - var viewInput: ReusableView.Input + public var viewInput: ReusableView.Input } /// `SectionDecorationItemable` 프로토콜은 데코레이션 뷰에 대한 인터페이스를 정의합니다. /// 이 프로토콜을 준수하는 타입은 데코레이션 뷰를 설정하고 반환하는 기능을 가져야 합니다. -protocol SectionDecorationItemable { - +public protocol SectionDecorationItemable { + /// 데코레이션 뷰의 타입을 정의합니다. 이 뷰는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. associatedtype ReusableView: UICollectionReusableView & Inputable - + /// 데코레이션 뷰의 종류를 나타내는 문자열입니다. var elementKind: String { get set } - + /// 데코레이션 뷰의 인스턴스입니다. var reusableView: ReusableView { get set } - + /// 데코레이션 뷰에 주입될 데이터입니다. var viewInput: ReusableView.Input { get set } } diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift similarity index 73% rename from Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift index a8c41655..00b9c7fe 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift @@ -1,52 +1,47 @@ -// -// SectionSupplementaryItem.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/29/24. -// - import UIKit +import Infrastructure + /// `SectionSupplementaryItem` 구조체는 섹션에 추가될 Supplementary View에 대한 정보를 정의합니다. /// 제네릭 타입 `View`는 `UICollectionReusableView`와 `InOutputable` 프로토콜을 준수해야 합니다. -struct SectionSupplementaryItem: SectionSupplementaryItemable { - typealias ReusableView = View - var widthDimension: NSCollectionLayoutDimension - var heightDimension: NSCollectionLayoutDimension - var elementKind: String - var alignment: NSRectAlignment - var reusableView: ReusableView - var viewInput: ReusableView.Input +public struct SectionSupplementaryItem: SectionSupplementaryItemable { + public typealias ReusableView = View + public var widthDimension: NSCollectionLayoutDimension + public var heightDimension: NSCollectionLayoutDimension + public var elementKind: String + public var alignment: NSRectAlignment + public var reusableView: ReusableView + public var viewInput: ReusableView.Input } /// `SectionSupplementaryItemable` 프로토콜은 Supplementary View에 대한 인터페이스를 정의합니다. /// 해당 프로토콜을 준수하는 타입은 Supplementary View를 설정하고 반환하는 기능을 가져야 합니다. -protocol SectionSupplementaryItemable { - +public protocol SectionSupplementaryItemable { + /// Supplementary View의 타입을 정의합니다. 이 뷰는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. associatedtype ReusableView: UICollectionReusableView, Inputable - + /// Supplementary View의 너비를 정의합니다. var widthDimension: NSCollectionLayoutDimension { get set } - + /// Supplementary View의 높이를 정의합니다. var heightDimension: NSCollectionLayoutDimension { get set } - + /// Supplementary View의 종류를 나타내는 문자열입니다. var elementKind: String { get set } - + /// Supplementary View의 정렬 방법을 정의합니다. var alignment: NSRectAlignment { get set } - + /// Supplementary View의 인스턴스입니다. var reusableView: ReusableView { get set } - + /// Supplementary View에 주입될 데이터입니다. var viewInput: ReusableView.Input { get set } } extension SectionSupplementaryItemable { - + /// 주어진 인덱스 경로에 해당하는 Supplementary View를 생성하고 반환합니다. /// /// - Parameters: @@ -59,24 +54,21 @@ extension SectionSupplementaryItemable { viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath ) -> UICollectionReusableView { - + collectionView.register( ReusableView.self, forSupplementaryViewOfKind: kind, - withReuseIdentifier: reusableView.identifiers + withReuseIdentifier: ReusableView.identifiers ) - + guard let view = collectionView.dequeueReusableSupplementaryView( ofKind: kind, - withReuseIdentifier: reusableView.identifiers, + withReuseIdentifier: ReusableView.identifiers, for: indexPath ) as? ReusableView else { Logger.log( - message: "ReusableView Error", - category: .error, - fileName: #file, - line: #line - ) + "ReusableView Error", + category: .error) return UICollectionReusableView() } view.injection(with: viewInput) diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/Sectionable.swift similarity index 85% rename from Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift rename to Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/Sectionable.swift index dd23e99e..b1a0ce5f 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift +++ b/Poppool/PresentationLayer/DesignSystem/DesignSystem/Utills/Interfaces/Sectionable/Sectionable.swift @@ -1,34 +1,29 @@ -// -// Sectionable.swift -// MomsVillage -// -// Created by SeoJunYoung on 8/29/24. -// - import UIKit +import Infrastructure + import RxSwift import SnapKit /// `Sectionable` 프로토콜은 컬렉션 뷰의 섹션 및 셀을 설정하기 위한 인터페이스를 정의합니다. /// 해당 프로토콜은 제네릭 타입 `CellType`을 사용하여 유연하게 컬렉션 뷰 셀을 처리할 수 있습니다. -protocol Sectionable { - +public protocol Sectionable { + /// 컬렉션 뷰 셀의 타입을 정의합니다. 이 셀은 `UICollectionViewCell`과 `Inputable` 프로토콜을 준수해야 합니다. associatedtype CellType: UICollectionViewCell, Inputable - + /// 셀에 입력될 데이터의 리스트를 정의합니다. var inputDataList: [CellType.Input] { get set } - + /// 섹션에 추가될 Supplementary View의 리스트를 정의합니다. var supplementaryItems: [any SectionSupplementaryItemable]? { get set } - + /// 섹션에 추가될 Decoration View의 리스트를 정의합니다. var decorationItems: [any SectionDecorationItemable]? { get set } - + /// 섹션의 페이지 var currentPage: PublishSubject { get set } - + /// 주어진 섹션 인덱스와 레이아웃 환경을 바탕으로 `NSCollectionLayoutSection`을 반환합니다. /// /// - Parameters: @@ -36,7 +31,7 @@ protocol Sectionable { /// - env: 레이아웃 환경 /// - Returns: 섹션 레이아웃을 반환합니다. func getSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection - + /// 섹션의 레이아웃을 설정합니다. /// /// - Parameters: @@ -44,7 +39,7 @@ protocol Sectionable { /// - env: 레이아웃 환경 /// - Returns: 설정된 섹션 레이아웃을 반환합니다. func setSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection - + /// 주어진 인덱스 경로에 해당하는 셀을 반환합니다. /// /// - Parameters: @@ -52,7 +47,7 @@ protocol Sectionable { /// - indexPath: 셀의 인덱스 경로 /// - Returns: 셀을 반환합니다. func getCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell - + /// Supplementary View를 반환합니다. /// /// - Parameters: @@ -67,8 +62,8 @@ protocol Sectionable { ) -> UICollectionReusableView } -extension Sectionable { - +public extension Sectionable { + /// `setSection` 메서드를 호출하여 섹션 레이아웃을 설정하고, Supplementary View를 추가합니다. /// /// - Parameters: @@ -76,55 +71,53 @@ extension Sectionable { /// - env: 레이아웃 환경 /// - Returns: 설정된 섹션 레이아웃을 반환합니다. func getSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - + let section = setSection(section: section, env: env) section.visibleItemsInvalidationHandler = { _, contentOffset, environment in let bannerIndex = Int(max(0, round(contentOffset.x / environment.container.contentSize.width))) // 음수가 되는 것을 방지하기 위해 max 사용 currentPage.onNext(bannerIndex) } - + if let supplementaryItems = supplementaryItems { - + let items = supplementaryItems.map { - + let size = NSCollectionLayoutSize( widthDimension: $0.widthDimension, heightDimension: $0.heightDimension ) - let sectionItem = NSCollectionLayoutBoundarySupplementaryItem( + + return NSCollectionLayoutBoundarySupplementaryItem( layoutSize: size, elementKind: $0.elementKind, alignment: $0.alignment ) - - return sectionItem } section.boundarySupplementaryItems = items } - + if let decorationItems = decorationItems { - + let items = decorationItems.map { - - let sectionItem = NSCollectionLayoutDecorationItem.background( + + return NSCollectionLayoutDecorationItem.background( elementKind: $0.elementKind ) - return sectionItem } section.decorationItems = items } return section } - + /// `inputDataList`의 비어 있지 않은지를 반환합니다. var isEmpty: Bool { return inputDataList.isEmpty } - + /// `inputDataList`의 아이템 수를 반환합니다. var dataCount: Int { return inputDataList.count - + } /// 주어진 인덱스 경로에 해당하는 셀을 생성하고 반환합니다. /// @@ -138,10 +131,8 @@ extension Sectionable { for: indexPath ) as? CellType else { Logger.log( - message: "dequeueReusableCell Fail", - category: .error, - fileName: #file, - line: #line + "dequeueReusableCell Fail", + category: .error ) return UICollectionViewCell() } @@ -149,7 +140,7 @@ extension Sectionable { cell.injection(with: input) return cell } - + /// 주어진 Supplementary View를 생성하고 반환합니다. /// /// - Parameters: @@ -162,18 +153,15 @@ extension Sectionable { viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath ) -> UICollectionReusableView { - + guard let item = supplementaryItems?.filter({ $0.elementKind == kind }).first else { Logger.log( - message: "ReusableView Not Register", - category: .error, - fileName: #file, - line: #line + "ReusableView Not Register", + category: .error ) fatalError() } - - let view = item.getItem(collectionView: collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) - return view + + return item.getItem(collectionView: collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) } } diff --git a/Poppool/PresentationLayer/Presentation/Presentation.xcodeproj/project.pbxproj b/Poppool/PresentationLayer/Presentation/Presentation.xcodeproj/project.pbxproj new file mode 100644 index 00000000..75b55bd5 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation.xcodeproj/project.pbxproj @@ -0,0 +1,797 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 05125B982DB626E3001342A2 /* ReactorKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05125B972DB626E3001342A2 /* ReactorKit */; }; + 05125BA12DB6275C001342A2 /* PanModal in Frameworks */ = {isa = PBXBuildFile; productRef = 05125BA02DB6275C001342A2 /* PanModal */; }; + 051631302DC3D1FD00A6C0D1 /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0516312F2DC3D1FD00A6C0D1 /* DesignSystem.framework */; }; + 0522C1E12DB67C8300B141FF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0522C1E02DB67C8300B141FF /* RxSwift */; }; + 05734C3C2DCDF6FE0093825D /* PresentationInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C352DCDF6EC0093825D /* PresentationInterface.framework */; }; + 05734C412DCDF7190093825D /* SearchFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C402DCDF7190093825D /* SearchFeatureInterface.framework */; }; + 05734C492DCDF7960093825D /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C482DCDF7960093825D /* DesignSystem.framework */; }; + 05734C572DCDF9E80093825D /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 05734C562DCDF9E80093825D /* NMapsMap */; }; + 05BDD5CC2DB6756500C1E192 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05BDD5CB2DB6756500C1E192 /* SnapKit */; }; + 05BDD5CF2DB6770300C1E192 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 05BDD5CE2DB6770300C1E192 /* Lottie */; }; + 05C1D62C2DB53A8200508FFD /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D62A2DB53A8200508FFD /* DomainInterface.framework */; }; + 05C1D62E2DB53A8200508FFD /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C1D62B2DB53A8200508FFD /* Infrastructure.framework */; }; + 05EC93D32DB6536200771CB3 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC93D22DB6536200771CB3 /* RxCocoa */; }; + 08B2A3582DB66B4100E57EFA /* RxKeyboard in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A3572DB66B4100E57EFA /* RxKeyboard */; }; + 08B2A35B2DB66B5A00E57EFA /* FloatingPanel in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A35A2DB66B5A00E57EFA /* FloatingPanel */; }; + 08B2A35E2DB66B8600E57EFA /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A35D2DB66B8600E57EFA /* RxDataSources */; }; + 08B2A3612DB66BAB00E57EFA /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A3602DB66BAB00E57EFA /* Then */; }; + 08B2A3642DB66BBC00E57EFA /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A3632DB66BBC00E57EFA /* Tabman */; }; + 08B2A3672DB66BD400E57EFA /* Pageboy in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A3662DB66BD400E57EFA /* Pageboy */; }; + 08B2A36A2DB66BF200E57EFA /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 08B2A3692DB66BF200E57EFA /* RxGesture */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 05734C3E2DCDF6FE0093825D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 058CC8FB2DB537960084221A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05734C342DCDF6EC0093825D; + remoteInfo = PresentationInterface; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0516312F2DC3D1FD00A6C0D1 /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C352DCDF6EC0093825D /* PresentationInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PresentationInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C402DCDF7190093825D /* SearchFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SearchFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C482DCDF7960093825D /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 058CC9042DB537960084221A /* Presentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Presentation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D62A2DB53A8200508FFD /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05C1D62B2DB53A8200508FFD /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 05734C362DCDF6EC0093825D /* PresentationInterface */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = PresentationInterface; + sourceTree = ""; + }; + 058CC9062DB537960084221A /* Presentation */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Presentation; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05734C322DCDF6EC0093825D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05734C492DCDF7960093825D /* DesignSystem.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058CC9012DB537960084221A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 08B2A3672DB66BD400E57EFA /* Pageboy in Frameworks */, + 051631302DC3D1FD00A6C0D1 /* DesignSystem.framework in Frameworks */, + 05BDD5CC2DB6756500C1E192 /* SnapKit in Frameworks */, + 08B2A36A2DB66BF200E57EFA /* RxGesture in Frameworks */, + 05C1D62C2DB53A8200508FFD /* DomainInterface.framework in Frameworks */, + 05734C3C2DCDF6FE0093825D /* PresentationInterface.framework in Frameworks */, + 05125B982DB626E3001342A2 /* ReactorKit in Frameworks */, + 05BDD5CF2DB6770300C1E192 /* Lottie in Frameworks */, + 08B2A35B2DB66B5A00E57EFA /* FloatingPanel in Frameworks */, + 08B2A3612DB66BAB00E57EFA /* Then in Frameworks */, + 05734C572DCDF9E80093825D /* NMapsMap in Frameworks */, + 0522C1E12DB67C8300B141FF /* RxSwift in Frameworks */, + 05C1D62E2DB53A8200508FFD /* Infrastructure.framework in Frameworks */, + 05734C412DCDF7190093825D /* SearchFeatureInterface.framework in Frameworks */, + 05125BA12DB6275C001342A2 /* PanModal in Frameworks */, + 08B2A3642DB66BBC00E57EFA /* Tabman in Frameworks */, + 05EC93D32DB6536200771CB3 /* RxCocoa in Frameworks */, + 08B2A35E2DB66B8600E57EFA /* RxDataSources in Frameworks */, + 08B2A3582DB66B4100E57EFA /* RxKeyboard in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058CC8FA2DB537960084221A = { + isa = PBXGroup; + children = ( + 058CC9062DB537960084221A /* Presentation */, + 05734C362DCDF6EC0093825D /* PresentationInterface */, + 05C1D6292DB53A8200508FFD /* Frameworks */, + 058CC9052DB537960084221A /* Products */, + ); + sourceTree = ""; + }; + 058CC9052DB537960084221A /* Products */ = { + isa = PBXGroup; + children = ( + 058CC9042DB537960084221A /* Presentation.framework */, + 05734C352DCDF6EC0093825D /* PresentationInterface.framework */, + ); + name = Products; + sourceTree = ""; + }; + 05C1D6292DB53A8200508FFD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 05734C482DCDF7960093825D /* DesignSystem.framework */, + 05734C402DCDF7190093825D /* SearchFeatureInterface.framework */, + 0516312F2DC3D1FD00A6C0D1 /* DesignSystem.framework */, + 05C1D62A2DB53A8200508FFD /* DomainInterface.framework */, + 05C1D62B2DB53A8200508FFD /* Infrastructure.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 05734C302DCDF6EC0093825D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058CC8FF2DB537960084221A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 05734C342DCDF6EC0093825D /* PresentationInterface */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05734C3B2DCDF6EC0093825D /* Build configuration list for PBXNativeTarget "PresentationInterface" */; + buildPhases = ( + 05734C302DCDF6EC0093825D /* Headers */, + 05734C312DCDF6EC0093825D /* Sources */, + 05734C322DCDF6EC0093825D /* Frameworks */, + 05734C332DCDF6EC0093825D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 05734C362DCDF6EC0093825D /* PresentationInterface */, + ); + name = PresentationInterface; + packageProductDependencies = ( + ); + productName = PresentationInterface; + productReference = 05734C352DCDF6EC0093825D /* PresentationInterface.framework */; + productType = "com.apple.product-type.framework"; + }; + 058CC9032DB537960084221A /* Presentation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058CC90B2DB537960084221A /* Build configuration list for PBXNativeTarget "Presentation" */; + buildPhases = ( + 058CC8FF2DB537960084221A /* Headers */, + 058CC9002DB537960084221A /* Sources */, + 058CC9012DB537960084221A /* Frameworks */, + 058CC9022DB537960084221A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 05734C3F2DCDF6FE0093825D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 058CC9062DB537960084221A /* Presentation */, + ); + name = Presentation; + packageProductDependencies = ( + 05125B972DB626E3001342A2 /* ReactorKit */, + 05125BA02DB6275C001342A2 /* PanModal */, + 05EC93D22DB6536200771CB3 /* RxCocoa */, + 08B2A3572DB66B4100E57EFA /* RxKeyboard */, + 08B2A35A2DB66B5A00E57EFA /* FloatingPanel */, + 08B2A35D2DB66B8600E57EFA /* RxDataSources */, + 08B2A3602DB66BAB00E57EFA /* Then */, + 08B2A3632DB66BBC00E57EFA /* Tabman */, + 08B2A3662DB66BD400E57EFA /* Pageboy */, + 08B2A3692DB66BF200E57EFA /* RxGesture */, + 05BDD5CB2DB6756500C1E192 /* SnapKit */, + 05BDD5CE2DB6770300C1E192 /* Lottie */, + 0522C1E02DB67C8300B141FF /* RxSwift */, + 05734C562DCDF9E80093825D /* NMapsMap */, + ); + productName = Presentation; + productReference = 058CC9042DB537960084221A /* Presentation.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 058CC8FB2DB537960084221A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 05734C342DCDF6EC0093825D = { + CreatedOnToolsVersion = 16.3; + }; + 058CC9032DB537960084221A = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 058CC8FE2DB537960084221A /* Build configuration list for PBXProject "Presentation" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 058CC8FA2DB537960084221A; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 05125B932DB62295001342A2 /* XCRemoteSwiftPackageReference "RxSwift" */, + 05125B962DB626E3001342A2 /* XCRemoteSwiftPackageReference "ReactorKit" */, + 05125B992DB626ED001342A2 /* XCRemoteSwiftPackageReference "SnapKit" */, + 05125B9F2DB6275C001342A2 /* XCRemoteSwiftPackageReference "PanModal" */, + 08B2A3562DB66B4100E57EFA /* XCRemoteSwiftPackageReference "RxKeyboard" */, + 08B2A3592DB66B5A00E57EFA /* XCRemoteSwiftPackageReference "FloatingPanel" */, + 08B2A35C2DB66B8600E57EFA /* XCRemoteSwiftPackageReference "RxDataSources" */, + 08B2A35F2DB66BAB00E57EFA /* XCRemoteSwiftPackageReference "Then" */, + 08B2A3622DB66BBC00E57EFA /* XCRemoteSwiftPackageReference "Tabman" */, + 08B2A3652DB66BD400E57EFA /* XCRemoteSwiftPackageReference "Pageboy" */, + 08B2A3682DB66BF200E57EFA /* XCRemoteSwiftPackageReference "RxGesture" */, + 05BDD5CD2DB6770300C1E192 /* XCRemoteSwiftPackageReference "lottie-ios" */, + 05734C552DCDF9E80093825D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 058CC9052DB537960084221A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 058CC9032DB537960084221A /* Presentation */, + 05734C342DCDF6EC0093825D /* PresentationInterface */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05734C332DCDF6EC0093825D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058CC9022DB537960084221A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05734C312DCDF6EC0093825D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058CC9002DB537960084221A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 05734C3F2DCDF6FE0093825D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05734C342DCDF6EC0093825D /* PresentationInterface */; + targetProxy = 05734C3E2DCDF6FE0093825D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 05734C392DCDF6EC0093825D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.PresentationInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 05734C3A2DCDF6EC0093825D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.PresentationInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 058CC9092DB537960084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 058CC90A2DB537960084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 058CC90C2DB537960084221A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Presentation; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 058CC90D2DB537960084221A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.Presentation; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05734C3B2DCDF6EC0093825D /* Build configuration list for PBXNativeTarget "PresentationInterface" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05734C392DCDF6EC0093825D /* Debug */, + 05734C3A2DCDF6EC0093825D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058CC8FE2DB537960084221A /* Build configuration list for PBXProject "Presentation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC9092DB537960084221A /* Debug */, + 058CC90A2DB537960084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058CC90B2DB537960084221A /* Build configuration list for PBXNativeTarget "Presentation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058CC90C2DB537960084221A /* Debug */, + 058CC90D2DB537960084221A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 05125B932DB62295001342A2 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; + 05125B962DB626E3001342A2 /* XCRemoteSwiftPackageReference "ReactorKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactorKit/ReactorKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.0; + }; + }; + 05125B992DB626ED001342A2 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.7.1; + }; + }; + 05125B9F2DB6275C001342A2 /* XCRemoteSwiftPackageReference "PanModal" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/slackhq/PanModal"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.7; + }; + }; + 05734C552DCDF9E80093825D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/navermaps/SPM-NMapsMap"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.21.0; + }; + }; + 05BDD5CD2DB6770300C1E192 /* XCRemoteSwiftPackageReference "lottie-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/lottie-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.5.1; + }; + }; + 08B2A3562DB66B4100E57EFA /* XCRemoteSwiftPackageReference "RxKeyboard" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxKeyboard"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.1; + }; + }; + 08B2A3592DB66B5A00E57EFA /* XCRemoteSwiftPackageReference "FloatingPanel" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scenee/FloatingPanel"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.8.7; + }; + }; + 08B2A35C2DB66B8600E57EFA /* XCRemoteSwiftPackageReference "RxDataSources" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxDataSources"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.2; + }; + }; + 08B2A35F2DB66BAB00E57EFA /* XCRemoteSwiftPackageReference "Then" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devxoul/Then"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; + 08B2A3622DB66BBC00E57EFA /* XCRemoteSwiftPackageReference "Tabman" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uias/Tabman"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.0; + }; + }; + 08B2A3652DB66BD400E57EFA /* XCRemoteSwiftPackageReference "Pageboy" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uias/Pageboy"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.0; + }; + }; + 08B2A3682DB66BF200E57EFA /* XCRemoteSwiftPackageReference "RxGesture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxGesture"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 05125B972DB626E3001342A2 /* ReactorKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05125B962DB626E3001342A2 /* XCRemoteSwiftPackageReference "ReactorKit" */; + productName = ReactorKit; + }; + 05125BA02DB6275C001342A2 /* PanModal */ = { + isa = XCSwiftPackageProductDependency; + package = 05125B9F2DB6275C001342A2 /* XCRemoteSwiftPackageReference "PanModal" */; + productName = PanModal; + }; + 0522C1E02DB67C8300B141FF /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05125B932DB62295001342A2 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 05734C562DCDF9E80093825D /* NMapsMap */ = { + isa = XCSwiftPackageProductDependency; + package = 05734C552DCDF9E80093825D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */; + productName = NMapsMap; + }; + 05BDD5CB2DB6756500C1E192 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05125B992DB626ED001342A2 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 05BDD5CE2DB6770300C1E192 /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = 05BDD5CD2DB6770300C1E192 /* XCRemoteSwiftPackageReference "lottie-ios" */; + productName = Lottie; + }; + 05EC93D22DB6536200771CB3 /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 05125B932DB62295001342A2 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 08B2A3572DB66B4100E57EFA /* RxKeyboard */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3562DB66B4100E57EFA /* XCRemoteSwiftPackageReference "RxKeyboard" */; + productName = RxKeyboard; + }; + 08B2A35A2DB66B5A00E57EFA /* FloatingPanel */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3592DB66B5A00E57EFA /* XCRemoteSwiftPackageReference "FloatingPanel" */; + productName = FloatingPanel; + }; + 08B2A35D2DB66B8600E57EFA /* RxDataSources */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A35C2DB66B8600E57EFA /* XCRemoteSwiftPackageReference "RxDataSources" */; + productName = RxDataSources; + }; + 08B2A3602DB66BAB00E57EFA /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A35F2DB66BAB00E57EFA /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; + 08B2A3632DB66BBC00E57EFA /* Tabman */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3622DB66BBC00E57EFA /* XCRemoteSwiftPackageReference "Tabman" */; + productName = Tabman; + }; + 08B2A3662DB66BD400E57EFA /* Pageboy */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3652DB66BD400E57EFA /* XCRemoteSwiftPackageReference "Pageboy" */; + productName = Pageboy; + }; + 08B2A3692DB66BF200E57EFA /* RxGesture */ = { + isa = XCSwiftPackageProductDependency; + package = 08B2A3682DB66BF200E57EFA /* XCRemoteSwiftPackageReference "RxGesture" */; + productName = RxGesture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 058CC8FB2DB537960084221A /* Project object */; +} diff --git a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift similarity index 100% rename from Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetReactor.swift diff --git a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift similarity index 90% rename from Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift index 5876db70..09248345 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetView.swift @@ -1,15 +1,19 @@ import UIKit -import SnapKit -import RxSwift -import RxCocoa + +import DesignSystem +import Infrastructure + import ReactorKit +import RxCocoa +import RxSwift +import SnapKit final class AdminBottomSheetView: UIView { - + // MARK: - Properties private var contentHeightConstraint: Constraint? typealias Reactor = AdminBottomSheetReactor - + // MARK: - Components let containerView: UIView = { let view = UIView() @@ -19,44 +23,43 @@ final class AdminBottomSheetView: UIView { view.layer.masksToBounds = true return view }() - + let headerView: UIView = { let view = UIView() view.backgroundColor = .white return view }() - + let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") label.textColor = .black return label }() - + let closeButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_xmark"), for: .normal) - + button.tintColor = .black return button }() - + let segmentedControl: PPSegmentedControl = { - let control = PPSegmentedControl( + return PPSegmentedControl( type: .tab, segments: ["상태값", "카테고리"], selectedSegmentIndex: 0 ) - return control }() - + let contentCollectionView: UICollectionView = { - let layout = UICollectionViewCompositionalLayout { section, env in + let layout = UICollectionViewCompositionalLayout { section, _ in let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(26), heightDimension: .absolute(36) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) - + let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(36) @@ -66,7 +69,7 @@ final class AdminBottomSheetView: UIView { subitems: [item] ) group.interItemSpacing = .fixed(12) - + let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init( top: 20, @@ -75,10 +78,10 @@ final class AdminBottomSheetView: UIView { trailing: 20 ) section.interGroupSpacing = 16 - + return section } - + let collectionView = UICollectionView( frame: .zero, collectionViewLayout: layout @@ -87,14 +90,14 @@ final class AdminBottomSheetView: UIView { collectionView.isScrollEnabled = false return collectionView }() - + let filterChipsView = FilterChipsView() - + let resetButton: PPButton = { let button = PPButton( style: .secondary, text: "초기화", - font: .KorFont(style: .medium, size: 16), + font: .korFont(style: .medium, size: 16), cornerRadius: 4 ) button.isEnabled = false @@ -106,13 +109,13 @@ final class AdminBottomSheetView: UIView { ) return button }() - + let saveButton: PPButton = { let button = PPButton( style: .primary, text: "옵션저장", disabledText: "옵션저장", - font: .KorFont(style: .medium, size: 16), + font: .korFont(style: .medium, size: 16), cornerRadius: 4 ) button.isEnabled = false @@ -124,7 +127,7 @@ final class AdminBottomSheetView: UIView { ) return button }() - + private let buttonStack: UIStackView = { let stack = UIStackView() stack.axis = .horizontal @@ -132,74 +135,74 @@ final class AdminBottomSheetView: UIView { stack.distribution = .fillEqually return stack }() - + // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) setupLayout() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Setup private func setupLayout() { backgroundColor = .clear addSubview(containerView) - + containerView.addSubview(headerView) headerView.addSubview(titleLabel) headerView.addSubview(closeButton) - + [segmentedControl, contentCollectionView, filterChipsView, buttonStack].forEach { containerView.addSubview($0) } - + buttonStack.addArrangedSubview(resetButton) buttonStack.addArrangedSubview(saveButton) - + setupConstraints() } - + private func setupConstraints() { containerView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() make.top.equalTo(headerView.snp.top) } - + headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(60) } - + titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.centerY.equalToSuperview() } - + closeButton.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(16) make.centerY.equalToSuperview() make.size.equalTo(24) } - + segmentedControl.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview() } - + contentCollectionView.snp.makeConstraints { make in make.top.equalTo(segmentedControl.snp.bottom).offset(16) make.leading.trailing.equalToSuperview() contentHeightConstraint = make.height.equalTo(160).constraint } - + filterChipsView.snp.makeConstraints { make in make.top.equalTo(contentCollectionView.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(16) } - + buttonStack.snp.makeConstraints { make in make.top.equalTo(filterChipsView.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(16) @@ -207,20 +210,20 @@ final class AdminBottomSheetView: UIView { make.height.equalTo(52) } } - + // MARK: - Public Methods func updateContentVisibility(isCategorySelected: Bool) { - Logger.log(message: "높이 변경 시작: \(isCategorySelected ? "카테고리" : "상태값")", category: .debug) - + Logger.log("높이 변경 시작: \(isCategorySelected ? "카테고리" : "상태값")", category: .debug) + let newHeight: CGFloat = isCategorySelected ? 200 : 160 - + // 애니메이션 없이 바로 적용 contentHeightConstraint?.update(offset: newHeight) contentCollectionView.invalidateIntrinsicContentSize() - + setNeedsLayout() layoutIfNeeded() - - Logger.log(message: "높이 변경 완료", category: .debug) + + Logger.log("높이 변경 완료", category: .debug) } } diff --git a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift index dbee91f3..a02c00e7 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift @@ -1,9 +1,12 @@ import UIKit -import SnapKit -import RxSwift -import RxCocoa -import ReactorKit +import DesignSystem +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit final class AdminBottomSheetViewController: BaseViewController, View { @@ -28,8 +31,6 @@ final class AdminBottomSheetViewController: BaseViewController, View { fatalError("init(coder:) has not been implemented") } - - // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -42,7 +43,7 @@ final class AdminBottomSheetViewController: BaseViewController, View { private func setupViews() { view.backgroundColor = .clear - Logger.log(message: "초기 뷰 계층:", category: .debug) + Logger.log("초기 뷰 계층:", category: .debug) view.addSubview(mainView) mainView.isUserInteractionEnabled = true @@ -57,7 +58,7 @@ final class AdminBottomSheetViewController: BaseViewController, View { containerViewBottomConstraint = make.bottom.equalTo(view.snp.bottom).constraint } - Logger.log(message: "mainView 추가 후 계층:", category: .debug) + Logger.log("mainView 추가 후 계층:", category: .debug) dimmedView.backgroundColor = .black.withAlphaComponent(0.4) dimmedView.alpha = 0 @@ -72,9 +73,9 @@ final class AdminBottomSheetViewController: BaseViewController, View { make.edges.equalToSuperview() } - Logger.log(message: "최종 뷰 계층:", category: .debug) + Logger.log("최종 뷰 계층:", category: .debug) } - + private func setupCollectionView() { mainView.contentCollectionView.register( TagSectionCell.self, @@ -85,7 +86,7 @@ final class AdminBottomSheetViewController: BaseViewController, View { // MARK: - Binding func bind(reactor: Reactor) { mainView.segmentedControl.rx.selectedSegmentIndex - .do(onNext: { index in + .do(onNext: { _ in }) .map { Reactor.Action.segmentChanged($0) } .bind(to: reactor.action) @@ -214,6 +215,6 @@ final class AdminBottomSheetViewController: BaseViewController, View { } deinit { - Logger.log(message: "BottomSheet deinit", category: .debug) + Logger.log("BottomSheet deinit", category: .debug) } } diff --git a/Poppool/Poppool/Presentation/Admin/AdminReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminReactor.swift similarity index 69% rename from Poppool/Poppool/Presentation/Admin/AdminReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminReactor.swift index 1d0f2cbd..b023363b 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminReactor.swift @@ -1,6 +1,9 @@ +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class AdminReactor: Reactor { @@ -14,27 +17,27 @@ final class AdminReactor: Reactor { } enum Mutation { - case setStores([GetAdminPopUpStoreListResponseDTO.PopUpStore]) + case setStores([AdminStore]) case setIsLoading(Bool) case navigateToRegister(Bool) - case navigateToEdit(GetAdminPopUpStoreListResponseDTO.PopUpStore) // ✅ 수정 데이터 추가 + case navigateToEdit(AdminStore) // ✅ 수정 데이터 추가 } struct State { - var storeList: [GetAdminPopUpStoreListResponseDTO.PopUpStore] = [] + var storeList: [AdminStore] = [] var isLoading: Bool = false var shouldNavigateToRegister: Bool = false - var selectedStoreForEdit: GetAdminPopUpStoreListResponseDTO.PopUpStore? // ✅ 추가 + var selectedStoreForEdit: AdminStore? // ✅ 추가 } var initialState: State var disposeBag = DisposeBag() - private let useCase: AdminUseCase + private let adminUseCase: AdminUseCase - init(useCase: AdminUseCase) { - self.useCase = useCase + init(adminUseCase: AdminUseCase) { + self.adminUseCase = adminUseCase self.initialState = State() } @@ -43,21 +46,21 @@ final class AdminReactor: Reactor { case .viewDidLoad, .reloadData: return .concat([ .just(.setIsLoading(true)), - useCase.fetchStoreList(query: nil, page: 0, size: 100) - .map { .setStores($0.popUpStoreList ?? []) }, // ✅ nil 방지 + adminUseCase.fetchStoreList(query: nil, page: 0, size: 100) + .map { .setStores($0) }, // ✅ nil 방지 .just(.setIsLoading(false)) ]) case let .updateSearchQuery(query): return .concat([ .just(.setIsLoading(true)), - useCase.fetchStoreList(query: query, page: 0, size: 100) + adminUseCase.fetchStoreList(query: query, page: 0, size: 100) .do(onNext: { response in - Logger.log(message: "조회 성공 - 응답 데이터: \(response)", category: .info) + Logger.log("조회 성공 - 응답 데이터: \(response)", category: .info) }, onError: { error in - Logger.log(message: "조회 실패 - 에러: \(error.localizedDescription)", category: .error) + Logger.log("조회 실패 - 에러: \(error.localizedDescription)", category: .error) }) - .map { .setStores($0.popUpStoreList ?? []) }, // ✅ nil 방지 + .map { .setStores($0) }, .just(.setIsLoading(false)) ]) diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpImagesCollectionView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpImagesCollectionView.swift new file mode 100644 index 00000000..a54c62b5 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpImagesCollectionView.swift @@ -0,0 +1,99 @@ +import UIKit + +import DesignSystem + +import SnapKit + +final class PopUpImagesCollectionView: UICollectionView { + // MARK: - Properties + enum Constant { + static let imageWidth: CGFloat = 80 + static let imageHeight: CGFloat = 120 + static let imageSpacing: CGFloat = 8 + } + + var onImageSelected: ((Int) -> Void)? + var onMainImageToggled: ((Int) -> Void)? + var onImageDeleted: ((Int) -> Void)? + + private var images: [ExtendedImage] = [] + + // MARK: - init + init() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: Constant.imageWidth, height: Constant.imageHeight) + layout.minimumLineSpacing = Constant.imageSpacing + + super.init(frame: .zero, collectionViewLayout: layout) + + self.addViews() + self.setupContstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - Setup +private extension PopUpImagesCollectionView { + func addViews() { } + + func setupContstraints() { } + + func configureUI() { + self.backgroundColor = .clear + self.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.identifiers) + self.dataSource = self + self.delegate = self + self.showsHorizontalScrollIndicator = false + } +} + +// MARK: - Public Methods +extension PopUpImagesCollectionView { + func updateImages(_ images: [ExtendedImage]) { + self.images = images + self.reloadData() + } +} + +// MARK: - UICollectionViewDataSource +extension PopUpImagesCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.images.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: ImageCell.identifiers, + for: indexPath + ) as? ImageCell else { + return UICollectionViewCell() + } + + let item = self.images[indexPath.item] + cell.configure(with: item) + + // 대표이미지 변경 + cell.onMainCheckToggled = { [weak self] in + self?.onMainImageToggled?(indexPath.item) + } + + // 개별 삭제 + cell.onDeleteTapped = { [weak self] in + self?.onImageDeleted?(indexPath.item) + } + + return cell + } +} + +// MARK: - UICollectionViewDelegate +extension PopUpImagesCollectionView: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + self.onImageSelected?(indexPath.item) + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterReactor.swift new file mode 100644 index 00000000..ffde70f6 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterReactor.swift @@ -0,0 +1,887 @@ +import CoreLocation +import UIKit + +import DomainInterface +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift + +final class PopUpStoreRegisterReactor: Reactor { + + // MARK: - Properties + private let adminUseCase: AdminUseCase + private let preSignedUseCase: PreSignedUseCase + private let isEditMode: Bool + private let editingStoreId: Int64? + + private var disposeBag = DisposeBag() + + init( + adminUseCase: AdminUseCase, + preSignedUseCase: PreSignedUseCase, + editingStore: AdminStore? = nil + ) { + self.adminUseCase = adminUseCase + self.preSignedUseCase = preSignedUseCase + self.isEditMode = editingStore != nil + self.editingStoreId = editingStore?.id + + // 초기 상태 설정 + let initialState = State(isEditMode: self.isEditMode) + self.initialState = initialState + } + + // MARK: - Action + enum Action { + // 폼 데이터 업데이트 + case updateName(String) + case updateAddress(String) + case updateLat(String) + case updateLon(String) + case updateDescription(String) + case selectCategory(String) + case updateMarkerTitle(String) + case updateMarkerSnippet(String) + + // 날짜/시간 관련 + case selectDateRange(start: Date, end: Date) + case selectTimeRange(start: Date, end: Date) + + // 이미지 관련 + case addImages([ExtendedImage]) + case removeImage(Int) + case removeAllImages + case toggleMainImage(Int) + case markImageDeleted(String, Int64) + + // 네트워크 관련 + case loadStoreDetail(Int64) + case geocodeAddress(String) + case save + + // 기타 + case clearError + case dismissSuccess + } + + // MARK: - Mutation + enum Mutation { + // 폼 데이터 설정 + case setName(String) + case setAddress(String) + case setLat(String) + case setLon(String) + case setDescription(String) + case setCategory(String) + case setMarkerTitle(String) + case setMarkerSnippet(String) + + // 날짜/시간 설정 + case setDateRange(start: Date, end: Date) + case setTimeRange(start: Date, end: Date) + + // 이미지 관리 + case setImages([ExtendedImage]) + case addImages([ExtendedImage]) + case removeImage(Int) + case removeAllImages + case toggleMainImage(Int) + case addDeletedImage(id: Int64, path: String) + + // 기존 스토어 데이터 설정 + case setStoreDetail(AdminStoreDetail) + case setOriginalImageIds([String: Int64]) + + // UI 상태 관리 + case setLoading(Bool) + case setSaveEnabled(Bool) + case setSuccess(Bool) + case setError(String?) + } + + // MARK: - State + struct State { + // 폼 데이터 + var name: String = "" + var address: String = "" + var lat: String = "" + var lon: String = "" + var description: String = "" + var category: String = "" + var categoryId: Int = 0 + var markerTitle: String = "마커 제목" + var markerSnippet: String = "마커 설명" + + // 날짜 및 시간 + var selectedStartDate: Date? + var selectedEndDate: Date? + var selectedStartTime: Date? + var selectedEndTime: Date? + + // 이미지 관련 + var images: [ExtendedImage] = [] + var originalImageIds: [String: Int64] = [:] + var deletedImageIds: [Int64] = [] + var deletedImagePaths: [String] = [] + + // UI 상태 + var isLoading: Bool = false + var isSaveEnabled: Bool = false + var isSuccess: Bool = false + var errorMessage: String? + + // 모드 구분 + var isEditMode: Bool + + init(isEditMode: Bool = false) { + self.isEditMode = isEditMode + } + } + + let initialState: State + + // MARK: - Mutate + func mutate(action: Action) -> Observable { + switch action { + // 폼 데이터 업데이트 액션 + case let .updateName(name): + return .just(.setName(name)) + + case let .updateAddress(address): + return .just(.setAddress(address)) + + case let .updateLat(lat): + return .just(.setLat(lat)) + + case let .updateLon(lon): + return .just(.setLon(lon)) + + case let .updateDescription(desc): + return .just(.setDescription(desc)) + + case let .selectCategory(category): + return .just(.setCategory(category)) + + case let .updateMarkerTitle(title): + return .just(.setMarkerTitle(title)) + + case let .updateMarkerSnippet(snippet): + return .just(.setMarkerSnippet(snippet)) + + // 날짜/시간 관련 액션 + case let .selectDateRange(start, end): + return .just(.setDateRange(start: start, end: end)) + + case let .selectTimeRange(start, end): + return .just(.setTimeRange(start: start, end: end)) + + // 이미지 관련 액션 + case let .addImages(newImages): + return .just(.addImages(newImages)) + + case let .removeImage(index): + return .just(.removeImage(index)) + + case .removeAllImages: + return .just(.removeAllImages) + + case let .toggleMainImage(index): + return .just(.toggleMainImage(index)) + + case let .markImageDeleted(path, id): + return .just(.addDeletedImage(id: id, path: path)) + + // 주소 지오코딩 + case let .geocodeAddress(address): + return geocodeAddress(address: address) + .flatMap { location -> Observable in + guard let location = location else { + return .just(.setError("주소를 찾을 수 없습니다.")) + } + + let latMutation = Mutation.setLat(String(format: "%.6f", location.coordinate.latitude)) + let lonMutation = Mutation.setLon(String(format: "%.6f", location.coordinate.longitude)) + + return .concat([ + .just(latMutation), + .just(lonMutation) + ]) + } + + // 스토어 상세 정보 로드 (수정 모드) + case let .loadStoreDetail(storeId): + return Observable.concat([ + .just(.setLoading(true)), + loadStoreDetail(storeId: storeId) + .catch { error in .just(.setError(error.localizedDescription)) }, + .just(.setLoading(false)) + ]) + + // 저장 액션 + case .save: + return Observable.concat([ + .just(.setLoading(true)), + saveStore() + .catch { error in .just(.setError(error.localizedDescription)) }, + .just(.setLoading(false)) + ]) + + // 오류 초기화 + case .clearError: + return .just(.setError(nil)) + + // 성공 상태 초기화 + case .dismissSuccess: + return .just(.setSuccess(false)) + } + } + + // MARK: - Transform + func transform(mutation: Observable) -> Observable { + return mutation + .map { mutation -> [Mutation] in + // 특정 mutation이 발생한 경우 상태에 따라 추가 mutation을 발생시킴 + var mutations: [Mutation] = [mutation] + + if case .setLoading(true) = mutation { + mutations.append(.setError(nil)) + } + + return mutations + } + .flatMap { Observable.from($0) } + } + + // MARK: - Reduce + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + // 폼 데이터 설정 + case let .setName(name): + newState.name = name + + case let .setAddress(address): + newState.address = address + + case let .setLat(lat): + newState.lat = lat + + case let .setLon(lon): + newState.lon = lon + + case let .setDescription(desc): + newState.description = desc + + case let .setCategory(category): + newState.category = category + newState.categoryId = getCategoryId(from: category) + + case let .setMarkerTitle(title): + newState.markerTitle = title + + case let .setMarkerSnippet(snippet): + newState.markerSnippet = snippet + + // 날짜/시간 설정 + case let .setDateRange(start, end): + newState.selectedStartDate = start + newState.selectedEndDate = end + + case let .setTimeRange(start, end): + newState.selectedStartTime = start + newState.selectedEndTime = end + + // 이미지 관련 + case let .setImages(images): + newState.images = images + + case let .addImages(newImages): + newState.images.append(contentsOf: newImages) + + // 이미지가 이제 있고 대표 이미지가 없으면 첫 번째를 대표로 설정 + if !newState.images.isEmpty && !newState.images.contains(where: { $0.isMain }) { + newState.images[0].isMain = true + } + + case let .removeImage(index): + if index < newState.images.count { + let wasMainImage = newState.images[index].isMain + let removedImagePath = newState.images[index].filePath + + // 기존 이미지인 경우 삭제 목록에 추가 + if let imageId = newState.originalImageIds[removedImagePath] { + if !newState.deletedImageIds.contains(imageId) { + newState.deletedImageIds.append(imageId) + newState.deletedImagePaths.append(removedImagePath) + } + } + + // 이미지 배열에서 제거 + newState.images.remove(at: index) + + // 대표 이미지가 삭제되었고 다른 이미지가 있으면 첫 번째를 대표로 설정 + if wasMainImage && !newState.images.isEmpty { + newState.images[0].isMain = true + } + } + + case .removeAllImages: + // 기존 이미지인 경우 모두 삭제 목록에 추가 + for image in newState.images { + if let imageId = newState.originalImageIds[image.filePath] { + if !newState.deletedImageIds.contains(imageId) { + newState.deletedImageIds.append(imageId) + newState.deletedImagePaths.append(image.filePath) + } + } + } + + newState.images.removeAll() + + case let .toggleMainImage(index): + if index < newState.images.count { + for i in 0.. Bool { + Logger.log("폼 유효성 검사 시작", category: .debug) + + // 이름 필드 검사 + guard !state.name.isEmpty else { + Logger.log("유효성 검사 실패: 이름 비어있음", category: .debug) + return false + } + + // 주소 필드 검사 + guard !state.address.isEmpty else { + Logger.log("유효성 검사 실패: 주소 비어있음", category: .debug) + return false + } + + // 위도/경도 필드 검사 + guard !state.lat.isEmpty else { + Logger.log("유효성 검사 실패: 위도 비어있음", category: .debug) + return false + } + + guard !state.lon.isEmpty else { + Logger.log("유효성 검사 실패: 경도 비어있음", category: .debug) + return false + } + + // 설명 필드 검사 + guard !state.description.isEmpty else { + Logger.log("유효성 검사 실패: 설명 비어있음", category: .debug) + return false + } + + // 카테고리 필드 검사 + guard !state.category.isEmpty else { + Logger.log("유효성 검사 실패: 카테고리 비어있음", category: .debug) + return false + } + + // 이미지 검사 + guard !state.images.isEmpty else { + Logger.log("유효성 검사 실패: 이미지 없음", category: .debug) + return false + } + + // 대표 이미지 검사 + guard state.images.contains(where: { $0.isMain }) else { + Logger.log("유효성 검사 실패: 대표 이미지 없음", category: .debug) + return false + } + + // 날짜 검사 + guard state.selectedStartDate != nil else { + Logger.log("유효성 검사 실패: 시작 날짜 없음", category: .debug) + return false + } + + guard state.selectedEndDate != nil else { + Logger.log("유효성 검사 실패: 종료 날짜 없음", category: .debug) + return false + } + + // 위도/경도 유효성 검사 + guard let latVal = Double(state.lat), + let lonVal = Double(state.lon) else { + Logger.log("유효성 검사 실패: 위도/경도 형식 오류", category: .debug) + return false + } + + // 위도/경도 값이 유효한지 검사 + guard latVal != 0 || lonVal != 0 else { + Logger.log("유효성 검사 실패: 위도/경도 값이 모두 0", category: .debug) + return false + } + + // 날짜 순서 검사 + if let startDate = state.selectedStartDate, + let endDate = state.selectedEndDate, + startDate > endDate { + Logger.log("유효성 검사 실패: 시작일이 종료일보다 늦음", category: .debug) + return false + } + + Logger.log("유효성 검사 성공", category: .debug) + return true + } + + // 주소 지오코딩 + private func geocodeAddress(address: String) -> Observable { + Logger.log("지오코딩 함수 호출: \(address)", category: .debug) + + return Observable.create { observer in + let geocoder = CLGeocoder() + let fullAddress = "\(address), Korea" + + geocoder.geocodeAddressString( + fullAddress, + in: nil, + preferredLocale: Locale(identifier: "ko_KR") + ) { placemarks, error in + if let error = error { + Logger.log("Geocoding error: \(error.localizedDescription)", category: .error) + observer.onNext(nil) + observer.onCompleted() + return + } + + if let location = placemarks?.first?.location { + observer.onNext(location) + } else { + observer.onNext(nil) + } + observer.onCompleted() + } + + return Disposables.create() + } + } + + // S3에서 이미지 삭제 + private func deleteImagesFromS3(_ imagePaths: [String]) { + guard !imagePaths.isEmpty else { return } + + preSignedUseCase.tryDelete(objectKeyList: imagePaths) + .subscribe( + onCompleted: { + Logger.log("S3에서 모든 이미지 삭제 성공: \(imagePaths.count)개", category: .info) + }, + onError: { error in + Logger.log("S3에서 이미지 삭제 실패: \(error.localizedDescription)", category: .error) + } + ) + .disposed(by: disposeBag) + } + + // 카테고리 ID 매핑 + private func getCategoryId(from title: String) -> Int { + let cleanTitle = title.replacingOccurrences(of: " ▾", with: "") + Logger.log("카테고리 매핑 시작 - 타이틀: \(cleanTitle)", category: .debug) + + let categoryMap: [String: Int64] = [ + "패션": 1, + "라이프스타일": 2, + "뷰티": 3, + "음식/요리": 4, + "예술": 5, + "반려동물": 6, + "여행": 7, + "엔터테인먼트": 8, + "애니메이션": 9, + "키즈": 10, + "스포츠": 11, + "게임": 12 + ] + + if let id = categoryMap[cleanTitle] { + Logger.log("카테고리 매핑 성공: \(cleanTitle) -> \(id)", category: .debug) + return Int(id) + } else { + Logger.log("카테고리 매핑 실패: \(cleanTitle)에 해당하는 ID를 찾을 수 없음", category: .error) + return 1 // 기본값 + } + } + + // 날짜 형식 변환 + private func getFormattedDate(from date: Date?) -> String { + guard let date = date else { return "" } + + let formatter = DateFormatter() + formatter.timeZone = TimeZone(identifier: "Asia/Seoul") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + + return formatter.string(from: date) + } + + // 날짜 및 시간 결합 + private func createDateTime(date: Date?, time: Date?) -> Date? { + guard let date = date else { return nil } + + if let time = time { + let calendar = Calendar.current + + // 날짜 부분 추출 + var components = calendar.dateComponents([.year, .month, .day], from: date) + + // 시간 부분 추출 + let timeComponents = calendar.dateComponents([.hour, .minute], from: time) + + // 날짜와 시간 결합 + components.hour = timeComponents.hour + components.minute = timeComponents.minute + components.second = 0 + components.timeZone = TimeZone(identifier: "Asia/Seoul") + + return calendar.date(from: components) + } + + return date + } + + // 날짜/시간 준비 + private func prepareDateTime(state: State) -> (startDate: String, endDate: String) { + // 시작일/시간 결합 + let startDateTime = createDateTime(date: state.selectedStartDate, time: state.selectedStartTime) + + // 종료일/시간 결합 + let endDateTime = createDateTime(date: state.selectedEndDate, time: state.selectedEndTime) + + let startDate = getFormattedDate(from: startDateTime) + let endDate = getFormattedDate(from: endDateTime) + + return (startDate: startDate, endDate: endDate) + } + + // 스토어 상세 정보 로드 + private func loadStoreDetail(storeId: Int64) -> Observable { + return adminUseCase.fetchStoreDetail(id: storeId) + .flatMap { [weak self] storeDetail -> Observable in + guard let self = self else { return .empty() } + + // 이미지 ID 매핑 초기화 및 설정 + var originalImageIds: [String: Int64] = [:] + for image in storeDetail.images { + originalImageIds[image.imageUrl] = image.id + } + + // 중복 및 삭제된 이미지 제외를 위한 집합 + var loadedImageUrls = Set() + let deletedIdSet = Set(self.currentState.deletedImageIds) + + // 이미지 로드 및 변환 + let mainImageUrl = storeDetail.mainImageUrl + var loadedImages: [ExtendedImage] = [] + let dispatchGroup = DispatchGroup() + + let imageObservable = Observable.create { observer in + for imageData in storeDetail.images { + // 중복 이미지 건너뛰기 + if loadedImageUrls.contains(imageData.imageUrl) { + continue + } + + // 삭제된 이미지 건너뛰기 + if deletedIdSet.contains(imageData.id) { + continue + } + + loadedImageUrls.insert(imageData.imageUrl) + + dispatchGroup.enter() + + if let imageURL = self.preSignedUseCase.fullImageURL(from: imageData.imageUrl) { + URLSession.shared.dataTask(with: imageURL) { data, _, error in + defer { dispatchGroup.leave() } + + if let error = error { + Logger.log("이미지 로드 오류: \(error.localizedDescription)", category: .error) + return + } + + guard let data = data, + let image = UIImage(data: data) else { + Logger.log("이미지 변환 실패", category: .error) + return + } + + let isMain = (imageData.imageUrl == mainImageUrl) + let extendedImage = ExtendedImage(filePath: imageData.imageUrl, image: image, isMain: isMain) + + DispatchQueue.main.async { + loadedImages.append(extendedImage) + } + }.resume() + } else { + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + if !loadedImages.isEmpty && !loadedImages.contains(where: { $0.isMain }) { + loadedImages[0].isMain = true + } + + observer.onNext(.setImages(loadedImages)) + observer.onNext(.setOriginalImageIds(originalImageIds)) + observer.onCompleted() + } + + return Disposables.create() + } + + return Observable.concat([ + .just(.setStoreDetail(storeDetail)), + imageObservable + ]) + } + } + + // 저장 액션 처리 + private func saveStore() -> Observable { + let state = self.currentState + + // 유효성 검사 + if !validateForm(state: state) { + return .just(.setError("필수 항목을 모두 입력해 주세요.")) + } + + if state.isEditMode { + return updateStore() + } else { + return createStore() + } + } + + // 이미지 업로드 + private func uploadImages() -> Observable<[String]> { + let uuid = UUID().uuidString + let updatedImages = currentState.images.enumerated().map { index, image in + let filePath = "PopUpImage/\(currentState.name)/\(uuid)/\(index).jpg" + return ExtendedImage( + filePath: filePath, + image: image.image, + isMain: image.isMain) + } + + return preSignedUseCase.tryUpload(presignedURLRequest: updatedImages.map { + return (filePath: $0.filePath, image: $0.image) + }) + .asObservable() // Single을 Observable로 변환 + .map { _ in updatedImages.map { $0.filePath } } + } + + // 신규 스토어 등록 + private func createStore() -> Observable { + return uploadImages() + .flatMap { [weak self] imagePaths -> Observable in + guard let self = self else { return .empty() } + + let state = self.currentState + let dates = self.prepareDateTime(state: state) + + // 메인 이미지 찾기 + let mainImage = imagePaths.first { path in + if let index = state.images.firstIndex(where: { $0.filePath == path }) { + return state.images[index].isMain + } + return false + } ?? imagePaths.first ?? "" + + let params = CreateStoreParams( + name: state.name, + categoryId: state.categoryId, + desc: state.description, + address: state.address, + startDate: dates.startDate, + endDate: dates.endDate, + mainImageUrl: mainImage, + imageUrlList: imagePaths, + latitude: Double(state.lat) ?? 0, + longitude: Double(state.lon) ?? 0, + markerTitle: state.markerTitle, + markerSnippet: state.markerSnippet, + startDateBeforeEndDate: true + ) + + return self.adminUseCase.createStore(params: params) + .andThen(Observable.just(.setSuccess(true))) + } + } + + // 기존 스토어 수정 + private func updateStore() -> Observable { + // 기존에 저장된 이미지는 재업로드하지 않고 그대로 사용 + // 새로 추가된 이미지만 업로드 + let state = self.currentState + + // 새로 추가된 이미지만 필터링 + let newImages = state.images.filter { image in + return !state.originalImageIds.keys.contains(image.filePath) + } + + // 새 이미지가 없으면 바로 스토어 정보 업데이트 + if newImages.isEmpty { + return updateStoreInfo(nil) + } + + // 새 이미지 업로드 + return uploadNewImages(newImages) + .flatMap { [weak self] newImagePaths -> Observable in + guard let self = self else { return .empty() } + return self.updateStoreInfo(newImagePaths) + } + } + + // 새 이미지 업로드 + private func uploadNewImages(_ newImages: [ExtendedImage]) -> Observable<[String]> { + let uuid = UUID().uuidString + let updatedImages = newImages.enumerated().map { index, image in + let filePath = "PopUpImage/\(currentState.name)/\(uuid)/\(index).jpg" + return ExtendedImage( + filePath: filePath, + image: image.image, + isMain: image.isMain) + } + + return preSignedUseCase.tryUpload(presignedURLRequest: updatedImages.map { + return (filePath: $0.filePath, image: $0.image) + }) + .asObservable() + .map { _ in updatedImages.map { $0.filePath } } + } + + // 스토어 정보 업데이트 + private func updateStoreInfo(_ newImagePaths: [String]?) -> Observable { + guard let storeId = editingStoreId else { + return .just(.setError("스토어 ID가 없습니다.")) + } + + let state = self.currentState + let dates = prepareDateTime(state: state) + + // 모든 이미지 경로 (기존 이미지 + 새 이미지) + var allPaths: [String] = [] + + // 삭제되지 않은 기존 이미지 경로 추가 + let deletedIdSet = Set(state.deletedImageIds) + for (path, id) in state.originalImageIds { + if !deletedIdSet.contains(id) { + allPaths.append(path) + } + } + + // 새로 업로드된 이미지 경로 추가 + if let newPaths = newImagePaths { + allPaths.append(contentsOf: newPaths) + } + + // 메인 이미지 경로 결정 + let mainImage: String + if let mainImg = state.images.first(where: { $0.isMain }) { + if state.originalImageIds.keys.contains(mainImg.filePath) { + // 기존 이미지가 메인 + mainImage = mainImg.filePath + } else { + // 새 이미지가 메인인 경우 + // 현재 이미지 배열에서의 위치 찾기 + let idx = state.images.firstIndex(where: { $0.filePath == mainImg.filePath }) ?? 0 + + // 해당 위치가 새 이미지 배열 범위 내에 있는지 확인 + if let newPaths = newImagePaths, idx < newPaths.count { + mainImage = newPaths[idx] + } else if !allPaths.isEmpty { + mainImage = allPaths[0] + } else { + mainImage = "" + } + } + } else if !allPaths.isEmpty { + mainImage = allPaths[0] + } else { + mainImage = "" + } + + let params = UpdateStoreParams( + id: storeId, + name: state.name, + categoryId: state.categoryId, + desc: state.description, + address: state.address, + startDate: dates.startDate, + endDate: dates.endDate, + mainImageUrl: mainImage, + imageUrlList: allPaths, + imagesToDelete: state.deletedImageIds, + latitude: Double(state.lat) ?? 0, + longitude: Double(state.lon) ?? 0, + markerTitle: state.markerTitle, + markerSnippet: state.markerSnippet, + startDateBeforeEndDate: true + ) + + return self.adminUseCase.updateStore(params: params) + .andThen(Observable.just(.setSuccess(true))) + + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterView.swift new file mode 100644 index 00000000..f33fb43b --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterView.swift @@ -0,0 +1,525 @@ +import UIKit + +import SnapKit + +final class PopUpRegisterView: UIView { + // MARK: - Properties + + enum Constant { + static let navigationHeight: CGFloat = 44 + static let logoWidth: CGFloat = 22 + static let logoHeight: CGFloat = 35 + static let edgeInset: CGFloat = 16 + static let buttonSize: CGFloat = 32 + static let cornerRadius: CGFloat = 8 + static let verticalSpacing: CGFloat = 8 + static let formLabelWidth: CGFloat = 80 + } + + // 네비게이션 영역 + let navigationContainer = UIView() + var writerLabel: UILabel! + + let logoImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "image_login_logo") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + let accountIdLabel: UILabel = { + let lbl = UILabel() + lbl.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + lbl.textColor = .black + return lbl + }() + + let menuButton: UIButton = { + let btn = UIButton(type: .system) + btn.setImage(UIImage(systemName: "adminlist"), for: .normal) + btn.tintColor = .black + return btn + }() + + // 타이틀 영역 + let titleContainer = UIView() + + let backButton: UIButton = { + let btn = UIButton(type: .system) + btn.setImage(UIImage(systemName: "chevron.left"), for: .normal) + btn.tintColor = .black + return btn + }() + + let pageTitleLabel: UILabel = { + let lbl = UILabel() + lbl.text = "팝업스토어 등록" + lbl.font = UIFont.boldSystemFont(ofSize: 18) + lbl.textColor = .black + return lbl + }() + + // 스크롤 영역 + let scrollView = UIScrollView() + let contentView = UIView() + + // 이미지 영역 + let addImageButton = UIButton(type: .system).then { + $0.setTitle("이미지 추가", for: .normal) + $0.setTitleColor(.systemBlue, for: .normal) + } + + let removeAllButton = UIButton(type: .system).then { + $0.setTitle("전체 삭제", for: .normal) + $0.setTitleColor(.red, for: .normal) + } + + let imagesCollectionView: PopUpImagesCollectionView = PopUpImagesCollectionView() + + // 폼 영역 + let formBackgroundView = UIView().then { + $0.backgroundColor = .white + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.layer.cornerRadius = 8 + } + + let verticalStack = UIStackView().then { + $0.axis = .vertical + $0.spacing = 0 + } + + let nameField = UITextField().then { + $0.placeholder = "팝업스토어 이름을 입력해 주세요." + $0.font = UIFont.systemFont(ofSize: 14) + $0.textColor = .darkGray + $0.borderStyle = .none + $0.layer.cornerRadius = 8 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.setLeftPaddingPoints(8) + } + + let categoryButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("카테고리 선택 ▾", for: .normal) + btn.setTitleColor(.darkGray, for: .normal) + btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor.lightGray.cgColor + btn.contentHorizontalAlignment = .left + btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8) + return btn + }() + + let addressField = UITextField().then { + $0.placeholder = "팝업스토어 주소를 입력해 주세요." + $0.font = UIFont.systemFont(ofSize: 14) + $0.textColor = .darkGray + $0.borderStyle = .none + $0.layer.cornerRadius = 8 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.setLeftPaddingPoints(8) + } + + let latField = UITextField().then { + $0.placeholder = "" + $0.font = UIFont.systemFont(ofSize: 14) + $0.textColor = .darkGray + $0.borderStyle = .none + $0.layer.cornerRadius = 8 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.textAlignment = .center + $0.setLeftPaddingPoints(8) + } + + let lonField = UITextField().then { + $0.placeholder = "" + $0.font = UIFont.systemFont(ofSize: 14) + $0.textColor = .darkGray + $0.borderStyle = .none + $0.layer.cornerRadius = 8 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.textAlignment = .center + $0.setLeftPaddingPoints(8) + } + + let periodButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("기간 선택 ▾", for: .normal) + btn.setTitleColor(.darkGray, for: .normal) + btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor.lightGray.cgColor + btn.contentHorizontalAlignment = .left + btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8) + return btn + }() + + let timeButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("시간 선택 ▾", for: .normal) + btn.setTitleColor(.darkGray, for: .normal) + btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor.lightGray.cgColor + btn.contentHorizontalAlignment = .left + btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8) + return btn + }() + + let descriptionTextView = UITextView().then { + $0.font = UIFont.systemFont(ofSize: 14) + $0.textColor = .darkGray + $0.layer.cornerRadius = 8 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7) + $0.isScrollEnabled = true + } + + let saveButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("저장", for: .normal) + btn.setTitleColor(.white, for: .normal) + btn.backgroundColor = .lightGray + btn.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) + btn.layer.cornerRadius = 8 + btn.isEnabled = false + return btn + }() + + // MARK: - init + init() { + super.init(frame: .zero) + + self.addViews() + self.setupContstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension PopUpRegisterView { + func addViews() { + // 네비게이션 영역 + self.addSubview(self.navigationContainer) + self.navigationContainer.addSubview(self.logoImageView) + self.navigationContainer.addSubview(self.accountIdLabel) + self.navigationContainer.addSubview(self.menuButton) + + // 타이틀 영역 + self.addSubview(self.titleContainer) + self.titleContainer.addSubview(self.backButton) + self.titleContainer.addSubview(self.pageTitleLabel) + + // 스크롤 영역 + self.addSubview(self.scrollView) + self.scrollView.addSubview(self.contentView) + + // 이미지 영역 + let buttonStack = UIStackView(arrangedSubviews: [self.addImageButton, self.removeAllButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.spacing = 16 + + self.contentView.addSubview(buttonStack) + self.contentView.addSubview(self.imagesCollectionView) + + // 폼 영역 + self.contentView.addSubview(self.formBackgroundView) + self.formBackgroundView.addSubview(self.verticalStack) + + // 저장 버튼 + self.addSubview(self.saveButton) + } + + func setupContstraints() { + // 네비게이션 영역 + self.navigationContainer.snp.makeConstraints { make in + make.top.equalTo(self.safeAreaLayoutGuide) + make.left.right.equalToSuperview() + make.height.equalTo(Constant.navigationHeight) + } + + self.logoImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(8) + make.centerY.equalToSuperview() + make.width.equalTo(Constant.logoWidth) + make.height.equalTo(Constant.logoHeight) + } + + self.accountIdLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(self.logoImageView.snp.right).offset(8) + } + + self.menuButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().inset(Constant.edgeInset) + make.width.height.equalTo(Constant.buttonSize) + } + + // 타이틀 영역 + self.titleContainer.snp.makeConstraints { make in + make.top.equalTo(self.navigationContainer.snp.bottom) + make.left.right.equalToSuperview() + make.height.equalTo(Constant.navigationHeight) + } + + self.backButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(8) + make.width.height.equalTo(32) + } + + self.pageTitleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(self.backButton.snp.right).offset(4) + } + + // 스크롤 영역 + self.scrollView.snp.makeConstraints { make in + make.top.equalTo(self.titleContainer.snp.bottom) + make.left.right.equalToSuperview() + make.bottom.equalTo(self.safeAreaLayoutGuide).offset(-74) + } + + self.contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.width.equalTo(self.scrollView.snp.width) + } + + // 이미지 영역 + guard let buttonStack = contentView.subviews.first as? UIStackView else { + return + } + buttonStack.snp.makeConstraints { make in + make.top.equalToSuperview().offset(16) + make.left.right.equalToSuperview().inset(16) + make.height.equalTo(40) + } + + self.imagesCollectionView.snp.makeConstraints { make in + make.top.equalTo(buttonStack.snp.bottom).offset(8) + make.left.right.equalToSuperview().inset(16) + make.height.equalTo(130) + } + + // 폼 영역 + self.formBackgroundView.snp.makeConstraints { make in + make.top.equalTo(self.imagesCollectionView.snp.bottom).offset(16) + make.left.right.equalToSuperview().inset(16) + make.bottom.equalToSuperview().offset(-16) + } + + self.verticalStack.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + self.saveButton.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(16) + make.bottom.equalTo(self.safeAreaLayoutGuide).offset(-16) + make.height.equalTo(44) + } + } + + func configureUI() { + self.backgroundColor = UIColor(white: 0.95, alpha: 1) + + // 폼 요소 추가 + self.setupFormRows() + } + + func setupFormRows() { + self.addFormRow(leftTitle: "이름", rightView: self.nameField) + + self.addFormRow(leftTitle: "카테고리", rightView: self.categoryButton) + + let latLabel = self.makePlainLabel("위도") + let lonLabel = self.makePlainLabel("경도") + + let latStack = UIStackView(arrangedSubviews: [latLabel, self.latField]) + latStack.axis = .horizontal + latStack.spacing = 8 + latStack.distribution = .fillProportionally + + let lonStack = UIStackView(arrangedSubviews: [lonLabel, self.lonField]) + lonStack.axis = .horizontal + lonStack.spacing = 8 + lonStack.distribution = .fillProportionally + + let latLonRow = UIStackView(arrangedSubviews: [latStack, lonStack]) + latLonRow.axis = .horizontal + latLonRow.spacing = 16 + latLonRow.distribution = .fillEqually + + let locationVStack = UIStackView(arrangedSubviews: [self.addressField, latLonRow]) + locationVStack.axis = .vertical + locationVStack.spacing = 8 + locationVStack.distribution = .fillEqually + + self.addFormRow(leftTitle: "위치", rightView: locationVStack, rowHeight: nil, totalHeight: 80) + + // 마커 필드 추가 + let markerLabel = self.makePlainLabel("마커명") + let markerField = self.makeRoundedTextField("") + let markerStackH = UIStackView(arrangedSubviews: [markerLabel, markerField]) + markerStackH.axis = .horizontal + markerStackH.spacing = 8 + markerStackH.distribution = .fillProportionally + + let snippetLabel = self.makePlainLabel("스니펫") + let snippetField = self.makeRoundedTextField("") + let snippetStackH = UIStackView(arrangedSubviews: [snippetLabel, snippetField]) + snippetStackH.axis = .horizontal + snippetStackH.spacing = 8 + snippetStackH.distribution = .fillProportionally + + let markerVStack = UIStackView(arrangedSubviews: [markerStackH, snippetStackH]) + markerVStack.axis = .vertical + markerVStack.spacing = 8 + markerVStack.distribution = .fillEqually + + self.addFormRow(leftTitle: "마커", rightView: markerVStack, rowHeight: nil, totalHeight: 80) + + // 기간 및 시간 + self.addFormRow(leftTitle: "기간", rightView: self.periodButton) + self.addFormRow(leftTitle: "시간", rightView: self.timeButton) + + // 작성자 및 작성시간 + let currentTime = self.getCurrentFormattedTime() + + self.writerLabel = self.makeSimpleLabel("") + let timeLabel = self.makeSimpleLabel(currentTime) + + self.addFormRow(leftTitle: "작성자", rightView: writerLabel) + self.addFormRow(leftTitle: "작성시간", rightView: timeLabel) + + // 상태값 + let statusLbl = self.makeSimpleLabel("진행") + self.addFormRow(leftTitle: "상태값", rightView: statusLbl) + + // 설명 + self.addFormRow(leftTitle: "설명", rightView: self.descriptionTextView, rowHeight: nil, totalHeight: 120) + } + + func addFormRow(leftTitle: String, rightView: UIView, rowHeight: CGFloat? = 36, totalHeight: CGFloat? = nil) { + let row = UIView() + row.backgroundColor = .white + + let leftBG = UIView() + leftBG.backgroundColor = UIColor(white: 0.94, alpha: 1) + row.addSubview(leftBG) + leftBG.snp.makeConstraints { make in + make.top.bottom.left.equalToSuperview() + make.width.equalTo(80) + } + + let leftLabel = UILabel() + leftLabel.text = leftTitle + leftLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold) + leftLabel.textColor = .black + leftLabel.textAlignment = .center + leftBG.addSubview(leftLabel) + leftLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.right.equalToSuperview().inset(8) + } + + let rightBG = UIView() + rightBG.backgroundColor = .white + row.addSubview(rightBG) + rightBG.snp.makeConstraints { make in + make.top.bottom.right.equalToSuperview() + make.left.equalTo(leftBG.snp.right) + } + + rightBG.addSubview(rightView) + rightView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(7) + make.bottom.equalToSuperview().offset(-7) + make.left.equalToSuperview().offset(8) + make.right.equalToSuperview().offset(-8) + if let fixH = rowHeight { + make.height.equalTo(fixH).priority(.medium) + } + } + + if let totalH = totalHeight { + row.snp.makeConstraints { make in + make.height.equalTo(totalH).priority(.high) + } + } else { + row.snp.makeConstraints { make in + make.height.greaterThanOrEqualTo(41) + } + } + + let separator = UIView() + separator.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) + row.addSubview(separator) + separator.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.height.equalTo(1) + } + + self.verticalStack.addArrangedSubview(row) + } + + // 유틸리티 메서드 + func makeRoundedTextField(_ placeholder: String) -> UITextField { + let tf = UITextField() + tf.placeholder = placeholder + tf.font = UIFont.systemFont(ofSize: 14) + tf.textColor = .darkGray + tf.borderStyle = .none + tf.layer.cornerRadius = 8 + tf.layer.borderWidth = 1 + tf.layer.borderColor = UIColor.lightGray.cgColor + tf.setLeftPaddingPoints(8) + return tf + } + + func makePlainLabel(_ text: String) -> UILabel { + let lbl = UILabel() + lbl.text = text + lbl.font = UIFont.systemFont(ofSize: 14) + lbl.textColor = .darkGray + lbl.textAlignment = .right + lbl.setContentHuggingPriority(.required, for: .horizontal) + return lbl + } + + func makeSimpleLabel(_ text: String) -> UILabel { + let lbl = UILabel() + lbl.text = text + lbl.font = UIFont.systemFont(ofSize: 14) + lbl.textColor = .darkGray + return lbl + } + + func getCurrentFormattedTime() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy.MM.dd HH:mm" + formatter.timeZone = TimeZone(identifier: "Asia/Seoul") + return formatter.string(from: Date()) + } +} +extension UITextField { + func setLeftPaddingPoints(_ amount: CGFloat) { + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: frame.size.height)) + leftView = paddingView + leftViewMode = .always + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterViewController.swift new file mode 100644 index 00000000..da9ccb95 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminRegister/PopUpStoreRegisterViewController.swift @@ -0,0 +1,573 @@ +import CoreLocation +import PhotosUI +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit + +final class PopUpStoreRegisterViewController: BaseViewController { + + // MARK: - Properties + private var pickerViewController: PHPickerViewController? + private let nickname: String + var completionHandler: (() -> Void)? + var disposeBag = DisposeBag() + + private var mainView: PopUpRegisterView + + // MARK: - Initializer + init( + nickname: String, + editingStore: AdminStore? = nil + ) { + self.nickname = nickname + self.mainView = PopUpRegisterView() + + super.init() + + let reactor = PopUpStoreRegisterReactor( + adminUseCase: DIContainer.resolve(AdminUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self), + editingStore: editingStore + ) + + self.reactor = reactor + self.mainView.accountIdLabel.text = nickname + "님" + self.mainView.writerLabel.text = nickname + + if editingStore != nil { + self.mainView.pageTitleLabel.text = "팝업스토어 수정" + + // 편집 모드일 경우 스토어 상세 정보 로드 + if let storeId = editingStore?.id { + reactor.action.onNext(.loadStoreDetail(storeId)) + } + } + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - Life Cycle +extension PopUpStoreRegisterViewController { + override func viewDidLoad() { + super.viewDidLoad() + + self.addViews() + self.setupContstraints() + self.configureUI() + self.setupHandlers() + + if let reactor = self.reactor as? PopUpStoreRegisterReactor { + self.bind(reactor: reactor) + } + } +} + +// MARK: - Setup +private extension PopUpStoreRegisterViewController { + func addViews() { + self.view.addSubview(self.mainView) + } + + func setupContstraints() { + self.mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configureUI() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap)) + tapGesture.cancelsTouchesInView = false + self.view.addGestureRecognizer(tapGesture) + } + + func setupHandlers() { + // 뒤로가기 버튼 + self.mainView.backButton.addTarget(self, action: #selector(self.onBack), for: .touchUpInside) + + // 이미지 관련 버튼 + self.mainView.addImageButton.rx.tap + .bind { [weak self] in + self?.showImagePicker() + } + .disposed(by: self.disposeBag) + self.mainView.removeAllButton.rx.tap + .map { PopUpStoreRegisterReactor.Action.removeAllImages } + .subscribe(onNext: { [weak self] action in + (self?.reactor as? PopUpStoreRegisterReactor)?.action.onNext(action) + }) + .disposed(by: self.disposeBag) + + self.mainView.categoryButton.rx.tap + .bind { [weak self] in + self?.showCategoryPicker() + } + .disposed(by: self.disposeBag) + + self.mainView.periodButton.rx.tap + .bind { [weak self] in + self?.showDateRangePicker() + } + .disposed(by: self.disposeBag) + + self.mainView.timeButton.rx.tap + .bind { [weak self] in + self?.showTimeRangePicker() + } + .disposed(by: self.disposeBag) + + // 이미지 컬렉션뷰 핸들러 + self.mainView.imagesCollectionView.onMainImageToggled = { [weak self] index in + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.toggleMainImage(index)) + } + + self.mainView.imagesCollectionView.onImageDeleted = { [weak self] index in + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.removeImage(index)) + } + + // 저장 버튼 + self.mainView.saveButton.rx.tap + .map { PopUpStoreRegisterReactor.Action.save } + .subscribe(onNext: { [weak self] action in + (self?.reactor as? PopUpStoreRegisterReactor)?.action.onNext(action) + }) + .disposed(by: self.disposeBag) + + self.setupKeyboardHandling() + } + + func setupKeyboardHandling() { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + + self.mainView.scrollView.keyboardDismissMode = .interactive + } +} + +// MARK: - UI Interaction Methods +extension PopUpStoreRegisterViewController { + @objc private func handleTap() { + self.view.endEditing(true) + } + + @objc private func onBack() { + self.navigationController?.popViewController(animated: true) + } + + @objc private func keyboardWillShow(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + + let keyboardHeight = keyboardFrame.height + let contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: keyboardHeight, + right: 0 + ) + + self.mainView.scrollView.contentInset = contentInset + self.mainView.scrollView.scrollIndicatorInsets = contentInset + + // 현재 활성화된 필드가 키보드에 가려지는지 확인 + if let activeField = self.view.findFirstResponder() { + let activeRect = activeField.convert(activeField.bounds, to: self.mainView.scrollView) + let bottomOffset = activeRect.maxY + 20 // 여유 공간 + + if bottomOffset > (self.mainView.scrollView.frame.height - keyboardHeight) { + let scrollPoint = CGPoint( + x: 0, + y: bottomOffset - (self.mainView.scrollView.frame.height - keyboardHeight) + ) + self.mainView.scrollView.setContentOffset(scrollPoint, animated: true) + } + } + } + + @objc private func keyboardWillHide(_ notification: Notification) { + UIView.animate(withDuration: 0.3) { + self.mainView.scrollView.contentInset = .zero + self.mainView.scrollView.scrollIndicatorInsets = .zero + } + } + + private func showImagePicker() { + var configuration = PHPickerConfiguration() + configuration.filter = .images + configuration.selectionLimit = 0 // 무제한 + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + self.pickerViewController = picker + + self.present(picker, animated: true, completion: nil) + } + + private func showCategoryPicker() { + let categories = ["게임", "라이프스타일", "반려동물", "뷰티", "스포츠", "애니메이션", "엔터테인먼트", "여행", "예술", "음식/요리", "키즈", "패션"] + + let alertController = UIAlertController(title: "카테고리 선택", message: nil, preferredStyle: .actionSheet) + + for category in categories { + let action = UIAlertAction(title: category, style: .default) { [weak self] _ in + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.selectCategory(category)) + } + alertController.addAction(action) + } + + let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + + if let popoverController = alertController.popoverPresentationController { + popoverController.sourceView = self.mainView.categoryButton + popoverController.sourceRect = self.mainView.categoryButton.bounds + } + + self.present(alertController, animated: true, completion: nil) + } + + private func showDateRangePicker() { + DateTimePickerManager.shared.showDateRange(on: self) { [weak self] start, end in + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.selectDateRange(start: start, end: end)) + } + } + + private func showTimeRangePicker() { + DateTimePickerManager.shared.showTimeRange(on: self) { [weak self] start, end in + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.selectTimeRange(start: start, end: end)) + } + } + + private func updateCategoryButtonTitle(with category: String) { + self.mainView.categoryButton.setTitle("\(category) ▾", for: .normal) + } + + private func updatePeriodButtonTitle(start: Date, end: Date) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy.MM.dd" + let startString = dateFormatter.string(from: start) + let endString = dateFormatter.string(from: end) + + self.mainView.periodButton.setTitle("\(startString) ~ \(endString)", for: .normal) + } + + private func updateTimeButtonTitle(start: Date, end: Date) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + let startString = dateFormatter.string(from: start) + let endString = dateFormatter.string(from: end) + + self.mainView.timeButton.setTitle("\(startString) ~ \(endString)", for: .normal) + } + + private func showLoadingIndicator() { + // 로딩 인디케이터 표시 로직 구현 + // 예: Activity Indicator 또는 커스텀 로딩 뷰 표시 + } + + private func hideLoadingIndicator() { + // 로딩 인디케이터 숨김 로직 구현 + } + + private func showSuccessAlert(isUpdate: Bool) { + let message = isUpdate ? "팝업스토어가 성공적으로 수정되었습니다." : "팝업스토어가 성공적으로 등록되었습니다." + let alert = UIAlertController( + title: isUpdate ? "수정 성공" : "등록 성공", + message: message, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "확인", style: .default) { [weak self] _ in + self?.completionHandler?() // 목록 새로고침 + self?.navigationController?.popViewController(animated: true) + }) + self.present(alert, animated: true) + } + + private func showErrorAlert(message: String) { + let alert = UIAlertController( + title: "오류", + message: message, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "확인", style: .default)) + self.present(alert, animated: true) + } +} + +// MARK: - ReactorKit Binding +extension PopUpStoreRegisterViewController: View { + typealias Reactor = PopUpStoreRegisterReactor + + func bind(reactor: Reactor) { + // MARK: - Input (View -> Reactor) + // 텍스트 필드 바인딩 + self.mainView.nameField.rx.text.orEmpty + .distinctUntilChanged() + .map { Reactor.Action.updateName($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + self.mainView.addressField.rx.text.orEmpty + .distinctUntilChanged() + .map { Reactor.Action.updateAddress($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + self.mainView.latField.rx.text.orEmpty + .distinctUntilChanged() + .map { Reactor.Action.updateLat($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + self.mainView.lonField.rx.text.orEmpty + .distinctUntilChanged() + .map { Reactor.Action.updateLon($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + self.mainView.descriptionTextView.rx.text.orEmpty + .distinctUntilChanged() + .map { Reactor.Action.updateDescription($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // 주소 변경 시 지오코딩 요청 + self.mainView.addressField.rx.text.orEmpty + .distinctUntilChanged() + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .filter { !$0.isEmpty } + .map { Reactor.Action.geocodeAddress($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // MARK: - Output (Reactor -> View) + // 저장 버튼 활성화 상태 + reactor.state.map { $0.isSaveEnabled } + .distinctUntilChanged() + .bind(onNext: { [weak self] isEnabled in + self?.mainView.saveButton.isEnabled = isEnabled + self?.mainView.saveButton.backgroundColor = isEnabled ? .systemBlue : .lightGray + }) + .disposed(by: self.disposeBag) + + // 로딩 상태 + reactor.state.map { $0.isLoading } + .distinctUntilChanged() + .bind(onNext: { [weak self] isLoading in + if isLoading { + self?.showLoadingIndicator() + } else { + self?.hideLoadingIndicator() + } + }) + .disposed(by: self.disposeBag) + + // 에러 메시지 + reactor.state.map { $0.errorMessage } + .distinctUntilChanged() + .compactMap { $0 } + .bind(onNext: { [weak self] message in + self?.showErrorAlert(message: message) + reactor.action.onNext(.clearError) + }) + .disposed(by: self.disposeBag) + + // 성공 상태 + reactor.state.map { $0.isSuccess } + .distinctUntilChanged() + .filter { $0 } + .bind(onNext: { [weak self] _ in + self?.showSuccessAlert(isUpdate: reactor.currentState.isEditMode) + reactor.action.onNext(.dismissSuccess) + }) + .disposed(by: self.disposeBag) + + // 이미지 목록 업데이트 + reactor.state.map { $0.images } + .distinctUntilChanged() + .bind(onNext: { [weak self] images in + self?.mainView.imagesCollectionView.updateImages(images) + }) + .disposed(by: self.disposeBag) + + // 필드 값 업데이트 + reactor.state.map { $0.name } + .distinctUntilChanged() + .bind(onNext: { [weak self] name in + if self?.mainView.nameField.text != name { + self?.mainView.nameField.text = name + } + }) + .disposed(by: self.disposeBag) + + reactor.state.map { $0.address } + .distinctUntilChanged() + .bind(onNext: { [weak self] address in + if self?.mainView.addressField.text != address { + self?.mainView.addressField.text = address + } + }) + .disposed(by: self.disposeBag) + + reactor.state.map { $0.lat } + .distinctUntilChanged() + .bind(onNext: { [weak self] lat in + if self?.mainView.latField.text != lat { + self?.mainView.latField.text = lat + } + }) + .disposed(by: self.disposeBag) + + reactor.state.map { $0.lon } + .distinctUntilChanged() + .bind(onNext: { [weak self] lon in + if self?.mainView.lonField.text != lon { + self?.mainView.lonField.text = lon + } + }) + .disposed(by: self.disposeBag) + + reactor.state.map { $0.description } + .distinctUntilChanged() + .bind(onNext: { [weak self] description in + if self?.mainView.descriptionTextView.text != description { + self?.mainView.descriptionTextView.text = description + } + }) + .disposed(by: self.disposeBag) + + reactor.state.map { $0.category } + .distinctUntilChanged() + .filter { !$0.isEmpty } + .bind(onNext: { [weak self] category in + self?.updateCategoryButtonTitle(with: category) + }) + .disposed(by: self.disposeBag) + + // 날짜 범위 업데이트 + let dateRangeObservable = reactor.state + .compactMap { state -> (Date, Date)? in + guard let start = state.selectedStartDate, + let end = state.selectedEndDate else { + return nil + } + return (start, end) + } + + dateRangeObservable + .distinctUntilChanged { prev, current in + return prev.0 == current.0 && prev.1 == current.1 + } + .bind(onNext: { [weak self] dateRange in + self?.updatePeriodButtonTitle(start: dateRange.0, end: dateRange.1) + }) + .disposed(by: self.disposeBag) + + // 시간 범위 업데이트 + let timeRangeObservable = reactor.state + .compactMap { state -> (Date, Date)? in + guard let start = state.selectedStartTime, + let end = state.selectedEndTime else { + return nil + } + return (start, end) + } + + timeRangeObservable + .distinctUntilChanged { prev, current in + return prev.0 == current.0 && prev.1 == current.1 + } + .bind(onNext: { [weak self] timeRange in + self?.updateTimeButtonTitle(start: timeRange.0, end: timeRange.1) + }) + .disposed(by: self.disposeBag) + + } +} + +// MARK: - PHPickerViewController Delegate +extension PopUpStoreRegisterViewController: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + guard !results.isEmpty else { return } + + let itemProviders = results.map(\.itemProvider) + let dispatchGroup = DispatchGroup() + var newImages = [ExtendedImage]() + + // 이미 로드된 이미지 경로 목록 (중복 방지) + let existingPaths = Set((self.reactor as? PopUpStoreRegisterReactor)?.currentState.images.map { $0.filePath } ?? []) + + for (index, provider) in itemProviders.enumerated() { + if provider.canLoadObject(ofClass: UIImage.self) { + dispatchGroup.enter() + provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in + defer { dispatchGroup.leave() } + guard let image = object as? UIImage else { return } + + let name = (self?.reactor as? PopUpStoreRegisterReactor)?.currentState.name ?? "unnamed" + let uuid = UUID().uuidString + let filePath = "PopUpImage/\(name)/\(uuid)/\(index).jpg" + + // 이미 같은 경로가 있는지 확인 + if !existingPaths.contains(filePath) { + let extended = ExtendedImage( + filePath: filePath, + image: image, + isMain: false + ) + newImages.append(extended) + } + } + } + } + + dispatchGroup.notify(queue: .main) { [weak self] in + if !newImages.isEmpty { + guard let reactor = self?.reactor as? PopUpStoreRegisterReactor else { return } + reactor.action.onNext(.addImages(newImages)) + } + } + } +} +extension UIView { + func findFirstResponder() -> UIView? { + if self.isFirstResponder { + return self + } + + for subview in self.subviews { + if let firstResponder = subview.findFirstResponder() { + return firstResponder + } + } + + return nil + } +} diff --git a/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminStoreCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminStoreCell.swift index 706addaf..fca9ddfa 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminStoreCell.swift @@ -1,6 +1,11 @@ import UIKit -import SnapKit + +import DomainInterface +import Infrastructure + import RxSwift +import SnapKit + final class AdminStoreCell: UITableViewCell { private let disposeBag = DisposeBag() @@ -75,16 +80,15 @@ final class AdminStoreCell: UITableViewCell { } // MARK: - Configure - func configure(with store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { - Logger.log(message: "셀 데이터 바인딩: \(store)", category: .debug) + func configure(with store: AdminStore) { + Logger.log("셀 데이터 바인딩: \(store)", category: .debug) titleLabel.text = store.name categoryLabel.text = store.categoryName statusChip.text = "운영" - // mainImageUrl에서 baseURL 부분 제거 - let imagePath = store.mainImageUrl.replacingOccurrences(of: Secrets.popPoolS3BaseURL.rawValue, with: "") - Logger.log(message: "이미지 경로: \(imagePath)", category: .debug) + let imagePath = store.mainImageUrl.replacingOccurrences(of: Secrets.popPoolS3BaseURL, with: "") + Logger.log("이미지 경로: \(imagePath)", category: .debug) storeImageView.setPPImage(path: imagePath) } } diff --git a/Poppool/Poppool/Presentation/Admin/AdminView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift similarity index 99% rename from Poppool/Poppool/Presentation/Admin/AdminView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift index 3ec4149e..7393cd2a 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminView.swift @@ -1,4 +1,7 @@ import UIKit + +import DesignSystem + import SnapKit import Then @@ -18,7 +21,7 @@ final class AdminView: UIView { let usernameLabel = PPLabel( style: .bold, fontSize: 14, - text: "" + text: "" ) let menuButton = UIButton(type: .system).then { diff --git a/Poppool/Poppool/Presentation/Admin/AdminViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift similarity index 72% rename from Poppool/Poppool/Presentation/Admin/AdminViewController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift index 24e86807..c599adae 100644 --- a/Poppool/Poppool/Presentation/Admin/AdminViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/AdminViewController.swift @@ -1,7 +1,12 @@ import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class AdminViewController: BaseViewController, View { @@ -16,7 +21,10 @@ final class AdminViewController: BaseViewController, View { private let adminUseCase: AdminUseCase // MARK: - Init - init(nickname: String, adminUseCase: AdminUseCase = DefaultAdminUseCase(repository: DefaultAdminRepository(provider: ProviderImpl()))) { + init( + nickname: String, + adminUseCase: AdminUseCase + ) { self.nickname = nickname self.adminUseCase = adminUseCase self.mainView = AdminView(frame: .zero) @@ -122,7 +130,6 @@ final class AdminViewController: BaseViewController, View { alert.addAction(UIAlertAction(title: "취소", style: .cancel)) - // iPad support if let popoverController = alert.popoverPresentationController { popoverController.sourceView = mainView.menuButton popoverController.sourceRect = mainView.menuButton.bounds @@ -151,7 +158,7 @@ final class AdminViewController: BaseViewController, View { present(alert, animated: true) } - private func showDeleteConfirmation(for store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { + private func showDeleteConfirmation(for store: AdminStore) { let alert = UIAlertController( title: "삭제 확인", message: "\(store.name)을(를) 삭제하시겠습니까?", @@ -166,37 +173,70 @@ final class AdminViewController: BaseViewController, View { present(alert, animated: true) } - private func editStore(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { - let registerVC = PopUpStoreRegisterViewController( - nickname: nickname, - adminUseCase: adminUseCase, - editingStore: store - ) + private func editStore(_ store: AdminStore) { + adminUseCase.fetchStoreDetail(id: store.id) + .observe(on: MainScheduler.instance) + .subscribe( + onNext: { [weak self] _ in + guard let self = self else { return } + let registerVC = PopUpStoreRegisterViewController( + nickname: self.nickname, + editingStore: store + ) - // 수정할 때도 completionHandler 추가 - registerVC.completionHandler = { [weak self] in - self?.reactor?.action.onNext(.reloadData) - } + registerVC.completionHandler = { [weak self] in + self?.reactor?.action.onNext(.reloadData) + } - navigationController?.pushViewController(registerVC, animated: true) + self.navigationController?.pushViewController(registerVC, animated: true) + }, + onError: { [weak self] error in + self?.showErrorAlert(message: "스토어 정보 조회 실패: \(error.localizedDescription)") + } + ) + .disposed(by: disposeBag) } - private func deleteStore(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) { - let imageService = PreSignedService() - - imageService.tryDelete(targetPaths: .init(objectKeyList: [store.mainImageUrl])) - .andThen(adminUseCase.deleteStore(id: store.id)) + private func deleteStore(_ store: AdminStore) { + adminUseCase.fetchStoreDetail(id: store.id) .observe(on: MainScheduler.instance) .subscribe( - onNext: { [weak self] _ in - self?.reactor?.action.onNext(.reloadData) - ToastMaker.createToast(message: "삭제되었습니다") + onNext: { [weak self] storeDetail in + guard let self = self else { return } + + var allImageUrls = [String]() + + allImageUrls.append(storeDetail.mainImageUrl) + + // 다른 모든 이미지 URL 추가 + let otherImageUrls = storeDetail.images.map { $0.imageUrl } + allImageUrls.append(contentsOf: otherImageUrls) + + allImageUrls = Array(Set(allImageUrls)) + + Logger.log("삭제할 이미지: \(allImageUrls.count)개", category: .debug) + + @Dependency var preSignedUseCase: PreSignedUseCase + preSignedUseCase.tryDelete(objectKeyList: allImageUrls) + .andThen(self.adminUseCase.deleteStore(id: store.id)) + .observe(on: MainScheduler.instance) + .subscribe( + onCompleted: { [weak self] in + self?.reactor?.action.onNext(.reloadData) + ToastMaker.createToast(message: "삭제되었습니다") + }, + onError: { [weak self] error in + self?.showErrorAlert(message: "삭제 실패: \(error.localizedDescription)") + } + ) + .disposed(by: self.disposeBag) }, onError: { [weak self] error in - self?.showErrorAlert(message: "삭제 실패: \(error.localizedDescription)") + self?.showErrorAlert(message: "스토어 정보 조회 실패: \(error.localizedDescription)") } ) .disposed(by: disposeBag) + } private func showErrorAlert(message: String) { let alert = UIAlertController( @@ -220,10 +260,7 @@ final class AdminViewController: BaseViewController, View { mainView.registerButton.rx.tap .subscribe(onNext: { [weak self] _ in guard let self = self else { return } - let registerVC = PopUpStoreRegisterViewController( - nickname: self.nickname, - adminUseCase: self.adminUseCase - ) + let registerVC = PopUpStoreRegisterViewController(nickname: self.nickname) registerVC.completionHandler = { [weak self] in self?.reactor?.action.onNext(.reloadData) @@ -238,11 +275,10 @@ final class AdminViewController: BaseViewController, View { .compactMap { $0 } .subscribe(onNext: { [weak self] store in guard let self = self else { return } - self.editStore(store) + self.editStore(store) }) .disposed(by: disposeBag) - reactor.state.map { $0.storeList } .map { "총 \($0.count)개" } .bind(to: mainView.popupCountLabel.rx.text) @@ -252,8 +288,8 @@ final class AdminViewController: BaseViewController, View { .bind(to: mainView.tableView.rx.items( cellIdentifier: AdminStoreCell.identifier, cellType: AdminStoreCell.self - )) { _, item, cell in - cell.configure(with: item) + )) { _, store, cell in + cell.configure(with: store) } .disposed(by: disposeBag) } diff --git a/Poppool/Poppool/Presentation/Admin/ImageCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/ImageCell.swift similarity index 98% rename from Poppool/Poppool/Presentation/Admin/ImageCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/ImageCell.swift index 98f6fbf1..dbf1da59 100644 --- a/Poppool/Poppool/Presentation/Admin/ImageCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Admin/ImageCell.swift @@ -1,9 +1,10 @@ import UIKit + +import DesignSystem + import SnapKit final class ImageCell: UICollectionViewCell { - static let identifier = "ImageCell" - // UI private let thumbnailImageView = UIImageView().then { $0.contentMode = .scaleAspectFill diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift index 3bee6c71..68fd16d9 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift @@ -1,25 +1,20 @@ -// -// CommentCheckController.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentCheckController: BaseViewController, View { - + typealias Reactor = CommentCheckReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentCheckView() } @@ -48,7 +43,7 @@ extension CommentCheckController { .map { Reactor.Action.continueButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.stopButton.rx.tap .map { Reactor.Action.stopButtonTapped } .bind(to: reactor.action) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift index 4afbec9d..9653a396 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift @@ -6,41 +6,41 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentCheckReactor: Reactor { - + // MARK: - Reactor enum Action { case continueButtonTapped case stopButtonTapped } - + enum Mutation { case setSelectedType(type: SelectedType) } - + struct State { var selectedType: SelectedType = .none } - + enum SelectedType { case none case continues case stop } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +50,7 @@ final class CommentCheckReactor: Reactor { return Observable.just(.setSelectedType(type: .stop)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift similarity index 79% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift index 51d6458e..610435fd 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift @@ -1,51 +1,43 @@ -// -// CommentCheckView.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentCheckView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "코멘트 작성을 그만하시겠어요?") - return label + return PPLabel(style: .bold, fontSize: 18, text: "코멘트 작성을 그만하시겠어요?") }() - + private let descriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14, text: "화면을 나가실 경우 작성중인 내용은 저장되지 않아요.") label.textColor = .g600 return label }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + let continueButton: PPButton = { - let button = PPButton(style: .secondary, text: "계속하기") - return button + return PPButton(style: .secondary, text: "계속하기") }() - + let stopButton: PPButton = { - let button = PPButton(style: .primary, text: "그만하기") - return button + return PPButton(style: .primary, text: "그만하기") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,20 +45,20 @@ final class CommentCheckView: UIView { // MARK: - SetUp private extension CommentCheckView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + buttonStackView.addArrangedSubview(continueButton) buttonStackView.addArrangedSubview(stopButton) self.addSubview(buttonStackView) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift index e4fb63a0..aec59f84 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift @@ -1,29 +1,24 @@ -// -// CommentDetailController.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentDetailController: BaseViewController, View { - + typealias Reactor = CommentDetailReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = CommentDetailView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -47,16 +42,16 @@ private extension CommentDetailController { DetailCommentImageCell.self, forCellWithReuseIdentifier: DetailCommentImageCell.identifiers ) - + mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( CommentDetailContentSectionCell.self, forCellWithReuseIdentifier: CommentDetailContentSectionCell.identifiers ) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) @@ -67,12 +62,11 @@ private extension CommentDetailController { // MARK: - Methods extension CommentDetailController { func bind(reactor: Reactor) { - rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, indexPath) in @@ -80,19 +74,19 @@ extension CommentDetailController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.likeButton.rx.tap .map { Reactor.Action.likeButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.mainView.profileView.dateLabel.text = state.commentData.date owner.mainView.profileView.nickNameLabel.text = state.commentData.nickName owner.mainView.profileView.profileImageView.setPPImage(path: state.commentData.profileImagePath) - owner.mainView.likeButtonTitleLabel.setLineHeightText(text: "도움돼요 \(state.commentData.likeCount)", font: .KorFont(style: .medium, size: 13)) + owner.mainView.likeButtonTitleLabel.setLineHeightText(text: "도움돼요 \(state.commentData.likeCount)", font: .korFont(style: .medium, size: 13)) if state.commentData.isLike { owner.mainView.likeButtonImageView.image = UIImage(named: "icon_like_blue") owner.mainView.likeButtonTitleLabel.textColor = .blu500 @@ -112,19 +106,18 @@ extension CommentDetailController: UICollectionViewDelegate, UICollectionViewDat func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let _ = collectionView.cellForItem(at: indexPath) as? DetailCommentImageCell { cellTapped.onNext(indexPath) @@ -139,11 +132,11 @@ extension CommentDetailController: PanModalPresentable { var longFormHeight: PanModalHeight { return .contentHeight(UIScreen.main.bounds.height - 68) } - + var shortFormHeight: PanModalHeight { return .contentHeight(UIScreen.main.bounds.height - 68) } - + var showDragIndicator: Bool { return false } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift index 978cb6bf..c5a24de3 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift @@ -1,43 +1,40 @@ -// -// CommentDetailReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentDetailReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case imageCellTapped(controller: BaseViewController, row: Int) case likeButtonTapped } - + enum Mutation { case loadView case presentImageDetailView(controller: BaseViewController, row: Int) case likeChange } - + struct State { var commentData: DetailCommentSection.CellType.Input var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -51,17 +48,21 @@ final class CommentDetailReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var imageSection = CommentDetailImageSection(inputDataList: []) private var contentSection = CommentDetailContentSection(inputDataList: []) - + private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init - init(comment: DetailCommentSection.CellType.Input) { + init( + comment: DetailCommentSection.CellType.Input, + userAPIUseCase: UserAPIUseCase + ) { self.initialState = State(commentData: comment) + self.userAPIUseCase = userAPIUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -73,7 +74,7 @@ final class CommentDetailReactor: Reactor { return Observable.just(.presentImageDetailView(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -93,26 +94,26 @@ final class CommentDetailReactor: Reactor { if newState.commentData.isLike { newState.commentData.likeCount += 1 userAPIUseCase.postCommentLike(commentId: newState.commentData.commentID) - .subscribe(onDisposed: { - Logger.log(message: "CommentLike", category: .info) + .subscribe(onDisposed: { + Logger.log("CommentLike", category: .info) }) .disposed(by: disposeBag) } else { newState.commentData.likeCount -= 1 userAPIUseCase.deleteCommentLike(commentId: newState.commentData.commentID) - .subscribe(onDisposed: { - Logger.log(message: "CommentLikeDelete", category: .info) + .subscribe(onDisposed: { + Logger.log("CommentLikeDelete", category: .info) }) .disposed(by: disposeBag) } newState.sections = getSection() } - + return newState } - + func getSection() -> [any Sectionable] { - if imageSection.isEmpty { + if imageSection.isEmpty { return [ contentSection ] diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift index 1bb2c50e..49dbefca 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift @@ -1,26 +1,21 @@ -// -// CommentDetailContentSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem + import RxSwift struct CommentDetailContentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = CommentDetailContentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct CommentDetailContentSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift index 17650a73..96bea9e7 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift @@ -1,17 +1,12 @@ -// -// CommentDetailContentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class CommentDetailContentSectionCell: UICollectionViewCell { - + // MARK: - Components private let contentLabel: PPLabel = { @@ -19,15 +14,15 @@ final class CommentDetailContentSectionCell: UICollectionViewCell { label.numberOfLines = 0 return label }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -47,8 +42,8 @@ extension CommentDetailContentSectionCell: Inputable { struct Input { var content: String? } - + func injection(with input: Input) { - contentLabel.setLineHeightText(text: input.content, font: .KorFont(style: .medium, size: 13)) + contentLabel.setLineHeightText(text: input.content, font: .korFont(style: .medium, size: 13)) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift index a2644557..d307848f 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift @@ -1,26 +1,21 @@ -// -// CommentDetailImageSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem + import RxSwift struct CommentDetailImageSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailCommentImageCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(80), diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift index 6bf879f4..efbe9281 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift @@ -1,51 +1,44 @@ -// -// CommentDetailView.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentDetailView: UIView { - + // MARK: - Components let profileView: DetailCommentProfileView = { let view = DetailCommentProfileView() view.button.isHidden = true return view }() - + let likeButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let likeButtonTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13, text: "도움돼요") label.textColor = .g400 return label }() - + let likeButtonImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_like_gray") return view }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,32 +46,32 @@ final class CommentDetailView: UIView { // MARK: - SetUp private extension CommentDetailView { - + func setUpConstraints() { self.addSubview(profileView) profileView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) make.top.equalToSuperview().inset(40) } - + likeButton.addSubview(likeButtonTitleLabel) likeButtonTitleLabel.snp.makeConstraints { make in make.height.equalTo(20).priority(.high) make.top.bottom.trailing.equalToSuperview() } - + likeButton.addSubview(likeButtonImageView) likeButtonImageView.snp.makeConstraints { make in make.size.equalTo(20) make.leading.centerY.equalToSuperview() make.trailing.equalTo(likeButtonTitleLabel.snp.leading) } - + self.addSubview(likeButton) likeButton.snp.makeConstraints { make in make.bottom.trailing.equalToSuperview().inset(20) } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(profileView.snp.bottom).offset(16) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift index 3dcc05c4..5d672f41 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift @@ -1,25 +1,20 @@ -// -// CommentMyMenuController.swift -// Poppool -// -// Created by SeoJunYoung on 2/1/25. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentMyMenuController: BaseViewController, View { - + typealias Reactor = CommentMyMenuReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentMyMenuView() } @@ -50,14 +45,14 @@ extension CommentMyMenuController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.commentRemoveButton.rx.tap .map { _ in Reactor.Action.removeButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.commentEditButton.rx.tap .map { _ in Reactor.Action.editButtonTapped diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift index 25324ff7..caebe2ca 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift @@ -6,28 +6,28 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentMyMenuReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped case removeButtonTapped case editButtonTapped } - + enum Mutation { case moveToRecentScene case setRemoveType case setEditType } - + struct State { var selectedType: SelectedType = .none } - + enum SelectedType { case none case cancel @@ -35,15 +35,15 @@ final class CommentMyMenuReactor: Reactor { case edit } // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(nickName: String?) { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -55,7 +55,7 @@ final class CommentMyMenuReactor: Reactor { return Observable.just(.setEditType) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift index 59563034..96d8daa8 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift @@ -1,59 +1,54 @@ -// -// CommentMyMenuView.swift -// Poppool -// -// Created by SeoJunYoung on 2/1/25. -// - import UIKit +import DesignSystem + import SnapKit final class CommentMyMenuView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18) - label.setLineHeightText(text: "내가 작성한 코멘트", font: .KorFont(style: .bold, size: 18)) + label.setLineHeightText(text: "내가 작성한 코멘트", font: .korFont(style: .bold, size: 18)) return label }() - + let cancelButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let commentRemoveButton: UIButton = { let button = UIButton() button.setTitle("코멘트 삭제하기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + private let lineView: UIView = { let view = UIView() view.backgroundColor = .g50 return view }() - + let commentEditButton: UIButton = { let button = UIButton() button.setTitle("코멘트 수정하기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,35 +56,35 @@ final class CommentMyMenuView: UIView { // MARK: - SetUp private extension CommentMyMenuView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.equalToSuperview().inset(20) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(commentRemoveButton) commentRemoveButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(57) } - + self.addSubview(lineView) lineView.snp.makeConstraints { make in make.top.equalTo(commentRemoveButton.snp.bottom) make.height.equalTo(1) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(commentEditButton) commentEditButton.snp.makeConstraints { make in make.top.equalTo(lineView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift index 8c0ffa1d..d5adc11a 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift @@ -1,25 +1,20 @@ -// -// CommentUserInfoController.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentUserInfoController: BaseViewController, View { - + typealias Reactor = CommentUserInfoReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentUserInfoView() } @@ -50,27 +45,27 @@ extension CommentUserInfoController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.normalCommentButton.rx.tap .map { _ in Reactor.Action.normalButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.instaCommentButton.rx.tap .map { _ in Reactor.Action.instaButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.mainView.titleLabel.setLineHeightText( text: "\(state.nickName ?? "")님에 대해 더 알아보기", - font: .KorFont(style: .bold, size: 18) + font: .korFont(style: .bold, size: 18) ) } .disposed(by: disposeBag) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift index 9b12bdb6..76562c2b 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift @@ -6,29 +6,29 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentUserInfoReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped case normalButtonTapped case instaButtonTapped } - + enum Mutation { case moveToRecentScene case moveToCommentScene case moveToBlockScene } - + struct State { var selectedType: SelectedType = .none var nickName: String? } - + enum SelectedType { case none case cancel @@ -36,15 +36,15 @@ final class CommentUserInfoReactor: Reactor { case block } // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(nickName: String?) { self.initialState = State(nickName: nickName) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -56,7 +56,7 @@ final class CommentUserInfoReactor: Reactor { return Observable.just(.moveToBlockScene) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift index 25356672..4ace72f5 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift @@ -1,59 +1,54 @@ -// -// CommentUserInfoView.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentUserInfoView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18) - label.setLineHeightText(text: "님에 대해 더 알아보기", font: .KorFont(style: .bold, size: 18)) + label.setLineHeightText(text: "님에 대해 더 알아보기", font: .korFont(style: .bold, size: 18)) return label }() - + let cancelButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let normalCommentButton: UIButton = { let button = UIButton() button.setTitle("코멘트를 작성한 팝업 모두보기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + private let lineView: UIView = { let view = UIView() view.backgroundColor = .g50 return view }() - + let instaCommentButton: UIButton = { let button = UIButton() button.setTitle("이 유저 차단하기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,35 +56,35 @@ final class CommentUserInfoView: UIView { // MARK: - SetUp private extension CommentUserInfoView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.equalToSuperview().inset(20) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(normalCommentButton) normalCommentButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(57) } - + self.addSubview(lineView) lineView.snp.makeConstraints { make in make.top.equalTo(normalCommentButton.snp.bottom) make.height.equalTo(1) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(instaCommentButton) instaCommentButton.snp.makeConstraints { make in make.top.equalTo(lineView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListController.swift index 14a2f784..574bc41a 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListController.swift @@ -1,24 +1,19 @@ -// -// CommentListController.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class CommentListController: BaseViewController, View { - + typealias Reactor = CommentListReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentListView() private var sections: [any Sectionable] = [] private let scrollObserver: PublishSubject = .init() @@ -30,7 +25,7 @@ extension CommentListController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -58,18 +53,18 @@ private extension CommentListController { // MARK: - Methods extension CommentListController { func bind(reactor: Reactor) { - + scrollObserver .throttle(.seconds(1), scheduler: MainScheduler.instance) .map { Reactor.Action.scrollDidEndPoint } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -77,7 +72,7 @@ extension CommentListController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -85,7 +80,7 @@ extension CommentListController { if state.isReloadView { owner.mainView.contentCollectionView.reloadData()} } .disposed(by: disposeBag) - + } } @@ -94,11 +89,11 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -113,7 +108,7 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS } .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.profileView.button.rx.tap .withUnretained(self) .map { (owner, _) in @@ -121,7 +116,7 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS } .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.totalViewButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -129,7 +124,7 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS } .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.likeButton.rx.tap .map { Reactor.Action.likeButtonTapped(row: indexPath.row) } .bind(to: reactor.action) @@ -137,7 +132,7 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListReactor.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListReactor.swift index d62b4b1c..d673ae97 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/CommentListReactor.swift @@ -1,18 +1,15 @@ -// -// CommentListReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentListReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -24,7 +21,7 @@ final class CommentListReactor: Reactor { case profileButtonTapped(controller: BaseViewController, row: Int) case detailSceneLikeButtonTapped(row: Int) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) @@ -33,26 +30,26 @@ final class CommentListReactor: Reactor { case presentImageScene(controller: BaseViewController, commentRow: Int, imageRow: Int) case presentCommentMenuScene(controller: BaseViewController, row: Int) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let popUpID: Int64 private let popUpName: String? private var page: Int32 = 0 private var appendDataIsEmpty: Bool = false - - private var imageService = PreSignedService() - private let popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - private let commentAPIUseCase = CommentAPIUseCaseImpl(repository: CommentAPIRepository(provider: ProviderImpl())) - + + private let preSignedUseCase: PreSignedUseCase + private let popUpAPIUseCase: PopUpAPIUseCase + private let userAPIUseCase: UserAPIUseCase + private let commentAPIUseCase: CommentAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -66,19 +63,30 @@ final class CommentListReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var commentTitleSection = CommentListTitleSection(inputDataList: []) private var commentSection = DetailCommentSection(inputDataList: []) - + private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) private let spacing28Section = SpacingSection(inputDataList: [.init(spacing: 28)]) // MARK: - init - init(popUpID: Int64, popUpName: String?) { + init( + popUpID: Int64, + popUpName: String?, + userAPIUseCase: UserAPIUseCase, + popUpAPIUseCase: PopUpAPIUseCase, + commentAPIUseCase: CommentAPIUseCase, + preSignedUseCase: PreSignedUseCase + ) { self.initialState = State() self.popUpID = popUpID self.popUpName = popUpName + self.userAPIUseCase = userAPIUseCase + self.popUpAPIUseCase = popUpAPIUseCase + self.commentAPIUseCase = commentAPIUseCase + self.preSignedUseCase = preSignedUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -173,7 +181,7 @@ final class CommentListReactor: Reactor { return Observable.just(.loadView) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -188,7 +196,10 @@ final class CommentListReactor: Reactor { case .presentDetailScene(let controller, let row): let comment = commentSection.inputDataList[row] let nextController = CommentDetailController() - nextController.reactor = CommentDetailReactor(comment: comment) + nextController.reactor = CommentDetailReactor( + comment: comment, + userAPIUseCase: userAPIUseCase + ) nextController.mainView.likeButton.rx.tap .map { Action.detailSceneLikeButtonTapped(row: row)} .bind(to: action) @@ -207,11 +218,11 @@ final class CommentListReactor: Reactor { } else { showOtherUserCommentMenu(controller: controller, comment: comment) } - + } return newState } - + func getSection() -> [any Sectionable] { return [ spacing24Section, @@ -220,7 +231,7 @@ final class CommentListReactor: Reactor { commentSection ] } - + func showOtherUserCommentMenu(controller: BaseViewController, comment: DetailCommentSection.CellType.Input) { let nextController = CommentUserInfoController() nextController.reactor = CommentUserInfoReactor(nickName: comment.nickName) @@ -233,7 +244,10 @@ final class CommentListReactor: Reactor { case .normal: owner.dismiss(animated: true) { [weak controller] in let otherUserCommentController = OtherUserCommentController() - otherUserCommentController.reactor = OtherUserCommentReactor(commenterID: comment.creator) + otherUserCommentController.reactor = OtherUserCommentReactor( + commenterID: comment.creator, + userAPIUseCase: self.userAPIUseCase + ) controller?.navigationController?.pushViewController(otherUserCommentController, animated: true) } case .block: @@ -250,7 +264,7 @@ final class CommentListReactor: Reactor { case .block: ToastMaker.createToast(message: "\(comment.nickName ?? "")을 차단했어요") self.userAPIUseCase.postUserBlock(blockedUserId: comment.creator) - .subscribe(onDisposed: { + .subscribe(onDisposed: { blockController.dismiss(animated: true) }) .disposed(by: self.disposeBag) @@ -268,13 +282,12 @@ final class CommentListReactor: Reactor { }) .disposed(by: disposeBag) } - + func showMyCommentMenu(controller: BaseViewController, comment: DetailCommentSection.CellType.Input) { let nextController = CommentMyMenuController() nextController.reactor = CommentMyMenuReactor(nickName: comment.nickName) - imageService = PreSignedService() controller.presentPanModal(nextController) - + nextController.reactor?.state .withUnretained(nextController) .subscribe(onNext: { [weak self] (owner, state) in @@ -287,18 +300,24 @@ final class CommentListReactor: Reactor { ToastMaker.createToast(message: "작성한 코멘트를 삭제했어요") }) .disposed(by: self.disposeBag) - + let commentList = comment.imageList.compactMap { $0 } - self.imageService.tryDelete(targetPaths: .init(objectKeyList: commentList)) - .subscribe { - Logger.log(message: "S3 Image Delete 완료", category: .info) - } + self.preSignedUseCase.tryDelete(objectKeyList: commentList) + .subscribe(onDisposed: { + Logger.log("S3 Image Delete 완료", category: .info) + }) .disposed(by: self.disposeBag) case .edit: owner.dismiss(animated: true) { [weak controller] in guard let popUpName = self.popUpName else { return } let editController = NormalCommentEditController() - editController.reactor = NormalCommentEditReactor(popUpID: self.popUpID, popUpName: popUpName, comment: comment) + editController.reactor = NormalCommentEditReactor( + popUpID: self.popUpID, + popUpName: popUpName, + comment: comment, + commentAPIUseCase: self.commentAPIUseCase, + preSignedUseCase: self.preSignedUseCase + ) controller?.navigationController?.pushViewController(editController, animated: true) } case .cancel: diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift index 18057b37..9e5ee0ae 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift @@ -1,27 +1,21 @@ -// -// CommentListTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - - import UIKit +import DesignSystem + import RxSwift struct CommentListTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = CommentListTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -38,7 +32,7 @@ struct CommentListTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift index 708c4b59..a4d6fd3d 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift @@ -1,17 +1,12 @@ -// -// CommentListTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class CommentListTitleSectionCell: UICollectionViewCell { - + // MARK: - Components private let countLabel: PPLabel = { @@ -21,12 +16,12 @@ final class CommentListTitleSectionCell: UICollectionViewCell { }() let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -47,8 +42,8 @@ extension CommentListTitleSectionCell: Inputable { var count: Int var unit: String = "개" } - + func injection(with input: Input) { - countLabel.setLineHeightText(text: "총 \(input.count)\(input.unit)", font: .KorFont(style: .regular, size: 13)) + countLabel.setLineHeightText(text: "총 \(input.count)\(input.unit)", font: .korFont(style: .regular, size: 13)) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListView.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListView.swift index faa57203..2a8a2735 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentList/View/CommentListView.swift @@ -1,33 +1,26 @@ -// -// CommentListView.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentListView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { - let view = PPReturnHeaderView() - return view + return PPReturnHeaderView() }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,14 +28,14 @@ final class CommentListView: UIView { // MARK: - SetUp private extension CommentListView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift index 603c2dcd..03495b7b 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift @@ -1,25 +1,20 @@ -// -// CommentSelectedController.swift -// Poppool -// -// Created by SeoJunYoung on 12/13/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentSelectedController: BaseViewController, View { - + typealias Reactor = CommentSelectedReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentSelectedView() } @@ -50,14 +45,14 @@ extension CommentSelectedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.normalCommentButton.rx.tap .map { _ in Reactor.Action.normalButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.instaCommentButton.rx.tap .map { _ in Reactor.Action.instaButtonTapped diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift index 9ca6bd7a..271a84b6 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift @@ -6,28 +6,28 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentSelectedReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped case normalButtonTapped case instaButtonTapped } - + enum Mutation { case moveToRecentScene case moveToNormalScene case moveToInstaScene } - + struct State { var selectedType: SelectedType = .none } - + enum SelectedType { case none case cancel @@ -35,15 +35,15 @@ final class CommentSelectedReactor: Reactor { case insta } // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -55,7 +55,7 @@ final class CommentSelectedReactor: Reactor { return Observable.just(.moveToInstaScene) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift index 9ca4bc6e..0c6b341d 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift @@ -1,59 +1,54 @@ -// -// CommentSelectedView.swift -// Poppool -// -// Created by SeoJunYoung on 12/13/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentSelectedView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18) - label.setLineHeightText(text: "코멘트 작성 방법 선택", font: .KorFont(style: .bold, size: 18)) + label.setLineHeightText(text: "코멘트 작성 방법 선택", font: .korFont(style: .bold, size: 18)) return label }() - + let cancelButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let normalCommentButton: UIButton = { let button = UIButton() button.setTitle("일반 코멘트 작성하기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + private let lineView: UIView = { let view = UIView() view.backgroundColor = .g50 return view }() - + let instaCommentButton: UIButton = { let button = UIButton() button.setTitle("인스타그램 연동 코멘트 작성하기", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 15) + button.titleLabel?.font = .korFont(style: .medium, size: 15) button.contentHorizontalAlignment = .leading return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,34 +56,34 @@ final class CommentSelectedView: UIView { // MARK: - SetUp private extension CommentSelectedView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(34) make.leading.equalToSuperview().inset(20) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(normalCommentButton) normalCommentButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(40) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(lineView) lineView.snp.makeConstraints { make in make.top.equalTo(normalCommentButton.snp.bottom).offset(16) make.height.equalTo(2) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(instaCommentButton) instaCommentButton.snp.makeConstraints { make in make.top.equalTo(lineView.snp.bottom).offset(16) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift index a62c93ce..8500bae7 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift @@ -1,25 +1,20 @@ -// -// CommentUserBlockController.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CommentUserBlockController: BaseViewController, View { - + typealias Reactor = CommentUserBlockReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CommentUserBlockView() } @@ -48,18 +43,18 @@ extension CommentUserBlockController { .map { Reactor.Action.stopButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.blockButton.rx.tap .map { Reactor.Action.continueButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.mainView.titleLabel.setLineHeightText( text: "\(state.nickName ?? "")님을 차단할까요?", - font: .KorFont(style: .bold, size: 18) + font: .korFont(style: .bold, size: 18) ) } .disposed(by: disposeBag) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift index 7400fa39..72c9b9fa 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift @@ -6,42 +6,42 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CommentUserBlockReactor: Reactor { - + // MARK: - Reactor enum Action { case continueButtonTapped case stopButtonTapped } - + enum Mutation { case setSelectedType(type: SelectedType) } - + struct State { var selectedType: SelectedType = .none var nickName: String? } - + enum SelectedType { case none case cancel case block } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(nickName: String?) { self.initialState = State(nickName: nickName) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -51,7 +51,7 @@ final class CommentUserBlockReactor: Reactor { return Observable.just(.setSelectedType(type: .cancel)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift similarity index 81% rename from Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift index 1906ff04..f178c28a 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift @@ -1,52 +1,44 @@ -// -// CommentUserBlockView.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit +import DesignSystem + import SnapKit final class CommentUserBlockView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "님을 차단할까요?") - return label + return PPLabel(style: .bold, fontSize: 18, text: "님을 차단할까요?") }() - + private let descriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14, text: "차단하시면 앞으로 이 유저가 남긴\n코멘트와 반응을 볼 수 없어요.") label.numberOfLines = 2 label.textColor = .g600 return label }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + let cancelButton: PPButton = { - let button = PPButton(style: .secondary, text: "취소") - return button + return PPButton(style: .secondary, text: "취소") }() - + let blockButton: PPButton = { - let button = PPButton(style: .primary, text: "차단하기") - return button + return PPButton(style: .primary, text: "차단하기") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -54,20 +46,20 @@ final class CommentUserBlockView: UIView { // MARK: - SetUp private extension CommentUserBlockView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + buttonStackView.addArrangedSubview(cancelButton) buttonStackView.addArrangedSubview(blockButton) self.addSubview(buttonStackView) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift index b14cf4de..4545885d 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift @@ -1,29 +1,24 @@ -// -// NormalCommentAddController.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit -import SnapKit -import RxCocoa -import RxSwift +import DesignSystem + import ReactorKit +import RxCocoa import RxKeyboard +import RxSwift +import SnapKit final class NormalCommentAddController: BaseViewController, View { - + typealias Reactor = NormalCommentAddReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var keyBoardDisposeBag = DisposeBag() - + private var mainView = NormalCommentAddView() - + private var sections: [any Sectionable] = [] } @@ -33,7 +28,7 @@ extension NormalCommentAddController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -79,12 +74,12 @@ private extension NormalCommentAddController { // MARK: - Methods extension NormalCommentAddController { func bind(reactor: Reactor) { - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -92,7 +87,7 @@ extension NormalCommentAddController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // RxKeyboard로 키보드 높이 감지 RxKeyboard.instance.visibleHeight .skip(1) @@ -103,7 +98,7 @@ extension NormalCommentAddController { UIView.animate(withDuration: 0.3) { self.mainView.transform = .identity } - + } else { UIView.animate(withDuration: 0.3) { self.mainView.transform = .init(translationX: 0, y: -100) @@ -111,7 +106,7 @@ extension NormalCommentAddController { } }) .disposed(by: keyBoardDisposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -120,7 +115,7 @@ extension NormalCommentAddController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -136,7 +131,7 @@ extension NormalCommentAddController { } owner.sections = state.sections if state.isReloadView { owner.mainView.contentCollectionView.reloadData() } - + } .disposed(by: disposeBag) } @@ -147,11 +142,11 @@ extension NormalCommentAddController: UICollectionViewDelegate, UICollectionView func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -164,7 +159,7 @@ extension NormalCommentAddController: UICollectionViewDelegate, UICollectionView .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? AddCommentSectionCell { cell.commentTextView.rx.didChange .withUnretained(cell) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift similarity index 84% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift index 04bb9027..a3c2b594 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift @@ -1,19 +1,16 @@ -// -// NormalCommentAddReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - -import UIKit import PhotosUI +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class NormalCommentAddReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -24,7 +21,7 @@ final class NormalCommentAddReactor: Reactor { case inputComment(text: String?) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case showImagePicker(controller: BaseViewController) @@ -32,24 +29,24 @@ final class NormalCommentAddReactor: Reactor { case setComment(text: String?) case save(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var text: String? var isReloadView: Bool = true var isSaving: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var popUpID: Int64 private var popUpName: String - - private let commentAPIUseCase = CommentAPIUseCaseImpl(repository: CommentAPIRepository(provider: ProviderImpl())) - private let imageService = PreSignedService() - + + private let commentAPIUseCase: CommentAPIUseCase + private let preSignedUseCase: PreSignedUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -74,12 +71,19 @@ final class NormalCommentAddReactor: Reactor { private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private let spacing32Section = SpacingSection(inputDataList: [.init(spacing: 32)]) // MARK: - init - init(popUpID: Int64, popUpName: String) { + init( + popUpID: Int64, + popUpName: String, + commentAPIUseCase: CommentAPIUseCase, + preSignedUseCase: PreSignedUseCase + ) { self.initialState = State() self.popUpID = popUpID self.popUpName = popUpName + self.commentAPIUseCase = commentAPIUseCase + self.preSignedUseCase = preSignedUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -102,7 +106,7 @@ final class NormalCommentAddReactor: Reactor { return Observable.just(.save(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -154,27 +158,32 @@ final class NormalCommentAddReactor: Reactor { let images = imageSection.inputDataList.compactMap { $0.image }.enumerated().map { $0 } let uuid = UUID().uuidString let pathList = images.map { "PopUpComment/\(popUpName)/\(uuid)/\($0.offset).jpg" } - - imageService.tryUpload(datas: images.map { .init(filePath: "PopUpComment/\(popUpName)/\(uuid)/\($0.offset).jpg", image: $0.element)}) - .subscribe(onSuccess: { [weak self] _ in - guard let self = self else { return } - self.commentAPIUseCase.postCommentAdd(popUpStoreId: self.popUpID, content: newState.text, commentType: "NORMAL", imageUrlList: pathList) - .subscribe(onDisposed: { - controller.navigationController?.popViewController(animated: true) { - DispatchQueue.main.asyncAfter(deadline: .now()) { - ToastMaker.createToast(message: "코멘트 작성을 완료했어요") - } + + preSignedUseCase.tryUpload(presignedURLRequest: images.map { + return ( + filePath: "PopUpComment/\(popUpName)/\(uuid)/\($0.offset).jpg", + image: $0.element + ) + }) + .subscribe(onSuccess: { [weak self] _ in + guard let self = self else { return } + self.commentAPIUseCase.postCommentAdd(popUpStoreId: self.popUpID, content: newState.text, commentType: "NORMAL", imageUrlList: pathList) + .subscribe(onDisposed: { + controller.navigationController?.popViewController(animated: true) { + DispatchQueue.main.asyncAfter(deadline: .now()) { + ToastMaker.createToast(message: "코멘트 작성을 완료했어요") } - }) - .disposed(by: disposeBag) - }) - .disposed(by: disposeBag) + } + }) + .disposed(by: disposeBag) + }) + .disposed(by: disposeBag) } - + } return newState } - + func getSection() -> [any Sectionable] { return [ spacing25Section, @@ -199,30 +208,30 @@ final class NormalCommentAddReactor: Reactor { extension NormalCommentAddReactor: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) - + // 이미지가 로드된 순서를 보장하기 위해 선택한 이미지 개수만큼의 nil 배열을 생성 var originImageList = [UIImage?](repeating: nil, count: results.count) let dispatchGroup = DispatchGroup() // 모든 이미지를 로드할 때까지 대기 - + // results에서 이미지를 비동기적으로 로드 for (index, result) in results.enumerated() { if result.itemProvider.canLoadObject(ofClass: UIImage.self) { dispatchGroup.enter() // 이미지 로드가 시작될 때 그룹에 등록 - + result.itemProvider.loadObject(ofClass: UIImage.self) { image, _ in defer { dispatchGroup.leave() } // 이미지 로드가 끝날 때 그룹에서 제거 - + if let image = image as? UIImage { originImageList[index] = image // 로드된 이미지를 해당 인덱스에 저장 } else { - Logger.log(message: "Failed to load image", category: .error) + Logger.log("Failed to load image", category: .error) } } } else { - Logger.log(message: "ItemProvider Can Not Load Object", category: .error) + Logger.log("ItemProvider Can Not Load Object", category: .error) } } - + // 모든 이미지가 로드된 후에 한 번에 choiceImageList 업데이트 dispatchGroup.notify(queue: .main) { let filteredImages = originImageList.compactMap { $0 } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift index 3fb3c140..ba483b34 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift @@ -1,26 +1,21 @@ -// -// AddCommentDescriptionSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit +import DesignSystem + import RxSwift struct AddCommentDescriptionSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = AddCommentDescriptionSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -36,7 +31,7 @@ struct AddCommentDescriptionSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift index e83fe204..a42a0fbb 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift @@ -1,21 +1,16 @@ -// -// AddCommentDescriptionSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class AddCommentDescriptionSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let descriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g600 @@ -23,12 +18,12 @@ final class AddCommentDescriptionSectionCell: UICollectionViewCell { return label }() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -48,8 +43,8 @@ extension AddCommentDescriptionSectionCell: Inputable { struct Input { var description: String? } - + func injection(with input: Input) { - descriptionLabel.setLineHeightText(text: input.description, font: .KorFont(style: .regular, size: 13)) + descriptionLabel.setLineHeightText(text: input.description, font: .korFont(style: .regular, size: 13)) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift index 09d0822d..52d47c0e 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift @@ -1,26 +1,21 @@ -// -// AddCommentImageSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit +import DesignSystem + import RxSwift struct AddCommentImageSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = AddCommentImageSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(80), diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift index d9b4d329..8d732900 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift @@ -1,26 +1,20 @@ -// -// AddCommentImageSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class AddCommentImageSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let imageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + let deleteButton: UIButton = { let button = UIButton() button.backgroundColor = .w100 @@ -28,24 +22,24 @@ final class AddCommentImageSectionCell: UICollectionViewCell { button.clipsToBounds = true return button }() - + private let xmarkImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_xmark") return view }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -58,18 +52,18 @@ private extension AddCommentImageSectionCell { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true contentView.layer.borderColor = UIColor.blu400.cgColor - + contentView.addSubview(imageView) imageView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + contentView.addSubview(deleteButton) deleteButton.snp.makeConstraints { make in make.size.equalTo(16) make.top.trailing.equalToSuperview().inset(6) } - + deleteButton.addSubview(xmarkImageView) xmarkImageView.snp.makeConstraints { make in make.center.equalToSuperview() @@ -86,9 +80,9 @@ extension AddCommentImageSectionCell: Inputable { var imageURL: String? var imageID: Int64? } - + func injection(with input: Input) { - + if input.isFirstCell { imageView.image = UIImage(named: "icon_camera_blue") contentView.layer.borderWidth = 1 diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift index 051d1370..ef8bdb14 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift @@ -1,26 +1,21 @@ -// -// AddCommentSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - import UIKit +import DesignSystem + import RxSwift struct AddCommentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = AddCommentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift index 8bc041f7..a2447743 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift @@ -1,65 +1,60 @@ -// -// AddCommentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/15/24. -// - import UIKit -import SnapKit -import RxSwift +import DesignSystem + import RxCocoa +import RxSwift +import SnapKit final class AddCommentSectionCell: UICollectionViewCell { - + // MARK: - Components - + var disposeBag = DisposeBag() - + let commentTextView: UITextView = { let view = UITextView() view.textContainerInset = .zero view.textContainer.lineFragmentPadding = 0 - view.font = .KorFont(style: .medium, size: 14) + view.font = .korFont(style: .medium, size: 14) return view }() - + let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.textColor = .g500 return label }() - + private let placeHolderLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "최소 10자 이상 입력해주세요") label.textColor = .g200 return label }() - + private let noticeLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 12, text: "최대 500자까지 입력해주세요") label.textColor = .re500 label.isHidden = true return label }() - + private var isActiveComment: Bool = false - + private var commentState: BehaviorRelay = .init(value: .empty) - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() bind() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -70,7 +65,7 @@ final class AddCommentSectionCell: UICollectionViewCell { // MARK: - SetUp private extension AddCommentSectionCell { func bind() { - + commentTextView.rx.didBeginEditing .withUnretained(self) .subscribe { (owner, _) in @@ -78,7 +73,7 @@ private extension AddCommentSectionCell { owner.commentState.accept(owner.checkValidation(text: owner.commentTextView.text)) } .disposed(by: disposeBag) - + commentTextView.rx.didEndEditing .withUnretained(self) .subscribe { (owner, _) in @@ -86,7 +81,7 @@ private extension AddCommentSectionCell { owner.commentState.accept(owner.checkValidation(text: owner.commentTextView.text)) } .disposed(by: disposeBag) - + commentTextView.rx.didChange .debounce(.milliseconds(5), scheduler: MainScheduler.instance) .withUnretained(self) @@ -94,7 +89,7 @@ private extension AddCommentSectionCell { owner.commentState.accept(owner.checkValidation(text: owner.commentTextView.text)) } .disposed(by: disposeBag) - + commentState .withUnretained(self) .subscribe { (owner, state) in @@ -109,7 +104,7 @@ private extension AddCommentSectionCell { } .disposed(by: disposeBag) } - + func setUpConstraints() { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true @@ -136,13 +131,13 @@ private extension AddCommentSectionCell { make.bottom.equalToSuperview().inset(16) } } - + func checkValidation(text: String?) -> CommentState { guard let text = text else { return .empty } if text.isEmpty { return isActiveComment ? .emptyActive : .empty } - + switch text.count { case 1...9: return isActiveComment ? .shortLengthActive : .shortLength @@ -158,7 +153,7 @@ extension AddCommentSectionCell: Inputable { struct Input { var text: String? } - + func injection(with input: Input) { commentTextView.text = input.text commentState.accept(checkValidation(text: input.text)) @@ -174,7 +169,7 @@ enum CommentState { case longLengthActive case normal case normalActive - + var borderColor: UIColor? { switch self { case .shortLength, .longLength, .longLengthActive: @@ -183,7 +178,7 @@ enum CommentState { return .g100 } } - + var countLabelColor: UIColor? { switch self { case .shortLength, .longLength, .longLengthActive: @@ -192,7 +187,7 @@ enum CommentState { return .g500 } } - + var textColor: UIColor? { switch self { case .shortLength, .longLength, .longLengthActive: @@ -201,7 +196,7 @@ enum CommentState { return .g1000 } } - + var description: String? { switch self { case .longLength, .longLengthActive: @@ -212,7 +207,7 @@ enum CommentState { return nil } } - + var isHiddenNoticeLabel: Bool { switch self { case .longLength, .longLengthActive, .shortLength: @@ -221,7 +216,7 @@ enum CommentState { return true } } - + var isHiddenPlaceHolder: Bool { switch self { case .empty, .emptyActive: diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift index 3a50e804..0b5f1f4a 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift @@ -1,26 +1,21 @@ -// -// AddCommentTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit +import DesignSystem + import RxSwift struct AddCommentTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = AddCommentTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -36,7 +31,7 @@ struct AddCommentTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift index 8c7ad96d..fbbf718b 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift @@ -1,32 +1,26 @@ -// -// AddCommentTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class AddCommentTitleSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -46,8 +40,8 @@ extension AddCommentTitleSectionCell: Inputable { struct Input { var title: String? } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift index c5030a02..a969b3d2 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift @@ -1,42 +1,36 @@ -// -// NormalCommentAddView.swift -// Poppool -// -// Created by SeoJunYoung on 12/14/24. -// - import UIKit +import DesignSystem + import SnapKit final class NormalCommentAddView: UIView { - + // MARK: - Components - + let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "코멘트 작성하기", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "코멘트 작성하기", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 view.isScrollEnabled = false return view }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -44,19 +38,19 @@ final class NormalCommentAddView: UIView { // MARK: - SetUp private extension NormalCommentAddView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(52) } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift index 84732cc8..d7dc9399 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift @@ -1,29 +1,24 @@ -// -// NormalCommentEditController.swift -// Poppool -// -// Created by SeoJunYoung on 2/1/25. -// - import UIKit -import SnapKit -import RxCocoa -import RxSwift +import DesignSystem + import ReactorKit +import RxCocoa import RxKeyboard +import RxSwift +import SnapKit final class NormalCommentEditController: BaseViewController, View { - + typealias Reactor = NormalCommentEditReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var keyBoardDisposeBag = DisposeBag() - + private var mainView = NormalCommentEditView() - + private var sections: [any Sectionable] = [] } @@ -33,7 +28,7 @@ extension NormalCommentEditController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -79,12 +74,12 @@ private extension NormalCommentEditController { // MARK: - Methods extension NormalCommentEditController { func bind(reactor: Reactor) { - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -92,7 +87,7 @@ extension NormalCommentEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // RxKeyboard로 키보드 높이 감지 RxKeyboard.instance.visibleHeight .skip(1) @@ -103,7 +98,7 @@ extension NormalCommentEditController { UIView.animate(withDuration: 0.3) { self.mainView.transform = .identity } - + } else { UIView.animate(withDuration: 0.3) { self.mainView.transform = .init(translationX: 0, y: -100) @@ -111,7 +106,7 @@ extension NormalCommentEditController { } }) .disposed(by: keyBoardDisposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -120,7 +115,7 @@ extension NormalCommentEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -136,17 +131,17 @@ extension NormalCommentEditController { } owner.sections = state.sections if state.isReloadView { owner.mainView.contentCollectionView.reloadData() } - + } .disposed(by: disposeBag) - + reactor.state .take(1) .withUnretained(self) .subscribe { (owner, state) in owner.sections = state.sections if state.isReloadView { owner.mainView.contentCollectionView.reloadData() } - + } .disposed(by: disposeBag) } @@ -157,11 +152,11 @@ extension NormalCommentEditController: UICollectionViewDelegate, UICollectionVie func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -174,7 +169,7 @@ extension NormalCommentEditController: UICollectionViewDelegate, UICollectionVie .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? AddCommentSectionCell { cell.commentTextView.rx.didChange .withUnretained(cell) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift similarity index 78% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift index 437a7046..dc535c64 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift @@ -1,19 +1,16 @@ -// -// NormalCommentEditReactor.swift -// Poppool -// -// Created by SeoJunYoung on 2/1/25. -// - -import UIKit import PhotosUI +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class NormalCommentEditReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -24,7 +21,7 @@ final class NormalCommentEditReactor: Reactor { case inputComment(text: String?) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case showImagePicker(controller: BaseViewController) @@ -32,25 +29,31 @@ final class NormalCommentEditReactor: Reactor { case setComment(text: String?) case save(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var text: String? var isReloadView: Bool = true var isSaving: Bool = false } - + + struct PutCommentImageData { + var imageId: Int64? + var imageUrl: String? + var actionType: String? + } + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var popUpID: Int64 private var popUpName: String private var originComment: DetailCommentSection.CellType.Input - - private let commentAPIUseCase = CommentAPIUseCaseImpl(repository: CommentAPIRepository(provider: ProviderImpl())) - private let imageService = PreSignedService() - + + private let commentAPIUseCase: CommentAPIUseCase + private let preSignedUseCase: PreSignedUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -74,19 +77,27 @@ final class NormalCommentEditReactor: Reactor { private let spacing5Section = SpacingSection(inputDataList: [.init(spacing: 5)]) private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private let spacing32Section = SpacingSection(inputDataList: [.init(spacing: 32)]) - + // MARK: - init - init(popUpID: Int64, popUpName: String, comment: DetailCommentSection.CellType.Input) { + init( + popUpID: Int64, + popUpName: String, + comment: DetailCommentSection.CellType.Input, + commentAPIUseCase: CommentAPIUseCase, + preSignedUseCase: PreSignedUseCase + ) { self.initialState = State(text: comment.comment) self.popUpID = popUpID self.popUpName = popUpName self.originComment = comment + self.commentAPIUseCase = commentAPIUseCase + self.preSignedUseCase = preSignedUseCase let imageList = zip(comment.imageList, comment.imageIDList) imageSection.inputDataList.append(contentsOf: imageList.map({ url, id in .init(image: nil, isFirstCell: false, isEditCase: true, imageURL: url, imageID: id) })) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -108,7 +119,7 @@ final class NormalCommentEditReactor: Reactor { return Observable.just(.save(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -147,57 +158,59 @@ final class NormalCommentEditReactor: Reactor { commentSection.inputDataList[0].text = text case .save(let controller): newState.isSaving = true - + let addImages = imageSection.inputDataList.compactMap { $0.image }.enumerated().map { $0 } let uuid = UUID().uuidString let pathList = addImages.map { "PopUpComment/\(popUpName)/\(uuid)/\($0.offset).jpg" } - + let keepImages = imageSection.inputDataList.compactMap { $0.imageURL } - + let originImages = zip(originComment.imageList, originComment.imageIDList) var deleteImages: [(String?, Int64)] = [] - + for (imageURL, imageID) in originImages { if !keepImages.contains(imageURL!) { deleteImages.append((imageURL, imageID)) } } - - var convertAddImages: [PutCommentImageDataRequestDTO] = addImages.map { .init(imageId: nil, imageUrl: pathList[$0.offset], actionType: "ADD")} - var convertKeepImages: [PutCommentImageDataRequestDTO] = keepImages.map { .init(imageId: nil, imageUrl: $0, actionType: "KEEP")} - var convertDeleteImages: [PutCommentImageDataRequestDTO] = deleteImages.map { .init(imageId: $0.1, imageUrl: $0.0, actionType: "DELETE")} - + + var convertAddImages: [PutCommentImageData] = addImages.map { .init(imageId: nil, imageUrl: pathList[$0.offset], actionType: "ADD")} + var convertKeepImages: [PutCommentImageData] = keepImages.map { .init(imageId: nil, imageUrl: $0, actionType: "KEEP")} + var convertDeleteImages: [PutCommentImageData] = deleteImages.map { .init(imageId: $0.1, imageUrl: $0.0, actionType: "DELETE")} + if !addImages.isEmpty { - imageService.tryUpload(datas: addImages.map { .init(filePath: pathList[$0.offset], image: $0.element)}) - .subscribe { [weak self] _ in + preSignedUseCase.tryUpload(presignedURLRequest: addImages.map { + return (filePath: pathList[$0.offset], image: $0.element) + }) + .subscribe { [weak self] _ in + guard let self = self else { return } + self.commentAPIUseCase.editComment( + popUpStoreId: self.popUpID, + commentId: self.originComment.commentID, + content: newState.text, + imageUrlList: (convertAddImages + convertKeepImages + convertDeleteImages).map { $0.imageUrl } + ) + .subscribe(onDisposed: { [weak self, weak controller] in guard let self = self else { return } - self.commentAPIUseCase.editComment( - popUpStoreId: self.popUpID, - commentId: self.originComment.commentID, - content: newState.text, - imageUrlList: convertAddImages + convertKeepImages + convertDeleteImages - ) - .subscribe { [weak self, weak controller] in - guard let self = self else { return } - self.imageService.tryDelete(targetPaths: .init(objectKeyList: deleteImages.compactMap { $0.0 })) - .subscribe { - controller?.navigationController?.popViewController(animated: true) - } - .disposed(by: self.disposeBag) - } - .disposed(by: self.disposeBag) - } - .disposed(by: disposeBag) + self.preSignedUseCase.tryDelete(objectKeyList: deleteImages.compactMap { $0.0 }) + .subscribe(onDisposed: { + controller?.navigationController?.popViewController(animated: true) + }) + .disposed(by: self.disposeBag) + }) + .disposed(by: self.disposeBag) + } + .disposed(by: disposeBag) } else { commentAPIUseCase.editComment( popUpStoreId: self.popUpID, commentId: self.originComment.commentID, content: newState.text, - imageUrlList: convertAddImages + convertKeepImages + convertDeleteImages + imageUrlList: (convertAddImages + convertKeepImages + convertDeleteImages).map { $0.imageUrl } ) .subscribe { [weak self, weak controller] in guard let self = self else { return } - self.imageService.tryDelete(targetPaths: .init(objectKeyList: deleteImages.compactMap { $0.0 })) + self.preSignedUseCase.tryDelete(objectKeyList: deleteImages.compactMap { $0.0 }) .subscribe { controller?.navigationController?.popViewController(animated: true) } @@ -208,7 +221,7 @@ final class NormalCommentEditReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ spacing25Section, @@ -233,30 +246,30 @@ final class NormalCommentEditReactor: Reactor { extension NormalCommentEditReactor: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) - + // 이미지가 로드된 순서를 보장하기 위해 선택한 이미지 개수만큼의 nil 배열을 생성 var originImageList = [UIImage?](repeating: nil, count: results.count) let dispatchGroup = DispatchGroup() // 모든 이미지를 로드할 때까지 대기 - + // results에서 이미지를 비동기적으로 로드 for (index, result) in results.enumerated() { if result.itemProvider.canLoadObject(ofClass: UIImage.self) { dispatchGroup.enter() // 이미지 로드가 시작될 때 그룹에 등록 - + result.itemProvider.loadObject(ofClass: UIImage.self) { image, _ in defer { dispatchGroup.leave() } // 이미지 로드가 끝날 때 그룹에서 제거 - + if let image = image as? UIImage { originImageList[index] = image // 로드된 이미지를 해당 인덱스에 저장 } else { - Logger.log(message: "Failed to load image", category: .error) + Logger.log("Failed to load image", category: .error) } } } else { - Logger.log(message: "ItemProvider Can Not Load Object", category: .error) + Logger.log("ItemProvider Can Not Load Object", category: .error) } } - + // 모든 이미지가 로드된 후에 한 번에 choiceImageList 업데이트 dispatchGroup.notify(queue: .main) { let filteredImages = originImageList.compactMap { $0 } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift index 544e47aa..93eaf9b5 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift @@ -1,42 +1,36 @@ -// -// NormalCommentEditView.swift -// Poppool -// -// Created by SeoJunYoung on 2/1/25. -// - import UIKit +import DesignSystem + import SnapKit final class NormalCommentEditView: UIView { - + // MARK: - Components - + let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "코멘트 수정하기", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "코멘트 수정하기", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 view.isScrollEnabled = false return view }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -44,19 +38,19 @@ final class NormalCommentEditView: UIView { // MARK: - SetUp private extension NormalCommentEditView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(52) } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift index 2665a516..9c5c5e0e 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift @@ -1,28 +1,23 @@ -// -// OtherUserCommentController.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class OtherUserCommentController: BaseViewController, View { - + typealias Reactor = OtherUserCommentReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = OtherUserCommentView() - + private var sections: [any Sectionable] = [] - + private let cellTapped: PublishSubject = .init() } @@ -31,9 +26,9 @@ extension OtherUserCommentController { override func viewDidLoad() { super.viewDidLoad() setUp() - + } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -48,13 +43,13 @@ private extension OtherUserCommentController { mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) } - + if let layout = reactor?.compositionalLayout { mainView.contentCollectionView.collectionViewLayout = layout } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers @@ -62,7 +57,7 @@ private extension OtherUserCommentController { mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyCommentedPopUpGridSectionCell.self, forCellWithReuseIdentifier: MyCommentedPopUpGridSectionCell.identifiers @@ -77,7 +72,7 @@ extension OtherUserCommentController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -85,7 +80,7 @@ extension OtherUserCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -93,7 +88,7 @@ extension OtherUserCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -109,19 +104,18 @@ extension OtherUserCommentController: UICollectionViewDelegate, UICollectionView func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift index af4281b6..69d98d5b 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift @@ -1,45 +1,42 @@ -// -// OtherUserCommentReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class OtherUserCommentReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case backButtonTapped(controller: BaseViewController) case cellTapped(controller: BaseViewController, row: Int) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) case loadView case skip case moveToDetailScene(controller: BaseViewController, row: Int) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private let commenterID: String? - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -56,13 +53,17 @@ final class OtherUserCommentReactor: Reactor { private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private var countTitleSection = CommentListTitleSection(inputDataList: []) private var popUpSection = MyCommentedPopUpGridSection(inputDataList: []) - + // MARK: - init - init(commenterID: String?) { + init( + commenterID: String?, + userAPIUseCase: UserAPIUseCase + ) { self.initialState = State() self.commenterID = commenterID + self.userAPIUseCase = userAPIUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -88,7 +89,7 @@ final class OtherUserCommentReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -103,12 +104,18 @@ final class OtherUserCommentReactor: Reactor { case .moveToDetailScene(let controller, let row): let id = popUpSection.inputDataList[row].popUpID let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: id) + nextController.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift index 68cacc55..d6c4fcca 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift @@ -1,26 +1,21 @@ -// -// OtherUserCommentSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/28/24. -// - import UIKit +import DesignSystem + import RxSwift struct OtherUserCommentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = OtherUserCommentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 8) / 2), diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift index 2640bfa9..4b232be9 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift @@ -1,62 +1,56 @@ -// -// OtherUserCommentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/28/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class OtherUserCommentSectionCell: UICollectionViewCell { - + // MARK: - Components private let imageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 11) label.textColor = .blu500 return label }() - + private let contentLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.lineBreakMode = . byTruncatingTail label.numberOfLines = 2 return label }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) label.textColor = .g400 return label }() - + private let likeImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_like_clear") return view }() - + private let likeCountLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.textColor = .w100 return label }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -77,31 +71,30 @@ private extension OtherUserCommentSectionCell { make.top.leading.trailing.equalToSuperview() make.height.equalTo(163.5) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(contentLabel) contentLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(12) make.bottom.equalToSuperview().inset(16) } - imageView.addSubview(likeCountLabel) likeCountLabel.snp.makeConstraints { make in make.trailing.bottom.equalToSuperview().inset(12) } - + imageView.addSubview(likeImageView) likeImageView.snp.makeConstraints { make in make.size.equalTo(18) @@ -120,12 +113,12 @@ extension OtherUserCommentSectionCell: Inputable { var date: String? var popUpID: Int64 } - + func injection(with input: Input) { imageView.setPPImage(path: input.imagePath) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 11)) - contentLabel.setLineHeightText(text: input.comment, font: .KorFont(style: .medium, size: 11)) - dateLabel.setLineHeightText(text: input.date, font: .KorFont(style: .regular, size: 11)) - likeCountLabel.setLineHeightText(text: "\(input.likeCount)", font: .KorFont(style: .regular, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 11)) + contentLabel.setLineHeightText(text: input.comment, font: .korFont(style: .medium, size: 11)) + dateLabel.setLineHeightText(text: input.date, font: .korFont(style: .regular, size: 11)) + likeCountLabel.setLineHeightText(text: "\(input.likeCount)", font: .korFont(style: .regular, size: 11)) } } diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift index e6d99298..782faaaf 100644 --- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift @@ -1,35 +1,30 @@ -// -// OtherUserCommentView.swift -// Poppool -// -// Created by SeoJunYoung on 12/27/24. -// - import UIKit +import DesignSystem + import SnapKit final class OtherUserCommentView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "코멘트 작성 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "코멘트 작성 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,13 +32,13 @@ final class OtherUserCommentView: UIView { // MARK: - SetUp private extension OtherUserCommentView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Detail/DetailController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailController.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/Detail/DetailController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailController.swift index 75cfd69c..d863da13 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/DetailController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailController.swift @@ -1,16 +1,11 @@ -// -// DetailController.swift -// Poppool -// -// Created by SeoJunYoung on 12/9/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class DetailController: BaseViewController, View { @@ -52,7 +47,6 @@ extension DetailController { tabBarController?.tabBar.isHidden = false } - } // MARK: - SetUp @@ -71,7 +65,7 @@ private extension DetailController { mainView.contentCollectionView.register(DetailCommentTitleSectionCell.self, forCellWithReuseIdentifier: DetailCommentTitleSectionCell.identifiers) mainView.contentCollectionView.register(DetailCommentSectionCell.self, forCellWithReuseIdentifier: DetailCommentSectionCell.identifiers) mainView.contentCollectionView.register(DetailEmptyCommetSectionCell.self, forCellWithReuseIdentifier: DetailEmptyCommetSectionCell.identifiers) - mainView.contentCollectionView.register(SearchTitleSectionCell.self, forCellWithReuseIdentifier: SearchTitleSectionCell.identifiers) + mainView.contentCollectionView.register(SimilarTitleSectionCell.self, forCellWithReuseIdentifier: SimilarTitleSectionCell.identifiers) mainView.contentCollectionView.register(DetailSimilarSectionCell.self, forCellWithReuseIdentifier: DetailSimilarSectionCell.identifiers) view.addSubview(mainView) mainView.snp.makeConstraints { make in @@ -205,12 +199,12 @@ extension DetailController: UICollectionViewDelegate, UICollectionViewDataSource .subscribe { (collectionView, _) in cell.isOpen.toggle() if cell.isOpen { - cell.buttonTitleLabel.setLineHeightText(text: "닫기", font: .KorFont(style: .medium, size: 13)) + cell.buttonTitleLabel.setLineHeightText(text: "닫기", font: .korFont(style: .medium, size: 13)) cell.contentLabel.numberOfLines = 0 cell.buttonImageView.image = UIImage(named: "icon_dropdown_top_gray") } else { cell.contentLabel.numberOfLines = 3 - cell.buttonTitleLabel.setLineHeightText(text: "더보기", font: .KorFont(style: .medium, size: 13)) + cell.buttonTitleLabel.setLineHeightText(text: "더보기", font: .korFont(style: .medium, size: 13)) cell.buttonImageView.image = UIImage(named: "icon_dropdown_bottom_gray") } collectionView.reloadData() @@ -232,7 +226,7 @@ extension DetailController: UICollectionViewDelegate, UICollectionViewDataSource cell.imageCollectionView.rx.itemSelected .withUnretained(self) .map { (owner, cellIndexPath) in - Reactor.Action.commentImageTapped(controller: owner, cellRow: indexPath.row, ImageRow: cellIndexPath.row) + Reactor.Action.commentImageTapped(controller: owner, cellRow: indexPath.row, imageRow: cellIndexPath.row) } .bind(to: reactor.action) .disposed(by: cell.disposeBag) @@ -266,7 +260,7 @@ extension DetailController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? DetailEmptyCommetSectionCell { cell.commentButton.rx.tap .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Scene/Detail/DetailReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailReactor.swift similarity index 84% rename from Poppool/Poppool/Presentation/Scene/Detail/DetailReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailReactor.swift index 165a8b7a..e6626483 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/DetailReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/DetailReactor.swift @@ -1,19 +1,16 @@ -// -// DetailReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/9/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + +import LinkPresentation import ReactorKit -import RxSwift import RxCocoa -import LinkPresentation +import RxSwift final class DetailReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -26,12 +23,12 @@ final class DetailReactor: Reactor { case commentMenuButtonTapped(controller: BaseViewController, indexPath: IndexPath) case commentDetailButtonTapped(controller: BaseViewController, indexPath: IndexPath) case commentLikeButtonTapped(indexPath: IndexPath) - case commentImageTapped(controller: BaseViewController, cellRow: Int, ImageRow: Int) + case commentImageTapped(controller: BaseViewController, cellRow: Int, imageRow: Int) case similarSectionTapped(controller: BaseViewController, indexPath: IndexPath) case backButtonTapped(controller: BaseViewController) case loginButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToCommentTypeSelectedScene(controller: BaseViewController) @@ -44,30 +41,30 @@ final class DetailReactor: Reactor { case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) case moveToRecentScene(controller: BaseViewController) case moveToLoginScene(controller: BaseViewController) - case moveToImageDetailScene(controller: BaseViewController, cellRow: Int, ImageRow: Int) + case moveToImageDetailScene(controller: BaseViewController, cellRow: Int, imageRow: Int) } - + private var commentButtonIsEnable: Bool = false - + struct State { var sections: [any Sectionable] = [] var barkGroundImagePath: String? var commentButtonIsEnable: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let popUpID: Int64 private var popUpName: String? private var isLogin: Bool = false private var isFirstRequest: Bool = true - - private var imageService = PreSignedService() - private let popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - private let commentAPIUseCase = CommentAPIUseCaseImpl(repository: CommentAPIRepository(provider: ProviderImpl())) + + private var preSignedUseCase: PreSignedUseCase + private let popUpAPIUseCase: PopUpAPIUseCase + private let userAPIUseCase: UserAPIUseCase + private let commentAPIUseCase: CommentAPIUseCase lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -81,7 +78,7 @@ final class DetailReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var imageBannerSection = ImageBannerSection(inputDataList: []) private var titleSection = DetailTitleSection(inputDataList: []) private var contentSection = DetailContentSection(inputDataList: []) @@ -89,10 +86,9 @@ final class DetailReactor: Reactor { private var commentTitleSection = DetailCommentTitleSection(inputDataList: []) private var commentSection = DetailCommentSection(inputDataList: []) private var commentEmptySection = DetailEmptyCommetSection(inputDataList: [.init()]) - private var similarTitleSecion = SearchTitleSection(inputDataList: [.init(title: "지금 보고있는 팝업과 비슷한 팝업")]) + private var similarTitleSecion = SimilarTitleSection(inputDataList: [.init(title: "지금 보고있는 팝업과 비슷한 팝업")]) private var similarSection = DetailSimilarSection(inputDataList: []) - - + private var spacing70Section = SpacingSection(inputDataList: [.init(spacing: 70)]) private var spacing40Section = SpacingSection(inputDataList: [.init(spacing: 40)]) private var spacing36Section = SpacingSection(inputDataList: [.init(spacing: 36)]) @@ -102,11 +98,21 @@ final class DetailReactor: Reactor { private var spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private var spacing16GraySection = SpacingSection(inputDataList: [.init(spacing: 16, backgroundColor: .g50)]) // MARK: - init - init(popUpID: Int64) { + init( + popUpID: Int64, + userAPIUseCase: UserAPIUseCase, + popUpAPIUseCase: PopUpAPIUseCase, + commentAPIUseCase: CommentAPIUseCase, + preSignedUseCase: PreSignedUseCase + ) { self.popUpID = popUpID + self.userAPIUseCase = userAPIUseCase + self.popUpAPIUseCase = popUpAPIUseCase + self.commentAPIUseCase = commentAPIUseCase + self.preSignedUseCase = preSignedUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -136,11 +142,11 @@ final class DetailReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) case .loginButtonTapped(let controller): return Observable.just(.moveToLoginScene(controller: controller)) - case .commentImageTapped(let controller, let cellRow, let ImageRow): - return Observable.just(.moveToImageDetailScene(controller: controller, cellRow: cellRow, ImageRow: ImageRow)) + case .commentImageTapped(let controller, let cellRow, let imageRow): + return Observable.just(.moveToImageDetailScene(controller: controller, cellRow: cellRow, imageRow: imageRow)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -153,11 +159,20 @@ final class DetailReactor: Reactor { case .moveToCommentTypeSelectedScene(let controller): if isLogin { let commentController = NormalCommentAddController() - commentController.reactor = NormalCommentAddReactor(popUpID: popUpID, popUpName: popUpName ?? "") + commentController.reactor = NormalCommentAddReactor( + popUpID: popUpID, + popUpName: popUpName ?? "", + commentAPIUseCase: commentAPIUseCase, + preSignedUseCase: preSignedUseCase + ) controller.navigationController?.pushViewController(commentController, animated: true) } else { let loginController = SubLoginController() - loginController.reactor = SubLoginReactor() + loginController.reactor = SubLoginReactor( + authAPIUseCase: DIContainer.resolve(AuthAPIUseCase.self), + kakaoLoginUseCase: DIContainer.resolve(KakaoLoginUseCase.self), + appleLoginUseCase: DIContainer.resolve(AppleLoginUseCase.self) + ) let nextController = UINavigationController(rootViewController: loginController) nextController.modalPresentationStyle = .fullScreen controller.present(nextController, animated: true) @@ -169,24 +184,35 @@ final class DetailReactor: Reactor { ToastMaker.createToast(message: "주소를 복사했어요") case .moveToAddressScene(let controller): let mapGuideController = MapGuideViewController(popUpStoreId: popUpID) - let reactor = MapGuideReactor(popUpStoreId: popUpID) + let reactor = MapGuideReactor( + popUpStoreId: popUpID, + mapDirectionRepository: DIContainer.resolve(MapDirectionRepository.self) + ) mapGuideController.reactor = reactor mapGuideController.modalPresentationStyle = .overCurrentContext mapGuideController.modalTransitionStyle = .coverVertical controller.present(mapGuideController, animated: true) - - - case .moveToCommentTotalScene(let controller): if isLogin { let nextController = CommentListController() - nextController.reactor = CommentListReactor(popUpID: popUpID, popUpName: popUpName) + nextController.reactor = CommentListReactor( + popUpID: popUpID, + popUpName: popUpName, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: popUpAPIUseCase, + commentAPIUseCase: commentAPIUseCase, + preSignedUseCase: preSignedUseCase + ) controller.navigationController?.pushViewController(nextController, animated: true) } else { let loginController = SubLoginController() - loginController.reactor = SubLoginReactor() + loginController.reactor = SubLoginReactor( + authAPIUseCase: DIContainer.resolve(AuthAPIUseCase.self), + kakaoLoginUseCase: DIContainer.resolve(KakaoLoginUseCase.self), + appleLoginUseCase: DIContainer.resolve(AppleLoginUseCase.self) + ) let nextController = UINavigationController(rootViewController: loginController) nextController.modalPresentationStyle = .fullScreen controller.present(nextController, animated: true) @@ -201,23 +227,36 @@ final class DetailReactor: Reactor { case .showCommentDetailScene(let controller, let indexPath): let comment = commentSection.inputDataList[indexPath.row] let nextController = CommentDetailController() - nextController.reactor = CommentDetailReactor(comment: comment) + nextController.reactor = CommentDetailReactor( + comment: comment, + userAPIUseCase: userAPIUseCase + ) controller.presentPanModal(nextController) case .moveToDetailScene(let controller, let indexPath): let id = similarSection.inputDataList[indexPath.row].id let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: id) + nextController.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: popUpAPIUseCase, + commentAPIUseCase: commentAPIUseCase, + preSignedUseCase: preSignedUseCase + ) controller.navigationController?.pushViewController(nextController, animated: true) case .moveToRecentScene(let controller): controller.navigationController?.popViewController(animated: true) case .moveToLoginScene(let controller): let loginController = SubLoginController() - loginController.reactor = SubLoginReactor() + loginController.reactor = SubLoginReactor( + authAPIUseCase: DIContainer.resolve(AuthAPIUseCase.self), + kakaoLoginUseCase: DIContainer.resolve(KakaoLoginUseCase.self), + appleLoginUseCase: DIContainer.resolve(AppleLoginUseCase.self) + ) let nextController = UINavigationController(rootViewController: loginController) nextController.modalPresentationStyle = .fullScreen controller.present(nextController, animated: true) - case .moveToImageDetailScene(let controller, let cellRow, let ImageRow): - let imagePath = commentSection.inputDataList[cellRow].imageList[ImageRow] + case .moveToImageDetailScene(let controller, let cellRow, let imageRow): + let imagePath = commentSection.inputDataList[cellRow].imageList[imageRow] let nextController = ImageDetailController() nextController.reactor = ImageDetailReactor(imagePath: imagePath) nextController.modalPresentationStyle = .overCurrentContext @@ -225,7 +264,10 @@ final class DetailReactor: Reactor { } return newState } - +} + +// MARK: Section function +extension DetailReactor { func getSection() -> [any Sectionable] { if similarSection.inputDataList.isEmpty { if commentSection.inputDataList.isEmpty { @@ -308,9 +350,8 @@ final class DetailReactor: Reactor { ] } } - } - + func setContent() -> Observable { return popUpAPIUseCase.getPopUpDetail(commentType: "NORMAL", popUpStoredId: popUpID, isViewCount: isFirstRequest) .withUnretained(self) @@ -325,7 +366,7 @@ final class DetailReactor: Reactor { // titleSection owner.titleSection.inputDataList = [.init(title: response.name, isBookMark: response.bookmarkYn, isLogin: response.loginYn)] owner.popUpName = response.name - + // contentSection owner.contentSection.inputDataList = [.init(content: response.desc)] owner.infoSection.inputDataList = [.init( @@ -362,7 +403,7 @@ final class DetailReactor: Reactor { return .loadView } } - + func bookMark() -> Observable { if let isBookMark = titleSection.inputDataList.first?.isBookMark { titleSection.inputDataList[0].isBookMark.toggle() @@ -378,32 +419,32 @@ final class DetailReactor: Reactor { return Observable.just(.loadView) } } - + func showSharedBoard(controller: BaseViewController) { let storeName = titleSection.inputDataList.first?.title ?? "" - let imagePath = Secrets.popPoolS3BaseURL.rawValue + (imageBannerSection.inputDataList.first?.imagePaths.first ?? "") - + let imagePath = Secrets.popPoolS3BaseURL + (imageBannerSection.inputDataList.first?.imagePaths.first ?? "") + // URL 인코딩 후 생성 guard let encodedPath = imagePath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedPath) else { - Logger.log(message: "URL 생성 실패", category: .error) + Logger.log("URL 생성 실패", category: .error) return } - + // 🔹 비동기적으로 이미지 다운로드 - URLSession.shared.dataTask(with: url) { data, response, error in + URLSession.shared.dataTask(with: url) { data, _, error in if let error = error { - Logger.log(message: "다운로드 실패", category: .error) + Logger.log("다운로드 실패", category: .error) return } - + guard let data = data, let image = UIImage(data: data) else { - Logger.log(message: "이미지 변환 실패", category: .error) + Logger.log("이미지 변환 실패", category: .error) return } - - Logger.log(message: "이미지 다운로드 성공", category: .info) - + + Logger.log("이미지 다운로드 성공", category: .info) + let sharedText = "[팝풀] \(storeName) 팝업 어때요?\n지금 바로 팝풀에서 확인해보세요!" // UI 업데이트는 메인 스레드에서 실행 DispatchQueue.main.async { @@ -414,10 +455,10 @@ final class DetailReactor: Reactor { ) controller.present(activityViewController, animated: true, completion: nil) } - + }.resume() } - + func commentLike(indexPath: IndexPath) -> Observable { let isLike = commentSection.inputDataList[indexPath.row].isLike let commentID = commentSection.inputDataList[indexPath.row].commentID @@ -432,7 +473,7 @@ final class DetailReactor: Reactor { .andThen(Observable.just(.loadView)) } } - + func showOtherUserCommentMenu(controller: BaseViewController, indexPath: IndexPath, comment: DetailCommentSection.CellType.Input) { let nextController = CommentUserInfoController() nextController.reactor = CommentUserInfoReactor(nickName: comment.nickName) @@ -445,7 +486,10 @@ final class DetailReactor: Reactor { case .normal: owner.dismiss(animated: true) { [weak controller] in let otherUserCommentController = OtherUserCommentController() - otherUserCommentController.reactor = OtherUserCommentReactor(commenterID: comment.creator) + otherUserCommentController.reactor = OtherUserCommentReactor( + commenterID: comment.creator, + userAPIUseCase: self.userAPIUseCase + ) controller?.navigationController?.pushViewController(otherUserCommentController, animated: true) } case .block: @@ -462,7 +506,7 @@ final class DetailReactor: Reactor { case .block: ToastMaker.createToast(message: "\(comment.nickName ?? "")을 차단했어요") self.userAPIUseCase.postUserBlock(blockedUserId: comment.creator) - .subscribe(onDisposed: { + .subscribe(onDisposed: { blockController.dismiss(animated: true) }) .disposed(by: self.disposeBag) @@ -480,13 +524,12 @@ final class DetailReactor: Reactor { }) .disposed(by: disposeBag) } - + func showMyCommentMenu(controller: BaseViewController, indexPath: IndexPath, comment: DetailCommentSection.CellType.Input) { let nextController = CommentMyMenuController() nextController.reactor = CommentMyMenuReactor(nickName: comment.nickName) - imageService = PreSignedService() controller.presentPanModal(nextController) - + nextController.reactor?.state .withUnretained(nextController) .subscribe(onNext: { [weak self] (owner, state) in @@ -499,19 +542,25 @@ final class DetailReactor: Reactor { ToastMaker.createToast(message: "작성한 코멘트를 삭제했어요") }) .disposed(by: self.disposeBag) - + let commentList = comment.imageList.compactMap { $0 } - self.imageService.tryDelete(targetPaths: .init(objectKeyList: commentList)) - .subscribe { - Logger.log(message: "S3 Image Delete 완료", category: .info) - } + self.preSignedUseCase.tryDelete(objectKeyList: commentList) + .subscribe(onDisposed: { + Logger.log("S3 Image Delete 완료", category: .info) + }) .disposed(by: self.disposeBag) case .edit: owner.dismiss(animated: true) { [weak controller] in guard let popUpName = self.popUpName else { return } let editController = NormalCommentEditController() - editController.reactor = NormalCommentEditReactor(popUpID: self.popUpID, popUpName: popUpName, comment: comment) + editController.reactor = NormalCommentEditReactor( + popUpID: self.popUpID, + popUpName: popUpName, + comment: comment, + commentAPIUseCase: self.commentAPIUseCase, + preSignedUseCase: self.preSignedUseCase + ) controller?.navigationController?.pushViewController(editController, animated: true) } case .cancel: @@ -527,7 +576,7 @@ final class DetailReactor: Reactor { class ItemDetailSource: NSObject { let name: String let image: UIImage - + init(name: String, image: UIImage) { self.name = name self.image = image @@ -535,14 +584,14 @@ class ItemDetailSource: NSObject { } extension ItemDetailSource: UIActivityItemSource { - + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { image } func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { image } - + func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { let metaData = LPLinkMetadata() metaData.title = name diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/FactoryImpl/DetailFactoryImpl.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/FactoryImpl/DetailFactoryImpl.swift new file mode 100644 index 00000000..d4339120 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/FactoryImpl/DetailFactoryImpl.swift @@ -0,0 +1,23 @@ +import DesignSystem +import DomainInterface +import Infrastructure +import PresentationInterface + +public final class DetailFactoryImpl: DetailFactory { + public init() { } + + public func make(popupID: Int) -> BaseViewController { + let viewController = DetailController() + let reactor = DetailReactor( + popUpID: Int64(popupID), + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) + + viewController.reactor = reactor + + return viewController + } +} diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift index b94dfefb..e2702c91 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift @@ -1,17 +1,12 @@ -// -// DetailCommentImageCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailCommentImageCell: UICollectionViewCell { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() @@ -19,14 +14,14 @@ final class DetailCommentImageCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -46,7 +41,7 @@ extension DetailCommentImageCell: Inputable { struct Input { var imagePath: String? } - + func injection(with input: Input) { imageView.setPPImage(path: input.imagePath) } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift index fdbada94..ea9b6839 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift @@ -1,16 +1,11 @@ -// -// DetailCommentProfileView.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit +import DesignSystem + import SnapKit final class DetailCommentProfileView: UIStackView { - + // MARK: - Components let profileImageView: UIImageView = { let view = UIImageView() @@ -19,47 +14,46 @@ final class DetailCommentProfileView: UIStackView { view.contentMode = .scaleAspectFill return view }() - + let contentStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 3 return view }() - + let nickNameLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 13) - return label + return PPLabel(style: .bold, fontSize: 13) }() - + let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.textColor = .g400 return label }() - + let button: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_comment_button"), for: .normal) return button }() - + let spacingView: UIView = UIView() // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + } // MARK: - SetUp private extension DetailCommentProfileView { - + func setUpConstraints() { self.alignment = .center self.spacing = 12 diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift index ada32f6b..478205b0 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift @@ -1,26 +1,21 @@ -// -// DetailCommentSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailCommentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailCommentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailCommentSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift index 6df1feb5..b270dcd1 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift @@ -1,17 +1,12 @@ -// -// DetailCommentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailCommentSectionCell: UICollectionViewCell { - + // MARK: - Components private let contentStackView: UIStackView = { let view = UIStackView() @@ -20,12 +15,11 @@ final class DetailCommentSectionCell: UICollectionViewCell { view.spacing = 16 return view }() - + let profileView: DetailCommentProfileView = { - let view = DetailCommentProfileView() - return view + return DetailCommentProfileView() }() - + let imageCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.itemSize = .init(width: 80, height: 80) @@ -36,53 +30,51 @@ final class DetailCommentSectionCell: UICollectionViewCell { view.showsHorizontalScrollIndicator = false return view }() - + private let contentLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 13) label.numberOfLines = 3 return label }() - + let totalViewButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let buttonTitleLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 13, text: "코멘트 전체보기") label.textColor = .g600 return label }() - + private let buttonImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_black") return view }() - + let likeButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let likeButtonTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13, text: "도움돼요") label.textColor = .g400 return label }() - + private let likeButtonImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_like_gray") return view }() - + private let borderView: UIView = { let view = UIView() view.backgroundColor = .g100 return view }() - + private let blurBackGroundView: UIView = { let view = UIView() view.backgroundColor = .white @@ -95,9 +87,9 @@ final class DetailCommentSectionCell: UICollectionViewCell { view.isUserInteractionEnabled = false return view }() - + var disposeBag = DisposeBag() - + private let loginStackView: UIStackView = { let view = UIStackView() view.axis = .vertical @@ -105,35 +97,35 @@ final class DetailCommentSectionCell: UICollectionViewCell { view.spacing = 16 return view }() - + private let loginNoticelabel: UILabel = { let label = UILabel() label.numberOfLines = 2 return label }() - + let loginButton: UIButton = { let button = UIButton() button.setTitle("로그인하고 후기보기", for: .normal) - button.titleLabel?.font = .KorFont(style: .medium, size: 13) + button.titleLabel?.font = .korFont(style: .medium, size: 13) button.setTitleColor(.w100, for: .normal) button.layer.cornerRadius = 4 button.backgroundColor = .blu500 return button }() - + private var imagePathList: [String?] = [] // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -143,12 +135,11 @@ final class DetailCommentSectionCell: UICollectionViewCell { // MARK: - SetUp private extension DetailCommentSectionCell { func setUpConstraints() { - + imageCollectionView.delegate = self imageCollectionView.dataSource = self imageCollectionView.register(DetailCommentImageCell.self, forCellWithReuseIdentifier: DetailCommentImageCell.identifiers) - - + totalViewButton.addSubview(buttonTitleLabel) buttonTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() @@ -163,7 +154,7 @@ private extension DetailCommentSectionCell { make.leading.equalTo(buttonTitleLabel.snp.trailing) make.centerY.equalToSuperview() } - + profileView.snp.makeConstraints { make in make.width.equalTo(contentView.bounds.width - 40).priority(.high) } @@ -178,32 +169,32 @@ private extension DetailCommentSectionCell { contentStackView.addArrangedSubview(imageCollectionView) contentStackView.addArrangedSubview(contentLabel) contentStackView.addArrangedSubview(totalViewButton) - + contentView.addSubview(contentStackView) contentStackView.snp.makeConstraints { make in make.top.equalToSuperview().inset(20) make.leading.trailing.equalToSuperview() } - + likeButton.addSubview(likeButtonTitleLabel) likeButtonTitleLabel.snp.makeConstraints { make in make.height.equalTo(20).priority(.high) make.top.bottom.trailing.equalToSuperview() } - + likeButton.addSubview(likeButtonImageView) likeButtonImageView.snp.makeConstraints { make in make.size.equalTo(20) make.leading.centerY.equalToSuperview() make.trailing.equalTo(likeButtonTitleLabel.snp.leading) } - + contentView.addSubview(likeButton) likeButton.snp.makeConstraints { make in make.top.equalTo(contentStackView.snp.bottom).offset(16) make.trailing.equalToSuperview().inset(20) } - + contentView.addSubview(borderView) borderView.snp.makeConstraints { make in make.top.equalTo(likeButton.snp.bottom).offset(16) @@ -211,7 +202,7 @@ private extension DetailCommentSectionCell { make.height.equalTo(1).priority(.high) make.bottom.equalToSuperview() } - + contentView.addSubview(blurBackGroundView) blurBackGroundView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -220,7 +211,7 @@ private extension DetailCommentSectionCell { blurView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + contentView.addSubview(loginStackView) loginStackView.snp.makeConstraints { make in make.centerY.equalToSuperview() @@ -281,15 +272,15 @@ extension DetailCommentSectionCell: Inputable { var isMyComment: Bool var isLastCell: Bool = false } - + func injection(with input: Input) { let comment = input.comment ?? "" profileView.profileImageView.setPPImage(path: input.profileImagePath) - profileView.nickNameLabel.setLineHeightText(text: input.nickName, font: .KorFont(style: .bold, size: 13)) - profileView.dateLabel.setLineHeightText(text: input.date, font: .KorFont(style: .regular, size: 12)) - contentLabel.setLineHeightText(text: input.comment, font: .KorFont(style: .regular, size: 13)) - likeButtonTitleLabel.setLineHeightText(text: "도움돼요 \(input.likeCount)", font: .KorFont(style: .regular, size: 13)) + profileView.nickNameLabel.setLineHeightText(text: input.nickName, font: .korFont(style: .bold, size: 13)) + profileView.dateLabel.setLineHeightText(text: input.date, font: .korFont(style: .regular, size: 12)) + contentLabel.setLineHeightText(text: input.comment, font: .korFont(style: .regular, size: 13)) + likeButtonTitleLabel.setLineHeightText(text: "도움돼요 \(input.likeCount)", font: .korFont(style: .regular, size: 13)) if input.isLike { likeButtonImageView.image = UIImage(named: "icon_like_blue") likeButtonTitleLabel.textColor = .blu500 @@ -308,7 +299,7 @@ extension DetailCommentSectionCell: Inputable { imageCollectionView.isHidden = false imagePathList = input.imageList } - + imageCollectionView.reloadData() if input.isLogin { blurBackGroundView.isHidden = true @@ -327,23 +318,23 @@ extension DetailCommentSectionCell: Inputable { let reviewRange = (fullText as NSString).range(of: "생생한 후기") let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.4 - + // 기본 스타일 (폰트, 색상 등) let normalAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.KorFont(style: .regular, size: 14)!, + .font: UIFont.korFont(style: .regular, size: 14), .foregroundColor: UIColor.g1000, .paragraphStyle: paragraphStyle ] // 스타일을 다르게 할 부분 (팝업스토어명, 생생한 후기) let popupStoreAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.KorFont(style: .bold, size: 14)!, // 다른 폰트 스타일 + .font: UIFont.korFont(style: .bold, size: 14), // 다른 폰트 스타일 .foregroundColor: UIColor.blu500, // 다른 색상 .paragraphStyle: paragraphStyle ] let reviewAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.KorFont(style: .bold, size: 14)!, // 이탤릭체 + .font: UIFont.korFont(style: .bold, size: 14), // 이탤릭체 .foregroundColor: UIColor.g1000, // 다른 색상 .paragraphStyle: paragraphStyle ] @@ -353,12 +344,11 @@ extension DetailCommentSectionCell: Inputable { attributedString.addAttributes(popupStoreAttributes, range: popupStoreRange) attributedString.addAttributes(reviewAttributes, range: reviewRange) - loginNoticelabel.attributedText = attributedString loginNoticelabel.textAlignment = .center loginNoticelabel.lineBreakStrategy = .hangulWordPriority loginNoticelabel.numberOfLines = 0 - + borderView.isHidden = input.isLastCell } } @@ -368,7 +358,7 @@ extension DetailCommentSectionCell: UICollectionViewDelegate, UICollectionViewDa func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return imagePathList.count } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift index d1a25558..48012717 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift @@ -1,26 +1,21 @@ -// -// DetailCommentTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/18/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailCommentTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailCommentTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailCommentTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift similarity index 83% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift index a285a972..c0ebbf91 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift @@ -1,35 +1,29 @@ -// -// DetailCommentTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/18/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailCommentTitleSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16, text: "이 팝업에 대한 코멘트") - return label + return PPLabel(style: .bold, fontSize: 16, text: "이 팝업에 대한 코멘트") }() - + private let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g600 return label }() - + let totalViewButton: UIButton = { let button = UIButton() let attributedTitle = NSAttributedString( string: "전체보기", attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 커스텀 폰트 적용 + .font: UIFont.korFont(style: .regular, size: 13), // 커스텀 폰트 적용 .underlineStyle: NSUnderlineStyle.single.rawValue // 밑줄 스타일 ] ) @@ -38,16 +32,16 @@ final class DetailCommentTitleSectionCell: UICollectionViewCell { }() var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -61,13 +55,13 @@ private extension DetailCommentTitleSectionCell { titleLabel.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + contentView.addSubview(countLabel) countLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(5) make.leading.bottom.equalToSuperview() } - + contentView.addSubview(totalViewButton) totalViewButton.snp.makeConstraints { make in make.centerY.equalToSuperview() @@ -81,9 +75,9 @@ extension DetailCommentTitleSectionCell: Inputable { var commentCount: Int64 var buttonIsHidden: Bool = false } - + func injection(with input: Input) { - countLabel.setLineHeightText(text: "총 \(input.commentCount)개", font: .KorFont(style: .regular, size: 13)) + countLabel.setLineHeightText(text: "총 \(input.commentCount)개", font: .korFont(style: .regular, size: 13)) totalViewButton.isHidden = input.buttonIsHidden } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift index 24a4e9d4..5f1ee0cc 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift @@ -1,26 +1,21 @@ -// -// DetailContentSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailContentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailContentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailContentSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift index aa7c0f73..012be73b 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift @@ -1,17 +1,12 @@ -// -// DetailContentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailContentSectionCell: UICollectionViewCell { - + // MARK: - Components private let contentStackView: UIStackView = { @@ -26,38 +21,37 @@ final class DetailContentSectionCell: UICollectionViewCell { label.numberOfLines = 3 return label }() - + let dropDownButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let buttonTitleLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 13, text: "더보기") label.textColor = .g600 return label }() - + let buttonImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown_bottom_gray") return view }() - + var isOpen: Bool = false - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -97,10 +91,10 @@ extension DetailContentSectionCell: Inputable { struct Input { var content: String? } - + func injection(with input: Input) { let text = input.content ?? "" - contentLabel.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 13)) + contentLabel.setLineHeightText(text: text, font: .korFont(style: .regular, size: 13)) if text.count >= 68 { dropDownButton.isHidden = false } else { diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift index 45ae3913..3cbc9fda 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift @@ -1,26 +1,21 @@ -// -// DetailEmptyCommetSection.swift -// Poppool -// -// Created by SeoJunYoung on 2/4/25. -// - import UIKit +import DesignSystem + import RxSwift struct DetailEmptyCommetSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailEmptyCommetSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailEmptyCommetSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift index a5a1e031..58fb2834 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift @@ -1,52 +1,47 @@ -// -// DetailEmptyCommetSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 2/4/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailEmptyCommetSectionCell: UICollectionViewCell { - + // MARK: - Components private let noticeLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 14, text: "아직 작성된 코멘트가 없어요\n가장 먼저 후기를 남겨주시겠어요?" , lineHeight: 1.5) + let label = PPLabel(style: .medium, fontSize: 14, text: "아직 작성된 코멘트가 없어요\n가장 먼저 후기를 남겨주시겠어요?", lineHeight: 1.5) label.textAlignment = .center label.numberOfLines = 2 label.textColor = .g400 return label }() - + let commentButton: UIButton = { let button = UIButton() let attributedTitle = NSAttributedString( string: "첫번째 코멘트 남기기", attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 커스텀 폰트 적용 + .font: UIFont.korFont(style: .regular, size: 13), // 커스텀 폰트 적용 .underlineStyle: NSUnderlineStyle.single.rawValue // 밑줄 스타일 ] ) button.setAttributedTitle(attributedTitle, for: .normal) return button }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -61,7 +56,7 @@ private extension DetailEmptyCommetSectionCell { make.top.equalToSuperview().inset(80) make.centerX.equalToSuperview() } - + contentView.addSubview(commentButton) commentButton.snp.makeConstraints { make in make.top.equalTo(noticeLabel.snp.bottom).offset(26) @@ -74,7 +69,7 @@ extension DetailEmptyCommetSectionCell: Inputable { struct Input { } - + func injection(with input: Input) { } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift index d21f060a..0bc8ff41 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift @@ -1,26 +1,21 @@ -// -// DetailInfoSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/18/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailInfoSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailInfoSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailInfoSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift similarity index 83% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift index e509e3c4..160a5eee 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift @@ -1,85 +1,74 @@ -// -// DetailInfoSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/18/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailInfoSectionCell: UICollectionViewCell { - + // MARK: - Components - + private let dateTitleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 13, text: "기간") - return label + return PPLabel(style: .bold, fontSize: 13, text: "기간") }() - + private let dateLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 14) - return label + return PPLabel(style: .regular, fontSize: 14) }() - + private let timeTitleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 13, text: "시간") - return label + return PPLabel(style: .bold, fontSize: 13, text: "시간") }() - + private let timeLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 14) - return label + return PPLabel(style: .regular, fontSize: 14) }() - + private let addressTitleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 13, text: "주소") - return label + return PPLabel(style: .bold, fontSize: 13, text: "주소") }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14) label.numberOfLines = 2 return label }() - + let copyButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_copy_gray"), for: .normal) return button }() - + let mapButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let mapButtonTitle: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13, text: "찾아가는 길") label.textColor = .blu500 return label }() - + let mapButtonImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_arrow_right_blue") return view }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -94,36 +83,36 @@ private extension DetailInfoSectionCell { make.top.equalToSuperview() make.leading.equalToSuperview() } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.centerY.equalTo(dateTitleLabel) make.leading.equalTo(dateTitleLabel.snp.trailing).offset(12) } - + contentView.addSubview(timeTitleLabel) timeTitleLabel.snp.makeConstraints { make in make.top.equalTo(dateTitleLabel.snp.bottom).offset(11) make.leading.equalToSuperview() } - + contentView.addSubview(timeLabel) timeLabel.snp.makeConstraints { make in make.centerY.equalTo(timeTitleLabel) make.leading.equalTo(timeTitleLabel.snp.trailing).offset(12) } - + contentView.addSubview(addressTitleLabel) addressTitleLabel.snp.makeConstraints { make in make.top.equalTo(timeTitleLabel.snp.bottom).offset(11) make.leading.equalToSuperview() } - + mapButton.addSubview(mapButtonTitle) mapButtonTitle.snp.makeConstraints { make in make.leading.centerY.equalToSuperview() } - + mapButton.addSubview(mapButtonImageView) mapButtonImageView.snp.makeConstraints { make in make.leading.equalTo(mapButtonTitle.snp.trailing) @@ -131,13 +120,13 @@ private extension DetailInfoSectionCell { make.trailing.equalToSuperview() make.centerY.equalToSuperview().offset(0.5) } - + contentView.addSubview(mapButton) mapButton.snp.makeConstraints { make in make.centerY.equalTo(addressTitleLabel) make.trailing.equalToSuperview() } - + contentView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.top.equalTo(addressTitleLabel).offset(-2) @@ -145,14 +134,13 @@ private extension DetailInfoSectionCell { make.width.lessThanOrEqualTo(188) make.bottom.equalToSuperview() } - + contentView.addSubview(copyButton) copyButton.snp.makeConstraints { make in make.size.equalTo(16) make.top.equalTo(addressLabel).offset(1) make.leading.equalTo(addressLabel.snp.trailing).offset(2) } - } } @@ -165,16 +153,15 @@ extension DetailInfoSectionCell: Inputable { var endTime: String? var address: String? } - + func injection(with input: Input) { let startDate = input.startDate ?? "?" let endDate = input.endDate ?? "?" let startTime = input.startTime ?? "?" let endTime = input.endTime ?? "?" - - dateLabel.setLineHeightText(text: startDate + " ~ " + endDate, font: .KorFont(style: .regular, size: 14)) - timeLabel.setLineHeightText(text: startTime + " ~ " + endTime, font: .KorFont(style: .regular, size: 14)) - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .regular, size: 13)) + + dateLabel.setLineHeightText(text: startDate + " ~ " + endDate, font: .korFont(style: .regular, size: 14)) + timeLabel.setLineHeightText(text: startTime + " ~ " + endTime, font: .korFont(style: .regular, size: 14)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .regular, size: 13)) } } - diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift index 664f296e..245ade0c 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift @@ -1,26 +1,21 @@ -// -// DetailSimilarSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailSimilarSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailSimilarSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(163), @@ -39,7 +34,7 @@ struct DetailSimilarSection: Sectionable { section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.orthogonalScrollingBehavior = .continuous section.interGroupSpacing = 12 - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift index d746d9ae..265b2a10 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift @@ -1,17 +1,12 @@ -// -// DetailSimilarSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/19/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailSimilarSectionCell: UICollectionViewCell { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() @@ -19,28 +14,26 @@ final class DetailSimilarSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) - label.font = .EngFont(style: .regular, size: 11) + label.font = .engFont(style: .regular, size: 11) label.textColor = .g400 return label }() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 12) - return label + return PPLabel(style: .bold, fontSize: 12) }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let trailingView: UIView = UIView() - + var disposeBag = DisposeBag() - + // MARK: - init override init(frame: CGRect) { super.init(frame: frame) @@ -52,12 +45,11 @@ final class DetailSimilarSectionCell: UICollectionViewCell { addHolesToCell() } - + required init?(coder: NSCoder) { fatalError() } - - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -65,27 +57,27 @@ final class DetailSimilarSectionCell: UICollectionViewCell { private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(roundedRect: contentView.bounds, cornerRadius: 4) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let leftHoleCenter = CGPoint(x: contentView.bounds.minX, y: 190) let rightHoleCenter = CGPoint(x: contentView.bounds.maxX, y: 190) - + // 구멍을 만드는 경로 생성 (반지름 6) let leftHolePath = UIBezierPath(arcCenter: leftHoleCenter, radius: 6, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: true) let rightHolePath = UIBezierPath(arcCenter: rightHoleCenter, radius: 6, startAngle: .pi / 2, endAngle: -.pi / 2, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(leftHolePath) fullPath.append(rightHolePath) fullPath.usesEvenOddFillRule = true - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd holeLayer.fillColor = UIColor.black.cgColor trailingView.layer.mask = holeLayer - + // 그림자 Layer let shadowLayer = CAShapeLayer() shadowLayer.path = fullPath.cgPath @@ -106,25 +98,25 @@ private extension DetailSimilarSectionCell { trailingView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + trailingView.addSubview(imageView) imageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(190) } - + trailingView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(10) make.leading.trailing.equalToSuperview().inset(12) } - + trailingView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(12) make.top.equalTo(dateLabel.snp.bottom).offset(5.5) } - + trailingView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(20) @@ -141,12 +133,12 @@ extension DetailSimilarSectionCell: Inputable { var id: Int64 var isBookMark: Bool? } - + func injection(with input: Input) { let date = input.date ?? "" imageView.setPPImage(path: input.imagePath) - dateLabel.setLineHeightText(text: "~" + date, font: .EngFont(style: .regular, size: 11)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 12)) + dateLabel.setLineHeightText(text: "~" + date, font: .engFont(style: .regular, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 12)) if let isBookMark = input.isBookMark { bookMarkButton.isHidden = false if isBookMark { diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift index f99f45b0..674ee593 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift @@ -1,26 +1,21 @@ -// -// DetailTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - import UIKit +import DesignSystem + import RxSwift struct DetailTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct DetailTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift index b2a86cce..d014e76d 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift @@ -1,47 +1,41 @@ -// -// DetailTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/10/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class DetailTitleSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18) label.numberOfLines = 2 return label }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let sharedButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_shared"), for: .normal) return button }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -56,14 +50,14 @@ private extension DetailTitleSectionCell { make.size.equalTo(28) make.top.trailing.equalToSuperview() } - + contentView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(28) make.top.equalToSuperview() make.trailing.equalTo(sharedButton.snp.leading).offset(-4) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.leading.equalToSuperview() @@ -79,11 +73,11 @@ extension DetailTitleSectionCell: Inputable { var isBookMark: Bool var isLogin: Bool } - + func injection(with input: Input) { let bookMarkImage = input.isBookMark ? UIImage(named: "icon_bookmark_blue") : UIImage(named: "icon_bookmark_gray") bookMarkButton.setImage(bookMarkImage, for: .normal) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 18)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 18)) bookMarkButton.isHidden = !input.isLogin } } diff --git a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Detail/View/DetailView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailView.swift index a79b9333..fabc06b0 100644 --- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/DetailView.swift @@ -1,28 +1,22 @@ -// -// DetailView.swift -// Poppool -// -// Created by SeoJunYoung on 12/9/24. -// - import UIKit +import DesignSystem + import SnapKit final class DetailView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.contentInsetAdjustmentBehavior = .never return view }() - + let commentPostButton: PPButton = { - let button = PPButton(style: .primary, text: "코멘트 작성하기", disabledText: "코멘트 작성 완료") - return button + return PPButton(style: .primary, text: "코멘트 작성하기", disabledText: "코멘트 작성 완료") }() - + private let buttonTopView: UIView = { var view = UIView() view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 32) @@ -39,13 +33,13 @@ final class DetailView: UIView { view.layer.addSublayer(gradientLayer) return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,21 +47,20 @@ final class DetailView: UIView { // MARK: - SetUp private extension DetailView { - + func setUpConstraints() { self.addSubview(commentPostButton) commentPostButton.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(52) } - self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(commentPostButton.snp.top) } - + self.addSubview(buttonTopView) buttonTopView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SearchTitleSection.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SearchTitleSection.swift index aa09c76a..ec700e25 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SearchTitleSection.swift @@ -1,26 +1,21 @@ -// -// SearchTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - import UIKit +import DesignSystem + import RxSwift -struct SearchTitleSection: Sectionable { - +struct SimilarTitleSection: Sectionable { + var currentPage: PublishSubject = .init() - - typealias CellType = SearchTitleSectionCell - + + typealias CellType = SimilarTitleSectionCell + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct SearchTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SimilarTitleSectionCell.swift similarity index 78% rename from Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SimilarTitleSectionCell.swift index adf7d9da..8226579f 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Detail/View/SimilarTitleSection/SimilarTitleSectionCell.swift @@ -1,42 +1,36 @@ -// -// SearchTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/4/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit + +final class SimilarTitleSectionCell: UICollectionViewCell { -final class SearchTitleSectionCell: UICollectionViewCell { - // MARK: - Components var disposeBag = DisposeBag() - + private let sectionTitleLabel: UILabel = { let label = UILabel() - label.font = .KorFont(style: .bold, size: 16) + label.font = .korFont(style: .bold, size: 16) return label }() - + let titleButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -44,7 +38,7 @@ final class SearchTitleSectionCell: UICollectionViewCell { } // MARK: - SetUp -private extension SearchTitleSectionCell { +private extension SimilarTitleSectionCell { func setUpConstraints() { self.addSubview(sectionTitleLabel) sectionTitleLabel.snp.makeConstraints { make in @@ -52,7 +46,7 @@ private extension SearchTitleSectionCell { make.centerY.equalToSuperview() make.height.equalTo(22) } - + self.addSubview(titleButton) titleButton.snp.makeConstraints { make in make.centerY.equalToSuperview() @@ -62,19 +56,19 @@ private extension SearchTitleSectionCell { } } -extension SearchTitleSectionCell: Inputable { +extension SimilarTitleSectionCell: Inputable { struct Input { var title: String? var buttonTitle: String? } - + func injection(with input: Input) { sectionTitleLabel.text = input.title if let buttonTitle = input.buttonTitle { titleButton.isHidden = false let attributes: [NSAttributedString.Key: Any] = [ .underlineStyle: NSUnderlineStyle.single.rawValue, - .font: UIFont.KorFont(style: .regular, size: 13)! + .font: UIFont.korFont(style: .regular, size: 13) ] let attributedTitle = NSAttributedString(string: buttonTitle, attributes: attributes) titleButton.setAttributedTitle(attributedTitle, for: .normal) diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListController.swift index ce91a1f1..50d0c9bc 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListController.swift @@ -1,30 +1,25 @@ -// -// HomeListController.swift -// Poppool -// -// Created by SeoJunYoung on 12/2/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class HomeListController: BaseViewController, View { - + typealias Reactor = HomeListReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = HomeListView() - + private var sections: [any Sectionable] = [] - + private let pageChange: PublishSubject = .init() - + private let cellTapped: PublishSubject = .init() } @@ -33,9 +28,9 @@ extension HomeListController { override func viewDidLoad() { super.viewDidLoad() setUp() - + } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -49,13 +44,13 @@ private extension HomeListController { mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) } - + if let layout = reactor?.compositionalLayout { mainView.contentCollectionView.collectionViewLayout = layout } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( HomeCardSectionCell.self, forCellWithReuseIdentifier: HomeCardSectionCell.identifiers @@ -74,7 +69,7 @@ extension HomeListController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -82,13 +77,13 @@ extension HomeListController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + pageChange .throttle(.milliseconds(1000), scheduler: MainScheduler.asyncInstance) .map { Reactor.Action.changePage } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -96,7 +91,7 @@ extension HomeListController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -113,11 +108,11 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -130,10 +125,10 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -142,7 +137,7 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour pageChange.onNext(()) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 1 { cellTapped.onNext(indexPath.row)} } diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListReactor.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListReactor.swift index 56480168..9dc1332a 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomeListReactor.swift @@ -1,18 +1,15 @@ -// -// HomeListReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/2/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class HomeListReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -21,7 +18,7 @@ final class HomeListReactor: Reactor { case changePage case cellTapped(controller: BaseViewController, row: Int) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) case loadView @@ -30,28 +27,28 @@ final class HomeListReactor: Reactor { case appendData case moveToDetailScene(controller: BaseViewController, row: Int) } - + struct State { var popUpType: HomePopUpType var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var popUpType: HomePopUpType - - private let homeAPIUseCase = HomeAPIUseCaseImpl() + + private let homeAPIUseCase: HomeAPIUseCase private let userDefaultService = UserDefaultService() - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + private let userAPIUseCase: UserAPIUseCase + private var isLoading: Bool = false private var totalPage: Int32 = 0 private var currentPage: Int32 = 0 private var size: Int32 = 10 - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -68,13 +65,19 @@ final class HomeListReactor: Reactor { private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) private var cardSections = HomeCardGridSection(inputDataList: []) - + // MARK: - init - init(popUpType: HomePopUpType) { + init( + popUpType: HomePopUpType, + userAPIUseCase: UserAPIUseCase, + homeAPIUseCase: HomeAPIUseCase + ) { self.initialState = State(popUpType: popUpType) self.popUpType = popUpType + self.userAPIUseCase = userAPIUseCase + self.homeAPIUseCase = homeAPIUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -122,7 +125,7 @@ final class HomeListReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -144,16 +147,22 @@ final class HomeListReactor: Reactor { isLoading = false case .moveToDetailScene(let controller, let row): let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: cardSections.inputDataList[row].id) + nextController.reactor = DetailReactor( + popUpID: cardSections.inputDataList[row].id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) } return newState } - + func getSection() -> [any Sectionable] { - return [spacing24Section,cardSections,spacing64Section] + return [spacing24Section, cardSections, spacing64Section] } - + func setSection(response: GetHomeInfoResponse) { let isLogin = response.loginYn switch popUpType { @@ -206,7 +215,7 @@ final class HomeListReactor: Reactor { totalPage = response.popularPopUpStoreTotalPages } } - + func appendSectionData(response: GetHomeInfoResponse) { let isLogin = response.loginYn switch popUpType { diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomePopUpType.swift similarity index 98% rename from Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomePopUpType.swift index d257675a..2a2a6071 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/HomePopUpType.swift @@ -11,7 +11,7 @@ enum HomePopUpType { case curation case new case popular - + var title: String { switch self { case .curation: @@ -22,7 +22,7 @@ enum HomePopUpType { return "인기 팝업 전체보기" } } - + var path: String { switch self { case .curation: diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeCardGridSection.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeCardGridSection.swift index 185069a7..404ee0f9 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeCardGridSection.swift @@ -7,20 +7,22 @@ import UIKit +import DesignSystem + import RxSwift struct HomeCardGridSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 16) / 2), diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeListView.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeListView.swift index 7db0f713..3ff3adc7 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/List/View/HomeListView.swift @@ -1,33 +1,26 @@ -// -// HomeListView.swift -// Poppool -// -// Created by SeoJunYoung on 12/2/24. -// - import UIKit +import DesignSystem + import SnapKit final class HomeListView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { - let view = PPReturnHeaderView() - return view + return PPReturnHeaderView() }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,13 +28,13 @@ final class HomeListView: UIView { // MARK: - SetUp private extension HomeListView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeController.swift index 5a507a7d..508c6c07 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeController.swift @@ -1,35 +1,29 @@ -// -// HomeController.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class HomeController: BaseViewController, View { - + typealias Reactor = HomeReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = HomeView() - + let homeHeaderView: HomeHeaderView = { - let view = HomeHeaderView() - return view + return HomeHeaderView() }() - + private let headerBackgroundView: UIView = UIView() let backGroundblurEffect = UIBlurEffect(style: .regular) lazy var backGroundblurView = UIVisualEffectView(effect: backGroundblurEffect) - + private var sections: [any Sectionable] = [] } @@ -39,7 +33,7 @@ extension HomeController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = false @@ -55,58 +49,57 @@ private extension HomeController { } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( ImageBannerSectionCell.self, forCellWithReuseIdentifier: ImageBannerSectionCell.identifiers ) - + mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomeTitleSectionCell.self, forCellWithReuseIdentifier: HomeTitleSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomeCardSectionCell.self, forCellWithReuseIdentifier: HomeCardSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomePopularCardSectionCell.self, forCellWithReuseIdentifier: HomePopularCardSectionCell.identifiers ) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.top.equalToSuperview() make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) } - + view.addSubview(homeHeaderView) homeHeaderView.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide).inset(7) + make.top.equalTo(view.safeAreaLayoutGuide).inset(12) make.leading.trailing.equalToSuperview() } - + headerBackgroundView.addSubview(backGroundblurView) backGroundblurView.snp.makeConstraints { make in make.edges.equalToSuperview() } backGroundblurView.isUserInteractionEnabled = false backGroundblurView.isHidden = true - + view.addSubview(headerBackgroundView) headerBackgroundView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(homeHeaderView.snp.bottom).offset(7) } - view.bringSubviewToFront(homeHeaderView) } } @@ -118,7 +111,7 @@ extension HomeController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + homeHeaderView.searchBarButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -126,7 +119,7 @@ extension HomeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -142,18 +135,18 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? ImageBannerSectionCell { cell.bannerTapped .withUnretained(self) @@ -162,7 +155,7 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { }) .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.imageSection.currentPage .distinctUntilChanged() .withUnretained(self) @@ -182,17 +175,17 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? HomeCardSectionCell { cell.bookmarkButton.rx.tap .map { Reactor.Action.bookMarkButtonTapped(indexPath: indexPath)} .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.contentOffset.y <= (307 - headerBackgroundView.frame.maxY) { backGroundblurView.isHidden = true @@ -201,7 +194,7 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { backGroundblurView.isHidden = false } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { reactor?.action.onNext(.collectionViewCellTapped(controller: self, indexPath: indexPath)) } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeReactor.swift similarity index 74% rename from Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeReactor.swift index d7bdfb8a..4a45bd65 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/HomeReactor.swift @@ -1,18 +1,16 @@ -// -// HomeReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure +import SearchFeatureInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class HomeReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -24,7 +22,7 @@ final class HomeReactor: Reactor { case bannerCellTapped(controller: BaseViewController, row: Int) case changeIndicatorColor(controller: BaseViewController, row: Int) } - + enum Mutation { case loadView case setHedaerState(isDarkMode: Bool) @@ -33,23 +31,23 @@ final class HomeReactor: Reactor { case moveToSearchScene(controller: BaseViewController) case skip } - + struct State { var sections: [any Sectionable] = [] var headerIsDarkMode: Bool = true var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State - + var disposeBag = DisposeBag() - - private let homeApiUseCase = HomeAPIUseCaseImpl() - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) + + private let homeAPIUseCase: HomeAPIUseCase + private let userAPIUseCase: UserAPIUseCase private let userDefaultService = UserDefaultService() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -70,7 +68,16 @@ final class HomeReactor: Reactor { private var popularTitleSection = HomeTitleSection(inputDataList: [ .init(blueText: "팝풀이", topSubText: "들은 지금 이런", bottomText: "팝업에 가장 관심있어요", backgroundColor: .g700, textColor: .w100) ]) - private var popularSection = HomePopularCardSection(inputDataList: [], decorationItems: [SectionDecorationItem(elementKind: "BackgroundView", reusableView: SectionBackGroundDecorationView(), viewInput: .init(backgroundColor: .g700))]) + private var popularSection = HomePopularCardSection( + inputDataList: [], + decorationItems: [ + SectionDecorationItem( + elementKind: "BackgroundView", + reusableView: SectionBackGroundDecorationView(), + viewInput: .init(backgroundColor: .g700) + ) + ] + ) private var newTitleSection = HomeTitleSection(inputDataList: [.init(blueText: "제일 먼저", topSubText: "피드 올리는", bottomText: "신규 오픈 팝업")]) private var newSection = HomeCardSection(inputDataList: []) private var spaceClear48Section = SpacingSection(inputDataList: [.init(spacing: 48)]) @@ -80,19 +87,24 @@ final class HomeReactor: Reactor { private var spaceGray40Section = SpacingSection(inputDataList: [.init(spacing: 40, backgroundColor: .g700)]) private var spaceGray28Section = SpacingSection(inputDataList: [.init(spacing: 28, backgroundColor: .g700)]) private var spaceGray24Section = SpacingSection(inputDataList: [.init(spacing: 24, backgroundColor: .g700)]) - + // MARK: - init - init() { + init( + userAPIUseCase: UserAPIUseCase, + homeAPIUseCase: HomeAPIUseCase + ) { + self.userAPIUseCase = userAPIUseCase + self.homeAPIUseCase = homeAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { case .changeIndicatorColor(let controller, let row): return Observable.just(.skip) case .viewWillAppear: - return homeApiUseCase.fetchHome(page: 0, size: 6, sort: "viewCount,desc") + return homeAPIUseCase.fetchHome(page: 0, size: 6, sort: "viewCount,desc") .withUnretained(self) .map { (owner, response) in owner.setBannerSection(response: response) @@ -125,15 +137,14 @@ final class HomeReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, indexPath: IndexPath(row: row, section: 0))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false switch mutation { case .moveToSearchScene(let controller): - let nextController = SearchMainController() - nextController.reactor = SearchMainReactor() - controller.navigationController?.pushViewController(nextController, animated: true) + @Dependency var factory: PopupSearchFactory + controller.navigationController?.pushViewController(factory.make(), animated: true) case .loadView: newState.isReloadView = true newState.sections = getSection() @@ -162,9 +173,9 @@ final class HomeReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { - + if isLoign { return [ loginImageBannerSection, @@ -193,7 +204,7 @@ final class HomeReactor: Reactor { } } - + func getNewSection() -> [any Sectionable] { if newSection.isEmpty { return [] @@ -206,17 +217,17 @@ final class HomeReactor: Reactor { ] } } - + func setBannerSection(response: GetHomeInfoResponse) { let imagePaths = response.bannerPopUpStoreList.map { $0.mainImageUrl } let idList = response.bannerPopUpStoreList.map { $0.id } loginImageBannerSection.inputDataList = imagePaths.isEmpty ? [] : [.init(imagePaths: imagePaths, idList: idList)] } - + func setCurationTitleSection(response: GetHomeInfoResponse) { curationTitleSection.inputDataList = [.init(blueText: response.nickname, topSubText: "님을 위한", bottomText: "맞춤 팝업 큐레이션")] } - + func setCurationSection(response: GetHomeInfoResponse) { let islogin = response.loginYn curationSection.inputDataList = response.customPopUpStoreList.map({ response in @@ -233,7 +244,7 @@ final class HomeReactor: Reactor { ) }) } - + func setPopularSection(response: GetHomeInfoResponse) { popularSection.inputDataList = response.popularPopUpStoreList.map({ response in return .init( @@ -246,7 +257,7 @@ final class HomeReactor: Reactor { ) }) } - + func setNewSection(response: GetHomeInfoResponse) { let islogin = response.loginYn newSection.inputDataList = response.newPopUpStoreList.map({ response in @@ -263,42 +274,78 @@ final class HomeReactor: Reactor { ) }) } - + func getDetailController(indexPath: IndexPath, currentController: BaseViewController) { if isLoign { switch indexPath.section { case 0: if let id = loginImageBannerSection.inputDataList.first?.idList[indexPath.row - 1] { let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) } case 2: let controller = HomeListController() - controller.reactor = HomeListReactor(popUpType: .curation) + controller.reactor = HomeListReactor( + popUpType: .curation, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: homeAPIUseCase + ) currentController.navigationController?.pushViewController(controller, animated: true) case 4: let id = curationSection.inputDataList[indexPath.row].id let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) case 7: let controller = HomeListController() - controller.reactor = HomeListReactor(popUpType: .popular) + controller.reactor = HomeListReactor( + popUpType: .popular, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: homeAPIUseCase + ) currentController.navigationController?.pushViewController(controller, animated: true) case 9: let id = popularSection.inputDataList[indexPath.row].id let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) case 12: let controller = HomeListController() - controller.reactor = HomeListReactor(popUpType: .new) + controller.reactor = HomeListReactor( + popUpType: .new, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: homeAPIUseCase + ) currentController.navigationController?.pushViewController(controller, animated: true) case 14: let id = newSection.inputDataList[indexPath.row].id let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) default: break @@ -308,33 +355,59 @@ final class HomeReactor: Reactor { case 0: if let id = loginImageBannerSection.inputDataList.first?.idList[indexPath.row - 1] { let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) } case 2: let controller = HomeListController() - controller.reactor = HomeListReactor(popUpType: .popular) + controller.reactor = HomeListReactor( + popUpType: .popular, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: homeAPIUseCase + ) currentController.navigationController?.pushViewController(controller, animated: true) case 4: let id = popularSection.inputDataList[indexPath.row].id let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) case 7: let controller = HomeListController() - controller.reactor = HomeListReactor(popUpType: .new) + controller.reactor = HomeListReactor( + popUpType: .new, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: homeAPIUseCase + ) currentController.navigationController?.pushViewController(controller, animated: true) case 9: let id = newSection.inputDataList[indexPath.row].id let controller = DetailController() - controller.reactor = DetailReactor(popUpID: id) + controller.reactor = DetailReactor( + popUpID: id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) currentController.navigationController?.pushViewController(controller, animated: true) default: break } } } - + func getPopUpData(indexPath: IndexPath) -> HomeCardSectionCell.Input { if isLoign { switch indexPath.section { diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift index 7d2e3c8e..1b27d1b2 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift @@ -1,26 +1,21 @@ -// -// HomeCardSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + import RxSwift struct HomeCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(158), diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift index 5bc534e3..664f82c1 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift @@ -1,41 +1,35 @@ -// -// HomeCardSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift -import Kingfisher +import SnapKit final class HomeCardSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let imageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + private let categoryLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 11) label.textColor = .blu500 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 14) label.numberOfLines = 2 label.lineBreakMode = .byTruncatingTail return label }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.numberOfLines = 1 @@ -43,19 +37,18 @@ final class HomeCardSectionCell: UICollectionViewCell { label.textColor = .g400 return label }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.lineBreakMode = .byTruncatingTail label.textColor = .g400 return label }() - + let bookmarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let rankLabel: UILabel = { let label = UILabel() label.backgroundColor = .w10 @@ -65,19 +58,18 @@ final class HomeCardSectionCell: UICollectionViewCell { label.textColor = .w100 return label }() - - private let imageService = PreSignedService() + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -89,7 +81,7 @@ private extension HomeCardSectionCell { func setUpConstraints() { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true - + contentView.addSubview(imageView) imageView.snp.makeConstraints { make in make.width.equalTo(contentView.bounds.width) @@ -99,42 +91,40 @@ private extension HomeCardSectionCell { } imageView.layer.cornerRadius = 4 imageView.clipsToBounds = true - + contentView.addSubview(categoryLabel) categoryLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.top.equalTo(imageView.snp.bottom).offset(12) make.height.equalTo(15) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(categoryLabel.snp.bottom).offset(4) make.leading.trailing.equalToSuperview() } - - contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.height.equalTo(15).priority(.high) make.bottom.equalToSuperview() } - + contentView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalTo(dateLabel.snp.top) make.height.equalTo(17).priority(.high) } - + contentView.addSubview(bookmarkButton) bookmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.top.trailing.equalToSuperview().inset(8) } - + imageView.addSubview(rankLabel) rankLabel.snp.makeConstraints { make in make.height.equalTo(24) @@ -158,21 +148,21 @@ extension HomeCardSectionCell: Inputable { var isPopular: Bool = false var row: Int? } - + func injection(with input: Input) { - categoryLabel.setLineHeightText(text: "#" + (input.category ?? ""), font: .KorFont(style: .bold, size: 11)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 14)) - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .medium, size: 11)) + categoryLabel.setLineHeightText(text: "#" + (input.category ?? ""), font: .korFont(style: .bold, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 14)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .medium, size: 11)) let date = input.startDate.toDate().toPPDateString() + " ~ " + input.endDate.toDate().toPPDateString() - dateLabel.setLineHeightText(text: date, font: .KorFont(style: .medium, size: 11)) + dateLabel.setLineHeightText(text: date, font: .korFont(style: .medium, size: 11)) let bookmarkImage = input.isBookmark ? UIImage(named: "icon_bookmark_fill") : UIImage(named: "icon_bookmark") bookmarkButton.setImage(bookmarkImage, for: .normal) imageView.setPPImage(path: input.imagePath) bookmarkButton.isHidden = !input.isLogin - + rankLabel.isHidden = !input.isPopular let rank = input.row ?? 0 - rankLabel.setLineHeightText(text: "\(rank + 1)위", font: .KorFont(style: .medium, size: 11), lineHeight: 1) + rankLabel.setLineHeightText(text: "\(rank + 1)위", font: .korFont(style: .medium, size: 11), lineHeight: 1) rankLabel.textAlignment = .center if rank > 2 { rankLabel.isHidden = true diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeHeaderView.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeHeaderView.swift index 1a3a4fee..28dac37a 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeHeaderView.swift @@ -1,46 +1,41 @@ -// -// HomeHeaderView.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + import SnapKit final class HomeHeaderView: UIView { - + // MARK: - Components - + let searchBarButton: UIButton = { let button = UIButton() button.layer.cornerRadius = 4 return button }() - + let blurEffect = UIBlurEffect(style: .regular) lazy var blurView = UIVisualEffectView(effect: blurEffect) - + private let searchIconImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_search_black") return view }() - + private let searchLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14, text: "팝업스토어명을 입력해보세요") label.textColor = .g1000 label.isUserInteractionEnabled = false return label }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -48,7 +43,7 @@ final class HomeHeaderView: UIView { // MARK: - SetUp private extension HomeHeaderView { - + func setUpConstraints() { blurView.isUserInteractionEnabled = false blurView.layer.cornerRadius = 4 @@ -57,21 +52,21 @@ private extension HomeHeaderView { blurView.snp.makeConstraints { make in make.edges.equalTo(searchBarButton) } - + self.addSubview(searchBarButton) searchBarButton.snp.makeConstraints { make in make.height.equalTo(37) make.leading.trailing.equalToSuperview().inset(20) make.top.bottom.equalToSuperview() } - + searchBarButton.addSubview(searchIconImageView) searchIconImageView.snp.makeConstraints { make in make.size.equalTo(20) make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(12) } - + searchBarButton.addSubview(searchLabel) searchLabel.snp.makeConstraints { make in make.centerY.equalTo(searchIconImageView) diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift index e790e333..c64754ff 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift @@ -1,26 +1,21 @@ -// -// HomePopularCardSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + import RxSwift struct HomePopularCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomePopularCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(232), @@ -33,13 +28,13 @@ struct HomePopularCardSection: Sectionable { heightDimension: .absolute(332) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - + // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 16 section.orthogonalScrollingBehavior = .continuous - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift index aa395113..a7f8816f 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift @@ -1,25 +1,19 @@ -// -// HomePopularCardSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift -import Kingfisher +import SnapKit final class HomePopularCardSectionCell: UICollectionViewCell { - + // MARK: - Components private var backGroundImageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + private let blurView: UIView = { var view = UIView() view.frame = CGRect(x: 0, y: 0, width: 232, height: 332) @@ -36,43 +30,42 @@ final class HomePopularCardSectionCell: UICollectionViewCell { view.layer.addSublayer(gradientLayer) return view }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .w100 return label }() - + private let categoryLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .g1000 label.backgroundColor = .w100 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.numberOfLines = 2 label.textColor = .w100 return label }() - + private let locationLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .w100 return label }() - + let disposeBag = DisposeBag() - - private let imageService = PreSignedService() + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -83,36 +76,36 @@ private extension HomePopularCardSectionCell { func setUpConstraints() { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true - + contentView.addSubview(backGroundImageView) backGroundImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + backGroundImageView.addSubview(blurView) blurView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + contentView.addSubview(categoryLabel) categoryLabel.snp.makeConstraints { make in make.bottom.equalTo(titleLabel.snp.top).offset(-16) make.leading.equalToSuperview().inset(20) make.height.equalTo(24) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.bottom.equalTo(categoryLabel.snp.top).offset(-6) make.leading.trailing.equalToSuperview().inset(20) } - + contentView.addSubview(locationLabel) locationLabel.snp.makeConstraints { make in make.centerY.equalTo(categoryLabel) @@ -131,10 +124,10 @@ extension HomePopularCardSectionCell: Inputable { var id: Int64 var address: String? } - + func injection(with input: Input) { let date = "#\(input.endDate.toDate().toPPDateMonthString())까지 열리는" - dateLabel.setLineHeightText(text: date, font: .KorFont(style: .regular, size: 16)) + dateLabel.setLineHeightText(text: date, font: .korFont(style: .regular, size: 16)) let category = "#\(input.category ?? "")" if let addressArray = input.address?.components(separatedBy: " ") { if addressArray.count > 2 { @@ -142,9 +135,9 @@ extension HomePopularCardSectionCell: Inputable { locationLabel.text = "#\(address)" } } - + categoryLabel.text = category - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 16)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 16)) backGroundImageView.setPPImage(path: input.imagePath) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift index 32dc908a..9ec1db9c 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift @@ -1,26 +1,21 @@ -// -// HomeTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + import RxSwift struct HomeTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,9 +30,8 @@ struct HomeTitleSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) // section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 16) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift index e74a2fcd..eeebf6c6 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift @@ -1,53 +1,46 @@ -// -// HomeTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class HomeTitleSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private var blueLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.textColor = .blu500 return label }() - + private let subLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + private let bottomLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + let detailButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_right_gray"), for: .normal) return button }() - + // MARK: - init override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -72,7 +65,7 @@ private extension HomeTitleSectionCell { make.leading.equalToSuperview().inset(20) make.bottom.equalToSuperview() } - + contentView.addSubview(detailButton) detailButton.snp.makeConstraints { make in make.size.equalTo(24) @@ -90,11 +83,11 @@ extension HomeTitleSectionCell: Inputable { var backgroundColor: UIColor? = .clear var textColor: UIColor? = .g1000 } - + func injection(with input: Input) { - blueLabel.setLineHeightText(text: input.blueText, font: .KorFont(style: .bold, size: 16)) - subLabel.setLineHeightText(text: input.topSubText, font: .KorFont(style: .bold, size: 16)) - bottomLabel.setLineHeightText(text: input.bottomText, font: .KorFont(style: .bold, size: 16)) + blueLabel.setLineHeightText(text: input.blueText, font: .korFont(style: .bold, size: 16)) + subLabel.setLineHeightText(text: input.topSubText, font: .korFont(style: .bold, size: 16)) + bottomLabel.setLineHeightText(text: input.bottomText, font: .korFont(style: .bold, size: 16)) contentView.backgroundColor = input.backgroundColor subLabel.textColor = input.textColor bottomLabel.textColor = input.textColor diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeView.swift index cab79f80..3a47888e 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/HomeView.swift @@ -1,29 +1,22 @@ -// -// HomeView.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit import SnapKit final class HomeView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.contentInsetAdjustmentBehavior = .never return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -31,7 +24,7 @@ final class HomeView: UIView { // MARK: - SetUp private extension HomeView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift index 14ddb820..43cecf09 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift @@ -1,26 +1,21 @@ -// -// ImageBannerChildSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/29/24. -// - import UIKit +import DesignSystem + import RxSwift struct ImageBannerChildSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ImageBannerChildSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift index 43dcc0e9..15784d70 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift @@ -1,34 +1,29 @@ -// -// ImageBannerChildSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/29/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class ImageBannerChildSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let imageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -49,7 +44,7 @@ extension ImageBannerChildSectionCell: Inputable { var imagePath: String? var id: Int64 } - + func injection(with input: Input) { imageView.setPPImage(path: input.imagePath) } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift similarity index 81% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift index 8a03671c..2aaab02c 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift @@ -1,26 +1,21 @@ -// -// ImageBannerSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit +import DesignSystem + import RxSwift struct ImageBannerSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ImageBannerSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +30,7 @@ struct ImageBannerSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift index 025ec4a9..5b904b9f 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift @@ -1,32 +1,26 @@ -// -// ImageBannerSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class ImageBannerSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private var autoScrollTimer: Timer? - + private lazy var contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) view.contentInsetAdjustmentBehavior = .never return view }() - + var pageControl: CustomPageControl = { - let controller = CustomPageControl() - return controller + return CustomPageControl() }() let stopButton: UIButton = { @@ -34,18 +28,18 @@ final class ImageBannerSectionCell: UICollectionViewCell { button.setImage(UIImage(named: "icon_banner_stopButton"), for: .normal) return button }() - + let playButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_banner_playButton"), for: .normal) return button }() - + private var isAutoBannerPlay: Bool = false private var isFirstResponseAutoScroll: Bool = true - + var imageSection = ImageBannerChildSection(inputDataList: []) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -59,30 +53,30 @@ final class ImageBannerSectionCell: UICollectionViewCell { return getSection()[section].getSection(section: section, env: env) } }() - + let bannerTapped: PublishSubject = .init() - + private var currentIndex: Int = 1 private var isHiddenPauseButton: Bool = true - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUp() setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() isFirstResponseAutoScroll = false } - + // 자동 스크롤 중지 함수 func stopAutoScroll() { stopButton.isHidden = true @@ -91,7 +85,9 @@ final class ImageBannerSectionCell: UICollectionViewCell { autoScrollTimer?.invalidate() autoScrollTimer = nil } - + + // FIXME: (홈 -> 상세) 이동 시 홈의 자동 스크롤이 계속 돌아감. + // FIXME: 또한 오토 스크롤을 한번만 실행하면 되는데, 사진이 넘어갈 때 마다 실행되는것으로 보임 func startAutoScroll(interval: TimeInterval = 3.0) { stopAutoScroll() // 기존 타이머를 중지 stopButton.isHidden = false @@ -112,13 +108,13 @@ private extension ImageBannerSectionCell { func setUp() { contentCollectionView.delegate = self contentCollectionView.dataSource = self - + contentCollectionView.register( ImageBannerChildSectionCell.self, forCellWithReuseIdentifier: ImageBannerChildSectionCell.identifiers ) } - + func setUpConstraints() { contentView.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in @@ -135,7 +131,7 @@ private extension ImageBannerSectionCell { make.centerY.equalTo(pageControl) make.leading.equalTo(pageControl.snp.trailing).offset(6) } - + contentView.addSubview(playButton) playButton.snp.makeConstraints { make in make.size.equalTo(6) @@ -143,11 +139,11 @@ private extension ImageBannerSectionCell { make.leading.equalTo(pageControl.snp.trailing).offset(6) } } - + func getSection() -> [any Sectionable] { return [imageSection] } - + private func findViewController() -> BaseViewController? { var nextResponder = self.next while nextResponder != nil { @@ -158,7 +154,7 @@ private extension ImageBannerSectionCell { } return nil } - + func bind() { stopButton.rx.tap .withUnretained(self) @@ -170,7 +166,7 @@ private extension ImageBannerSectionCell { } } .disposed(by: disposeBag) - + playButton.rx.tap .withUnretained(self) .subscribe { (owner, _) in @@ -181,7 +177,7 @@ private extension ImageBannerSectionCell { } } .disposed(by: disposeBag) - + imageSection.currentPage .distinctUntilChanged() .withUnretained(self) @@ -202,39 +198,43 @@ extension ImageBannerSectionCell: Inputable { var idList: [Int64] var isHiddenPauseButton: Bool = false } - + func injection(with input: Input) { if imageSection.isEmpty { pageControl.setNumberOfPages(input.imagePaths.count) let datas = zip(input.imagePaths, input.idList) - let backContents = datas.suffix(1) - let frontContents = datas.prefix(1) - imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } - imageSection.inputDataList.append(contentsOf: frontContents.map { .init(imagePath: $0.0, id: $0.1) }) - imageSection.inputDataList = backContents.map {.init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList - DispatchQueue.main.async { [weak self] in - self?.contentCollectionView.scrollToItem( - at: .init(row: 1, section: 0), - at: .centeredHorizontally, animated: false - ) + if input.imagePaths.count > 1 { + let backContents = datas.suffix(1) + let frontContents = datas.prefix(1) + imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList.append(contentsOf: frontContents.map { .init(imagePath: $0.0, id: $0.1) }) + imageSection.inputDataList = backContents.map { .init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList + DispatchQueue.main.async { [weak self] in + self?.contentCollectionView.scrollToItem( + at: .init(row: 1, section: 0), + at: .centeredHorizontally, animated: false + ) + } + } else { + imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } } } - + contentCollectionView.reloadData() isHiddenPauseButton = input.isHiddenPauseButton if isFirstResponseAutoScroll { startAutoScroll() isFirstResponseAutoScroll = false } - + if input.isHiddenPauseButton { stopAutoScroll() stopButton.isHidden = true playButton.isHidden = true } - + bind() - + if input.imagePaths.count == 1 { playButton.isHidden = true stopButton.isHidden = true @@ -245,27 +245,28 @@ extension ImageBannerSectionCell: Inputable { // MARK: - UICollectionViewDelegate, UICollectionViewDataSource extension ImageBannerSectionCell: UICollectionViewDelegate, UICollectionViewDataSource { - + func numberOfSections(in collectionView: UICollectionView) -> Int { return getSection().count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return getSection()[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { bannerTapped.onNext(indexPath.row) } - + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard imageSection.dataCount > 1 else { return } + if currentIndex == 0 { contentCollectionView.scrollToItem( at: .init(row: imageSection.dataCount - 2, section: 0), @@ -281,7 +282,7 @@ extension ImageBannerSectionCell: UICollectionViewDelegate, UICollectionViewData if !isHiddenPauseButton { startAutoScroll() } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift similarity index 83% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift index 6a2c2396..3f47504a 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift @@ -1,19 +1,14 @@ -// -// SectionBackGroundDecorationView.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + class SectionBackGroundDecorationView: UICollectionReusableView { // Decoration view의 UI 요소를 추가합니다. override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = .g700 } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -23,7 +18,7 @@ extension SectionBackGroundDecorationView: Inputable { struct Input { var backgroundColor: UIColor } - + func injection(with input: Input) { self.backgroundColor = input.backgroundColor } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift similarity index 81% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift index 02c2172b..97591ef2 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift @@ -1,26 +1,21 @@ -// -// SpacingSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit +import DesignSystem + import RxSwift struct SpacingSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = SpacingSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +30,7 @@ struct SpacingSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift index db08e964..a03d7330 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift @@ -1,30 +1,25 @@ -// -// SpacingSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class SpacingSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let spaceView: UIView = UIView() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -42,7 +37,7 @@ extension SpacingSectionCell: Inputable { var spacing: Float var backgroundColor: UIColor? = .clear } - + func injection(with input: Input) { spaceView.snp.removeConstraints() spaceView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailController.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailController.swift index c2b104a5..c0e4035f 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailController.swift @@ -1,26 +1,21 @@ -// -// ImageDetailController.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class ImageDetailController: BaseViewController, View { - + typealias Reactor = ImageDetailReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = ImageDetailView() - + private let cancelButton: UIButton = { let view = UIButton() view.setImage(UIImage(named: "icon_xmark_white"), for: .normal) @@ -56,7 +51,7 @@ private extension ImageDetailController { // MARK: - Methods extension ImageDetailController { func bind(reactor: Reactor) { - + cancelButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -64,7 +59,7 @@ extension ImageDetailController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailReactor.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailReactor.swift index 826253be..7af5a8c5 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailReactor.swift @@ -1,39 +1,34 @@ -// -// ImageDetailReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/25/24. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class ImageDetailReactor: Reactor { - + // MARK: - Reactor enum Action { case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) } - + struct State { var imagePath: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(imagePath: String?) { self.initialState = State(imagePath: imagePath) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -41,7 +36,7 @@ final class ImageDetailReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToRecentScene(let controller): diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailView.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailView.swift index 512c88d1..6092969d 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/ImageDetail/ImageDetailView.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class ImageDetailView: UIView { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -18,13 +18,13 @@ final class ImageDetailView: UIView { view.contentMode = .scaleAspectFit return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -32,7 +32,7 @@ final class ImageDetailView: UIView { // MARK: - SetUp private extension ImageDetailView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/LastLoginView.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/LastLoginView.swift index 28c0ff4b..eeebbd5a 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/LastLoginView.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class LastLoginView: UIView { - + /// 방향에 따라 툴팁을 다르게 표시합니다 enum TipDirection { case pointUp case pointDown } - + /// 툴팁의 색상을 지정하였습니다 /// 텍스트 컬러 또한 TipColor에 따라 수정됩니다 enum TipColor { case blu500 case w100 - + var color: UIColor { switch self { case .blu500: return UIColor.blu500 case .w100: return UIColor.w100 } } - + var textColor: UIColor { switch self { case .blu500: return UIColor.w100 @@ -37,34 +37,33 @@ final class LastLoginView: UIView { } } } - + // MARK: - Properties - + private let bgView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let notificationLabel: UILabel = { let label = UILabel() - label.font = .KorFont(style: .medium, size: 13) + label.font = .korFont(style: .medium, size: 13) return label }() - + private var colorType: TipColor { didSet { self.setNeedsDisplay() } } - + private var midX: CGFloat { return self.bounds.midX } - + private var direction: TipDirection - + // MARK: - init - + /// 툴팁 뷰를 생성합니다 /// - Parameters: /// - colorType: 툴팁의 색상(UIColor)을 인자로 받습니다 - w100, blu500 @@ -77,29 +76,29 @@ final class LastLoginView: UIView { notificationLabel.textColor = colorType.textColor notificationLabel.text = text } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func draw(_ rect: CGRect) { drawToolTip() } } extension LastLoginView { - + // MARK: - Methods - + private func setupLayer(color: TipColor) { self.backgroundColor = .clear addSubview(bgView) - + bgView.snp.makeConstraints { make in make.height.equalTo(45) make.edges.equalToSuperview() } - + bgView.addSubview(notificationLabel) switch direction { case .pointUp: @@ -107,7 +106,7 @@ extension LastLoginView { make.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview().inset(11) } - + case .pointDown: notificationLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) @@ -115,20 +114,20 @@ extension LastLoginView { } } } - + /// 툴팁을 방향에 맞춰 그리고 섀도우를 더하는 메서드 private func drawToolTip() { switch direction { case .pointUp: drawUpPointingToolTip() addShadow() - + case .pointDown: drawDownPointingTip() addShadow() } } - + /// 위를 가리키는 툴팁을 만듭니다 private func drawUpPointingToolTip() { let textLength = notificationLabel.frame.width @@ -137,10 +136,10 @@ extension LastLoginView { tip.addLine(to: CGPoint(x: midX, y: 0)) tip.addLine(to: CGPoint(x: midX + 8, y: 10)) tip.close() - + colorType.color.setFill() tip.fill() - + let message = UIBezierPath( roundedRect: CGRect( x: 0, y: 10, @@ -149,25 +148,25 @@ extension LastLoginView { ), cornerRadius: 6 ) - + colorType.color.setFill() message.fill() message.close() } - + /// 아래를 가리키는 툴팁을 만듭니다 private func drawDownPointingTip() { let textLength = notificationLabel.frame.width - + let tip = UIBezierPath() tip.move(to: CGPoint(x: midX - 8, y: 35)) tip.addLine(to: CGPoint(x: midX, y: 45)) tip.addLine(to: CGPoint(x: midX + 8, y: 35)) tip.close() - + colorType.color.setFill() tip.fill() - + let message = UIBezierPath( roundedRect: CGRect( x: 0, y: 0, @@ -176,19 +175,19 @@ extension LastLoginView { ), cornerRadius: 6 ) - + colorType.color.setFill() message.fill() message.close() } - + /// 툴팁의 섀도우를 더합니다 private func addShadow() { layer.shadowOffset = CGSize(width: 0, height: 5) layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.2 layer.shadowRadius = 5 - + // 섀도우를 그릴 때 드는 리소스를 줄이기 위해 캐시를 적용하는 방식 // layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath layer.shouldRasterize = true @@ -200,9 +199,9 @@ extension UIView { func showToolTip(color: LastLoginView.TipColor, direction: LastLoginView.TipDirection, text: String? = "최근에 이 방법으로 로그인했어요") { // 호출하는 컴포넌트 위 또는 아래에 생성되기 위해 superview를 구합니다 guard let superview = self.superview else { return } - + let toolTip = LastLoginView(colorType: color, direction: direction, text: text) - + superview.addSubview(toolTip) toolTip.snp.makeConstraints { make in if direction == .pointDown { diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginController.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginController.swift index f0fd9f61..b24a556f 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginController.swift @@ -1,24 +1,20 @@ -// -// LoginController.swift -// Poppool -// -// Created by SeoJunYoung on 11/24/24. -// - import UIKit -import SnapKit +import DesignSystem +import Infrastructure + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class LoginController: BaseViewController, View { - + typealias Reactor = LoginReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = LoginView() } @@ -28,7 +24,7 @@ extension LoginController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let lastLogin = reactor?.userDefaultService.fetch(key: "lastLogin") { @@ -57,12 +53,6 @@ private extension LoginController { // MARK: - Methods extension LoginController { func bind(reactor: Reactor) { - - rx.viewWillAppear - .map { Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - mainView.guestButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -70,7 +60,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.kakaoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,7 +68,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.inquiryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -86,7 +76,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.appleButton.rx.tap .withUnretained(self) .map { (owner, _) in diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginReactor.swift similarity index 79% rename from Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginReactor.swift index 74ef7572..56fcff23 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginReactor.swift @@ -1,54 +1,57 @@ -// -// LoginReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/24/24. -// +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class LoginReactor: Reactor { - + // MARK: - Reactor enum Action { case kakaoButtonTapped(controller: BaseViewController) case appleButtonTapped(controller: BaseViewController) case guestButtonTapped(controller: BaseViewController) - case viewWillAppear case inquiryButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToSignUpScene(controller: BaseViewController) case moveToHomeScene(controller: BaseViewController) case loadView - case resetService case moveToInquiryScene(controller: BaseViewController) } - + struct State { } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - - private let kakaoLoginService = KakaoLoginService() - private var appleLoginService = AppleLoginService() - private let authApiUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) - private let keyChainService = KeyChainService() + + private let authAPIUseCase: AuthAPIUseCase + private let kakaoLoginUseCase: KakaoLoginUseCase + private let appleLoginUseCase: AppleLoginUseCase + + @Dependency private var keyChainService: KeyChainService let userDefaultService = UserDefaultService() - + // MARK: - init - init() { + init( + authAPIUseCase: AuthAPIUseCase, + kakaoLoginUseCase: KakaoLoginUseCase, + appleLoginUseCase: AppleLoginUseCase + ) { + self.authAPIUseCase = authAPIUseCase + self.kakaoLoginUseCase = kakaoLoginUseCase + self.appleLoginUseCase = appleLoginUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,30 +60,29 @@ final class LoginReactor: Reactor { case .appleButtonTapped(let controller): return loginWithApple(controller: controller) case .guestButtonTapped(let controller): - let _ = keyChainService.deleteToken(type: .accessToken) - let _ = keyChainService.deleteToken(type: .refreshToken) + _ = keyChainService.deleteToken(type: .accessToken) + _ = keyChainService.deleteToken(type: .refreshToken) return Observable.just(.moveToHomeScene(controller: controller)) - case .viewWillAppear: - return Observable.just(.resetService) case .inquiryButtonTapped(let controller): return Observable.just(.moveToInquiryScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToSignUpScene(let controller): let signUpController = SignUpMainController() - signUpController.reactor = SignUpMainReactor(isFirstResponderCase: true, authrizationCode: authrizationCode) + signUpController.reactor = SignUpMainReactor( + isFirstResponderCase: true, + authrizationCode: authrizationCode, + signUpAPIUseCase: DIContainer.resolve(SignUpAPIUseCase.self) + ) controller.navigationController?.pushViewController(signUpController, animated: true) case .moveToHomeScene(let controller): let homeTabbar = WaveTabBarController() controller.view.window?.rootViewController = homeTabbar case .loadView: break - case .resetService: - authrizationCode = nil - appleLoginService = AppleLoginService() case .moveToInquiryScene(let controller): let nextController = FAQController() nextController.reactor = FAQReactor() @@ -88,12 +90,12 @@ final class LoginReactor: Reactor { } return state } - + func loginWithKakao(controller: BaseViewController) -> Observable { - return kakaoLoginService.fetchUserCredential() + return kakaoLoginUseCase.fetchUserCredential() .withUnretained(self) .flatMap { owner, response in - return owner.authApiUseCase.postTryLogin(userCredential: response, socialType: "kakao") + return owner.authAPIUseCase.postTryLogin(userCredential: response, socialType: "kakao") } .withUnretained(self) .map { [weak controller] (owner, loginResponse) in @@ -115,16 +117,16 @@ final class LoginReactor: Reactor { } } } - + func loginWithApple(controller: BaseViewController) -> Observable { - return appleLoginService.fetchUserCredential() + return appleLoginUseCase.fetchUserCredential() .withUnretained(self) .flatMap { owner, response in owner.authrizationCode = response.authorizationCode - return owner.authApiUseCase.postTryLogin(userCredential: response, socialType: "apple") + return owner.authAPIUseCase.postTryLogin(userCredential: response, socialType: "apple") } .withUnretained(self) - .map { [weak controller] (owner, loginResponse) in + .map({ [weak controller] (owner, loginResponse) in guard let controller = controller else { return .loadView } owner.userDefaultService.save(key: "userID", value: loginResponse.userId) owner.userDefaultService.save(key: "socialType", value: loginResponse.socialType) @@ -141,6 +143,6 @@ final class LoginReactor: Reactor { case .failure: return .loadView } - } + }) } } diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginView.swift index ed3830da..62d5b8ad 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Main/LoginView.swift @@ -1,75 +1,68 @@ -// -// LoginView.swift -// Poppool -// -// Created by SeoJunYoung on 11/24/24. -// - import UIKit +import DesignSystem + import SnapKit final class LoginView: UIView { - + // MARK: - Components let guestButton: UIButton = { let button = UIButton(type: .system) button.setTitle("둘러보기", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 14) + button.titleLabel?.font = .korFont(style: .regular, size: 14) button.setTitleColor(.g1000, for: .normal) return button }() - + private let logoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_login_logo") view.contentMode = .scaleAspectFit return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16, text: "간편하게 SNS 로그인하고\n팝풀 서비스를 이용해보세요") label.numberOfLines = 0 label.textAlignment = .center return label }() - + let kakaoButton: PPButton = { - let button = PPButton(style: .kakao, text: "카카오톡으로 로그인") - return button + return PPButton(style: .kakao, text: "카카오톡으로 로그인") }() - + private let kakaoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_kakao") return view }() - + let appleButton: PPButton = { - let button = PPButton(style: .apple, text: "Apple로 로그인") - return button + return PPButton(style: .apple, text: "Apple로 로그인") }() - + private let appleImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_apple") return view }() - + let inquiryButton: UIButton = { let button = UIButton(type: .system) button.setTitle("로그인이 어려우신가요?", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 12) + button.titleLabel?.font = .korFont(style: .regular, size: 12) button.setTitleColor(.g1000, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -77,14 +70,14 @@ final class LoginView: UIView { // MARK: - SetUp private extension LoginView { - + func setUpConstraints() { self.addSubview(guestButton) guestButton.snp.makeConstraints { make in make.top.equalToSuperview().inset(11) make.trailing.equalToSuperview().inset(20) } - + self.addSubview(logoImageView) logoImageView.snp.makeConstraints { make in make.height.equalTo(90) @@ -92,41 +85,41 @@ private extension LoginView { make.top.equalTo(guestButton.snp.bottom).offset(75) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(logoImageView.snp.bottom).offset(28) } - + self.addSubview(kakaoButton) kakaoButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(156) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + kakaoButton.addSubview(kakaoImageView) kakaoImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(appleButton) appleButton.snp.makeConstraints { make in make.top.equalTo(kakaoButton.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + appleButton.addSubview(appleImageView) appleImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(inquiryButton) inquiryButton.snp.makeConstraints { make in make.bottom.equalToSuperview().inset(56) diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginController.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginController.swift index b35da1bb..f1d42ee9 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginController.swift @@ -1,24 +1,19 @@ -// -// SubLoginController.swift -// Poppool -// -// Created by SeoJunYoung on 12/28/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SubLoginController: BaseViewController, View { - + typealias Reactor = SubLoginReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SubLoginView() } @@ -28,7 +23,7 @@ extension SubLoginController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let lastLogin = reactor?.userDefaultService.fetch(key: "lastLogin") { @@ -58,11 +53,6 @@ private extension SubLoginController { // MARK: - Methods extension SubLoginController { func bind(reactor: Reactor) { - rx.viewWillAppear - .map { Reactor.Action.viewWillAppear} - .bind(to: reactor.action) - .disposed(by: disposeBag) - mainView.xmarkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -70,7 +60,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.kakaoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,7 +68,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.inquiryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -86,7 +76,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.appleButton.rx.tap .withUnretained(self) .map { (owner, _) in diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginReactor.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginReactor.swift index c9254e87..ce0c960b 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginReactor.swift @@ -1,54 +1,56 @@ -// -// SubLoginReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/28/24. -// +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SubLoginReactor: Reactor { - + // MARK: - Reactor enum Action { case kakaoButtonTapped(controller: BaseViewController) case appleButtonTapped(controller: BaseViewController) case xmarkButtonTapped(controller: BaseViewController) - case viewWillAppear case inquiryButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToSignUpScene(controller: BaseViewController) case dismissScene(controller: BaseViewController) case loadView - case resetService case moveToInquiryScene(controller: BaseViewController) } - + struct State { } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - - private let kakaoLoginService = KakaoLoginService() - private var appleLoginService = AppleLoginService() - private let authApiUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) - private let keyChainService = KeyChainService() + + private let authAPIUseCase: AuthAPIUseCase + private let kakaoLoginUseCase: KakaoLoginUseCase + private let appleLoginUseCase: AppleLoginUseCase + @Dependency private var keyChainService: KeyChainService let userDefaultService = UserDefaultService() - + // MARK: - init - init() { + init( + authAPIUseCase: AuthAPIUseCase, + kakaoLoginUseCase: KakaoLoginUseCase, + appleLoginUseCase: AppleLoginUseCase + ) { + self.authAPIUseCase = authAPIUseCase + self.kakaoLoginUseCase = kakaoLoginUseCase + self.appleLoginUseCase = appleLoginUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -58,26 +60,25 @@ final class SubLoginReactor: Reactor { return loginWithApple(controller: controller) case .xmarkButtonTapped(let controller): return Observable.just(.dismissScene(controller: controller)) - case .viewWillAppear: - return Observable.just(.resetService) case .inquiryButtonTapped(let controller): return Observable.just(.moveToInquiryScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToSignUpScene(let controller): let signUpController = SignUpMainController() - signUpController.reactor = SignUpMainReactor(isFirstResponderCase: false, authrizationCode: authrizationCode) + signUpController.reactor = SignUpMainReactor( + isFirstResponderCase: false, + authrizationCode: authrizationCode, + signUpAPIUseCase: DIContainer.resolve(SignUpAPIUseCase.self) + ) controller.navigationController?.pushViewController(signUpController, animated: true) case .dismissScene(let controller): controller.dismiss(animated: true) case .loadView: break - case .resetService: - authrizationCode = nil - appleLoginService = AppleLoginService() case .moveToInquiryScene(let controller): let nextController = FAQController() nextController.reactor = FAQReactor() @@ -85,12 +86,12 @@ final class SubLoginReactor: Reactor { } return state } - + func loginWithKakao(controller: BaseViewController) -> Observable { - return kakaoLoginService.fetchUserCredential() + return kakaoLoginUseCase.fetchUserCredential() .withUnretained(self) .flatMap { owner, response in - owner.authApiUseCase.postTryLogin(userCredential: response, socialType: "kakao") + owner.authAPIUseCase.postTryLogin(userCredential: response, socialType: "kakao") } .withUnretained(self) .map { [weak controller] (owner, loginResponse) in @@ -112,13 +113,13 @@ final class SubLoginReactor: Reactor { } } } - + func loginWithApple(controller: BaseViewController) -> Observable { - return appleLoginService.fetchUserCredential() + return appleLoginUseCase.fetchUserCredential() .withUnretained(self) .flatMap { owner, response in owner.authrizationCode = response.authorizationCode - return owner.authApiUseCase.postTryLogin(userCredential: response, socialType: "apple") + return owner.authAPIUseCase.postTryLogin(userCredential: response, socialType: "apple") } .withUnretained(self) .map { [weak controller] (owner, loginResponse) in diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginView.swift index bcbe83c5..cea96869 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Login/Sub/SubLoginView.swift @@ -1,16 +1,11 @@ -// -// SubLoginView.swift -// Poppool -// -// Created by SeoJunYoung on 12/28/24. -// - import UIKit +import DesignSystem + import SnapKit final class SubLoginView: UIView { - + // MARK: - Components let xmarkButton: UIButton = { let button = UIButton(type: .system) @@ -18,58 +13,56 @@ final class SubLoginView: UIView { button.tintColor = .g1000 return button }() - + private let logoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_login_logo") view.contentMode = .scaleAspectFit return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16, text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?") - label.setLineHeightText(text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?", font: .KorFont(style: .bold, size: 16), lineHeight: 1.3) + label.setLineHeightText(text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?", font: .korFont(style: .bold, size: 16), lineHeight: 1.3) label.numberOfLines = 0 label.textAlignment = .center return label }() - + let kakaoButton: PPButton = { - let button = PPButton(style: .kakao, text: "카카오톡으로 로그인") - return button + return PPButton(style: .kakao, text: "카카오톡으로 로그인") }() - + private let kakaoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_kakao") return view }() - + let appleButton: PPButton = { - let button = PPButton(style: .apple, text: "Apple로 로그인") - return button + return PPButton(style: .apple, text: "Apple로 로그인") }() - + private let appleImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_apple") return view }() - + let inquiryButton: UIButton = { let button = UIButton(type: .system) button.setTitle("로그인이 어려우신가요?", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 12) + button.titleLabel?.font = .korFont(style: .regular, size: 12) button.setTitleColor(.g1000, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -77,7 +70,7 @@ final class SubLoginView: UIView { // MARK: - SetUp private extension SubLoginView { - + func setUpConstraints() { self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in @@ -85,7 +78,7 @@ private extension SubLoginView { make.trailing.equalToSuperview().inset(20) make.size.equalTo(32) } - + self.addSubview(logoImageView) logoImageView.snp.makeConstraints { make in make.height.equalTo(90) @@ -93,41 +86,41 @@ private extension SubLoginView { make.top.equalTo(xmarkButton.snp.bottom).offset(75) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(logoImageView.snp.bottom).offset(28) } - + self.addSubview(kakaoButton) kakaoButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(156) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + kakaoButton.addSubview(kakaoImageView) kakaoImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(appleButton) appleButton.snp.makeConstraints { make in make.top.equalTo(kakaoButton.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + appleButton.addSubview(appleImageView) appleImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(inquiryButton) inquiryButton.snp.makeConstraints { make in make.bottom.equalToSuperview().inset(56) diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonBackgroundView.swift similarity index 80% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonBackgroundView.swift index 3c673979..fff36989 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonBackgroundView.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class BalloonBackgroundView: UIView { @@ -13,9 +13,8 @@ final class BalloonBackgroundView: UIView { return view }() - // 기존 말풍선 UI: 서브 지역을 나열하는 CollectionView (서울/경기/부산용) private let collectionView: UICollectionView = { - let layout = UICollectionViewCompositionalLayout { section, env in + let layout = UICollectionViewCompositionalLayout { section, _ in let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(30), heightDimension: .absolute(30) @@ -28,15 +27,15 @@ final class BalloonBackgroundView: UIView { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(8) let section = NSCollectionLayoutSection(group: group) - section.contentInsets = .init(top: 20, leading: 20, bottom: 19, trailing: 20) + section.contentInsets = .init(top: 18, leading: 20, bottom: 19, trailing: 20) section.interGroupSpacing = 8 return section } - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.isScrollEnabled = false - cv.register(BalloonChipCell.self, forCellWithReuseIdentifier: BalloonChipCell.identifier) - return cv + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.isScrollEnabled = false + collectionView.register(BalloonChipCell.self, forCellWithReuseIdentifier: BalloonChipCell.identifiers) + return collectionView }() // "그 외 지역" 전용 UI: 아이콘과 안내문구 @@ -44,8 +43,8 @@ final class BalloonBackgroundView: UIView { let iv = UIImageView() iv.contentMode = .scaleAspectFit iv.tintColor = .blu500 - iv.image = UIImage(named: "Marker") // 에셋에 추가된 Marker 이미지 - iv.isHidden = true // 기본은 숨김 + iv.image = UIImage(named: "Marker") + iv.isHidden = true return iv }() @@ -62,7 +61,7 @@ final class BalloonBackgroundView: UIView { label.font = UIFont.systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor.g400 label.textAlignment = .center - label.numberOfLines = 2 // 두 줄 표시 + label.numberOfLines = 2 return label }() @@ -88,6 +87,9 @@ final class BalloonBackgroundView: UIView { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear + self.isUserInteractionEnabled = true + containerView.isUserInteractionEnabled = true + collectionView.isUserInteractionEnabled = true setupLayout() setupCollectionView() } @@ -97,11 +99,13 @@ final class BalloonBackgroundView: UIView { // MARK: - Setup private func setupLayout() { + addSubview(containerView) containerView.snp.makeConstraints { make in - make.left.right.bottom.equalToSuperview() - make.top.equalToSuperview().offset(arrowHeight) - } + make.left.right.equalToSuperview() + make.top.equalToSuperview().offset(arrowHeight) + make.bottom.equalToSuperview() + } containerView.addSubview(collectionView) containerView.addSubview(singleRegionIcon) @@ -109,7 +113,8 @@ final class BalloonBackgroundView: UIView { containerView.addSubview(singleRegionDetailLabel) collectionView.snp.makeConstraints { make in - make.edges.equalToSuperview() + make.top.left.right.equalToSuperview() + make.bottom.equalToSuperview() } singleRegionIcon.snp.makeConstraints { make in @@ -140,44 +145,33 @@ final class BalloonBackgroundView: UIView { override func draw(_ rect: CGRect) { super.draw(rect) - let arrowWidth: CGFloat = 12 // 화살표 너비 조정 - let arrowHeight: CGFloat = 8 // 화살표 높이 조정 + let arrowWidth: CGFloat = 12 + let arrowHeight: CGFloat = 8 - // 화살표의 시작 x좌표 계산 let arrowX = bounds.width * arrowPosition - (arrowWidth / 2) - // 경로 그리기 let path = UIBezierPath() - // 1. 화살표 그리기 - path.move(to: CGPoint(x: arrowX, y: arrowHeight)) // 왼쪽 아래 - path.addLine(to: CGPoint(x: arrowX + (arrowWidth / 2), y: 0)) // 상단 중앙 - path.addLine(to: CGPoint(x: arrowX + arrowWidth, y: arrowHeight)) // 오른쪽 아래 + path.move(to: CGPoint(x: arrowX, y: arrowHeight)) + path.addLine(to: CGPoint(x: arrowX + (arrowWidth / 2), y: 0)) + path.addLine(to: CGPoint(x: arrowX + arrowWidth, y: arrowHeight)) - // 2. 말풍선 본체 그리기 let balloonRect = CGRect(x: 0, y: arrowHeight, width: bounds.width, height: bounds.height - arrowHeight) - path.addLine(to: CGPoint(x: balloonRect.maxX, y: balloonRect.minY)) // 오른쪽 상단 - path.addLine(to: CGPoint(x: balloonRect.maxX, y: balloonRect.maxY)) // 오른쪽 하단 - path.addLine(to: CGPoint(x: balloonRect.minX, y: balloonRect.maxY)) // 왼쪽 하단 - path.addLine(to: CGPoint(x: balloonRect.minX, y: balloonRect.minY)) // 왼쪽 상단 + path.addLine(to: CGPoint(x: balloonRect.maxX, y: balloonRect.minY)) + path.addLine(to: CGPoint(x: balloonRect.maxX, y: balloonRect.maxY)) + path.addLine(to: CGPoint(x: balloonRect.minX, y: balloonRect.maxY)) + path.addLine(to: CGPoint(x: balloonRect.minX, y: balloonRect.minY)) path.close() UIColor.g50.setFill() path.fill() - // 그림자 설정 -// layer.shadowPath = path.cgPath -// layer.shadowColor = UIColor.black.cgColor -// layer.shadowOpacity = 0.1 -// layer.shadowOffset = CGSize(width: 0, height: 2) -// layer.shadowRadius = 4 } - // MARK: - Public /// configure 메서드 @@ -214,7 +208,6 @@ final class BalloonBackgroundView: UIView { } } - private func setupTagSection() { let allKey = "\(mainRegionTitle)전체" @@ -236,7 +229,6 @@ final class BalloonBackgroundView: UIView { ) } - func calculateHeight() -> CGFloat { if collectionView.isHidden { return 145 @@ -246,55 +238,44 @@ final class BalloonBackgroundView: UIView { collectionView.layoutIfNeeded() - print("실제 contentSize 높이: \(collectionView.collectionViewLayout.collectionViewContentSize.height)") - let balloonWidth = self.bounds.width let horizontalSpacing: CGFloat = 8 let leftPadding: CGFloat = 20 let rightPadding: CGFloat = 20 let availableWidth = balloonWidth - leftPadding - rightPadding - print("사용 가능한 너비: \(availableWidth)") - var currentRowWidth: CGFloat = 0 var numberOfRows: Int = 1 for input in inputDataList { let buttonWidth = calculateButtonWidth(for: input.title ?? "", font: .systemFont(ofSize: 12), isSelected: input.isSelected ?? false) - print("버튼 너비 [\(input.title ?? "")]: \(buttonWidth)") let widthWithSpacing = currentRowWidth == 0 ? buttonWidth : buttonWidth + horizontalSpacing if currentRowWidth + widthWithSpacing > availableWidth { numberOfRows += 1 currentRowWidth = buttonWidth - print("새로운 줄 시작: \(numberOfRows)번째 줄") } else { currentRowWidth += widthWithSpacing - print("현재 줄 너비: \(currentRowWidth)") } } let itemHeight: CGFloat = 36 let interGroupSpacing: CGFloat = 8 - let verticalInset: CGFloat = 20 + 20 // top: 20, bottom: 20 - let totalHeight = max( + let verticalInset: CGFloat = 20 + 20 + return max( (itemHeight * CGFloat(numberOfRows)) + (interGroupSpacing * CGFloat(numberOfRows - 1)) + verticalInset, 36 ) - - print("계산된 최종 높이: \(totalHeight)") - return totalHeight } private func calculateButtonWidth(for text: String, font: UIFont, isSelected: Bool) -> CGFloat { let textWidth = (text as NSString).size(withAttributes: [.font: font]).width let iconWidth: CGFloat = isSelected ? 16 : 0 let iconGap: CGFloat = isSelected ? 4 : 0 let horizontalPadding: CGFloat = 24 - let calculatedWidth = textWidth + iconWidth + iconGap + horizontalPadding - return calculatedWidth + return textWidth + iconWidth + iconGap + horizontalPadding } } @@ -308,7 +289,7 @@ extension BalloonBackgroundView: UICollectionViewDataSource { cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: BalloonChipCell.identifier, + withReuseIdentifier: BalloonChipCell.identifiers, for: indexPath ) as? BalloonChipCell, let input = tagSection?.inputDataList[indexPath.item] diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonChipCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonChipCell.swift new file mode 100644 index 00000000..3612b976 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/BalloonChipCell.swift @@ -0,0 +1,132 @@ +import UIKit + +import DesignSystem +import Infrastructure + +import SnapKit +import Then + +final class BalloonChipCell: UICollectionViewCell { + + private enum Constant { + static let verticalInset: CGFloat = 6 + static let selectedLeftInset: CGFloat = 10 + static let normalLeftInset: CGFloat = 12 + static let rightInset: CGFloat = 12 + static let checkIconSize: CGSize = .init(width: 16, height: 16) + static let baselineOffset: CGFloat = -1 + static let fontSize: CGFloat = 11 + } + + private let button = PPButton( + style: .secondary, + text: "", + font: .korFont(style: .medium, size: Constant.fontSize), + cornerRadius: 15 + ).then { + $0.titleLabel?.lineBreakMode = .byTruncatingTail + $0.titleLabel?.adjustsFontSizeToFitWidth = false + } + + private var currentAction: UIAction? + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(button) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + button.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configure(with title: String, isSelected: Bool) { + let attributedTitle = NSMutableAttributedString(string: title).then { + $0.addAttribute( + .baselineOffset, + value: Constant.baselineOffset, + range: NSRange(location: .zero, length: $0.length) + ) + } + + if isSelected { + let checkImage = UIImage(named: "icon_check_white")?.withRenderingMode(.alwaysOriginal).resize(to: Constant.checkIconSize) + + button.then { + $0.setImage(checkImage, for: .normal) + $0.semanticContentAttribute = .forceRightToLeft + $0.imageEdgeInsets = .init(top: .zero, left: 1, bottom: .zero, right: .zero) + $0.contentEdgeInsets = .init( + top: Constant.verticalInset, + left: Constant.selectedLeftInset, + bottom: Constant.verticalInset, + right: Constant.rightInset + ) + $0.setBackgroundColor(.blu500, for: .normal) + $0.setTitleColor(.white, for: .normal) + $0.layer.borderWidth = .zero + } + + attributedTitle.addAttribute( + .font, + value: UIFont.korFont(style: .bold, size: Constant.fontSize), + range: NSRange(location: .zero, length: attributedTitle.length) + ) + } else { + button.then { + $0.setImage(nil, for: .normal) + $0.semanticContentAttribute = .unspecified + $0.imageEdgeInsets = .zero + $0.contentEdgeInsets = .init( + top: Constant.verticalInset, + left: Constant.normalLeftInset, + bottom: Constant.verticalInset, + right: Constant.rightInset + ) + $0.setBackgroundColor(.white, for: .normal) + $0.setTitleColor(.g400, for: .normal) + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.g200.cgColor + } + + attributedTitle.addAttribute( + .font, + value: UIFont.korFont(style: .medium, size: Constant.fontSize), + range: NSRange(location: .zero, length: attributedTitle.length) + ) + } + + self.button.setAttributedTitle(attributedTitle, for: .normal) + } + + var buttonAction: (() -> Void)? { + didSet { + if let oldAction = currentAction { + self.button.removeAction(oldAction, for: .touchUpInside) + } + + let action = UIAction { [weak self] _ in + guard let self = self else { return } + self.buttonAction?() + } + + self.button.addAction(action, for: .touchUpInside) + self.currentAction = action + } + } +} + +extension UIImage { + func resize(to size: CGSize) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + defer { UIGraphicsEndImageContext() } + self.draw(in: CGRect(origin: .zero, size: size)) + return UIGraphicsGetImageFromCurrentImageContext() + } +} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetReactor.swift similarity index 90% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetReactor.swift index 190ad281..ed37ee77 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetReactor.swift @@ -1,5 +1,5 @@ -import ReactorKit import Foundation +import ReactorKit import RxSwift struct Location: Equatable { @@ -59,27 +59,27 @@ final class FilterBottomSheetReactor: Reactor { Location( main: "서울", sub: [ - "도봉/노원","강북/중랑","동대문/성북","중구/종로","성동/광진", - "송파/강동","동작/관악","서초/강남","은평/서대문/마포", - "영등포/구로","용산","양천/강서/금천" + "도봉/노원", "강북/중랑", "동대문/성북", "중구/종로", "성동/광진", + "송파/강동", "동작/관악", "서초/강남", "은평/서대문/마포", + "영등포/구로", "용산", "양천/강서/금천" ] ), Location( main: "경기", sub: [ - "포천/연천","동두천/양주/의정부","구리/남양주/가평", - "파주/고양/김포","용인/화성/수원","군포/의왕", - "과천/안양","부천/광명","시흥/안산", - "안성/평택/오산","성남/하남/광주","이천/여주/양평" + "포천/연천", "동두천/양주/의정부", "구리/남양주/가평", + "파주/고양/김포", "용인/화성/수원", "군포/의왕", + "과천/안양", "부천/광명", "시흥/안산", + "안성/평택/오산", "성남/하남/광주", "이천/여주/양평" ] ), Location(main: "인천", sub: ["부평", "송도"]), Location( main: "부산", sub: [ - "중구","서구","동구","영도구","부산진구", - "동래구","남구","북구","해운대구","사하구", - "금정구","강서구","연제구","수영구","사상구", + "중구", "서구", "동구", "영도구", "부산진구", + "동래구", "남구", "북구", "해운대구", "사하구", + "금정구", "강서구", "연제구", "수영구", "사상구", "기장군" ] ), @@ -177,14 +177,12 @@ final class FilterBottomSheetReactor: Reactor { switch mutation { case .setActiveSegment(let index): newState.activeSegment = index - - case .resetFilters: + case .resetFilters: newState.selectedSubRegions = [] newState.selectedCategories = [] newState.savedSubRegions = [] newState.savedCategories = [] // 여기서 forceSaveEnabled는 나중에 setForceSaveEnabled가 적용됨 - break case .applyFilters(let combined): print("필터 적용: \(newState.selectedSubRegions + newState.selectedCategories)") diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetView.swift similarity index 53% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetView.swift index 3301184c..bf932add 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetView.swift @@ -1,60 +1,60 @@ import UIKit + +import DesignSystem + import SnapKit +import Then final class FilterBottomSheetView: UIView { - // MARK: - UI Components - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 20 - view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - view.layer.masksToBounds = true - - return view - }() + private enum Constant { + static let cornerRadius: CGFloat = 20 + static let topInset: CGFloat = 30 + static let horizontalInset: CGFloat = 16 + static let segmentedTopOffset: CGFloat = 16 + static let scrollViewHeight: CGFloat = 36 + static let categoryHeight: CGFloat = 160 + static let balloonTopOffset: CGFloat = 16 + static let filterChipsHeight: CGFloat = 80 + static let buttonStackSpacing: CGFloat = 12 + static let buttonStackHeight: CGFloat = 52 + } - let headerView: UIView = { - let view = UIView() - view.backgroundColor = .white - return view - }() - let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") - label.textColor = .black - return label - }() + private let containerView = UIView().then { + $0.backgroundColor = .white + $0.layer.cornerRadius = Constant.cornerRadius + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.layer.masksToBounds = true + } - let closeButton: UIButton = { - let button = UIButton(type: .system) - button.setImage(UIImage(named: "icon_xmark"), for: .normal) - button.tintColor = .black - return button - }() + let titleLabel = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요").then { + $0.textColor = .black + } - let segmentedControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .tab, segments: ["지역", "카테고리"], selectedSegmentIndex: 0) - return control - }() + let closeButton = UIButton(type: .system).then { + $0.setImage(UIImage(named: "icon_xmark"), for: .normal) + $0.tintColor = .black + } + + let segmentedControl = PPSegmentedControl(type: .tab, segments: ["지역", "카테고리"], selectedSegmentIndex: 0) + + let locationScrollView = UIScrollView().then { + $0.showsHorizontalScrollIndicator = false + } - let locationScrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.showsHorizontalScrollIndicator = false - return scrollView - }() let locationContentView = UIView() var categoryHeightConstraint: Constraint? let categoryCollectionView: UICollectionView = { - let layout = UICollectionViewCompositionalLayout { section, env in + let layout = UICollectionViewCompositionalLayout { section, _ in let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(26), - heightDimension: .absolute(36) + heightDimension: .estimated(Constant.scrollViewHeight) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), - heightDimension: .absolute(36) + heightDimension: .estimated(Constant.scrollViewHeight) ) let group = NSCollectionLayoutGroup.horizontal( layoutSize: groupSize, @@ -73,46 +73,30 @@ final class FilterBottomSheetView: UIView { return section } - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.backgroundColor = .clear - collectionView.isScrollEnabled = false - collectionView.register(TagSectionCell.self, forCellWithReuseIdentifier: TagSectionCell.identifiers) - return collectionView + return UICollectionView(frame: .zero, collectionViewLayout: layout).then { + $0.backgroundColor = .clear + $0.isScrollEnabled = false + $0.register(TagSectionCell.self, forCellWithReuseIdentifier: TagSectionCell.identifiers) + } }() - let balloonBackgroundView = BalloonBackgroundView() - let resetButton: PPButton = { - let button = PPButton(style: .secondary, text: "초기화") - return button - }() - - - let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장") - return button - }() - + let resetButton = PPButton(style: .secondary, text: "초기화") + let saveButton = PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장") - private let buttonStack: UIStackView = { - let stack = UIStackView() - stack.axis = .horizontal - stack.spacing = 12 - stack.distribution = .fillEqually - return stack - }() + private let buttonStack = UIStackView().then { + $0.axis = .horizontal + $0.spacing = Constant.buttonStackSpacing + $0.distribution = .fillEqually + } - let filterChipsView: FilterChipsView = { - let view = FilterChipsView() - return view - }() + let filterChipsView = FilterChipsView() private var balloonHeightConstraint: Constraint? // MARK: - Initialization - + override init(frame: CGRect) { super.init(frame: frame) setupLayout() @@ -127,76 +111,52 @@ final class FilterBottomSheetView: UIView { backgroundColor = .clear addSubview(containerView) - containerView.addSubview(headerView) - headerView.addSubview(titleLabel) - headerView.addSubview(closeButton) - + containerView.addSubview(titleLabel) + containerView.addSubview(closeButton) containerView.addSubview(segmentedControl) containerView.addSubview(locationScrollView) locationScrollView.addSubview(locationContentView) - containerView.addSubview(balloonBackgroundView) containerView.addSubview(categoryCollectionView) - categoryCollectionView.snp.makeConstraints { make in - make.top.equalTo(segmentedControl.snp.bottom).offset(16) - make.leading.trailing.equalToSuperview() - categoryHeightConstraint = make.height.equalTo(160).constraint - } - containerView.addSubview(filterChipsView) + buttonStack.addArrangedSubview(resetButton) buttonStack.addArrangedSubview(saveButton) containerView.addSubview(buttonStack) - filterChipsView.snp.makeConstraints { make in - make.top.equalTo(balloonBackgroundView.snp.bottom).offset(24) - make.leading.trailing.equalToSuperview().inset(16) - make.height.equalTo(80) - } - setupConstraints() } private func setupConstraints() { - // 1. 먼저 self의 width 설정 self.snp.makeConstraints { make in make.width.equalTo(UIScreen.main.bounds.width) } - // 2. containerView 설정 containerView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - make.top.equalTo(headerView.snp.top) - } - - // 3. headerView 및 내부 요소들 - headerView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(70) + make.top.equalTo(self.snp.top) } titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(30) + make.leading.equalToSuperview().offset(Constant.horizontalInset) + make.top.equalToSuperview().offset(Constant.topInset) } closeButton.snp.makeConstraints { make in - make.trailing.equalToSuperview().inset(16) + make.trailing.equalToSuperview().inset(Constant.horizontalInset) make.centerY.equalTo(titleLabel) make.size.equalTo(24) } - // 4. segmentedControl segmentedControl.snp.makeConstraints { make in - make.top.equalTo(headerView.snp.bottom).offset(16) + make.top.equalTo(titleLabel.snp.bottom).offset(Constant.segmentedTopOffset) make.leading.trailing.equalToSuperview() } - // 5. locationScrollView 및 contentView locationScrollView.snp.makeConstraints { make in make.top.equalTo(segmentedControl.snp.bottom).offset(20) make.leading.trailing.equalToSuperview() - make.height.equalTo(36) + make.height.equalTo(Constant.scrollViewHeight) } locationContentView.snp.makeConstraints { make in @@ -204,39 +164,35 @@ final class FilterBottomSheetView: UIView { make.height.equalToSuperview() } - // 6. categoryCollectionView categoryCollectionView.snp.makeConstraints { make in - make.top.equalTo(segmentedControl.snp.bottom).offset(16) + make.top.equalTo(segmentedControl.snp.bottom).offset(Constant.segmentedTopOffset) make.leading.trailing.equalToSuperview() -// categoryHeightConstraint = make.height.equalTo(160).constraint + categoryHeightConstraint = make.height.equalTo(Constant.categoryHeight).constraint } - // 7. balloonBackgroundView balloonBackgroundView.snp.makeConstraints { make in - make.top.equalTo(locationScrollView.snp.bottom).offset(16) - make.leading.trailing.equalToSuperview().inset(16) + make.top.equalTo(locationScrollView.snp.bottom).offset(Constant.balloonTopOffset) + make.leading.trailing.equalToSuperview().inset(Constant.horizontalInset) balloonHeightConstraint = make.height.equalTo(0).constraint } - // 8. filterChipsView filterChipsView.snp.makeConstraints { make in make.top.equalTo(balloonBackgroundView.snp.bottom).offset(24) - make.leading.trailing.equalToSuperview().inset(16) - make.height.equalTo(80) + make.leading.trailing.equalToSuperview().inset(Constant.horizontalInset) + make.height.equalTo(Constant.filterChipsHeight) } - // 9. buttonStack buttonStack.snp.makeConstraints { make in make.top.equalTo(filterChipsView.snp.bottom).offset(24) - make.leading.trailing.equalToSuperview().inset(16) + make.leading.trailing.equalToSuperview().inset(Constant.horizontalInset) make.bottom.equalToSuperview().inset(40) - make.height.equalTo(52) + make.height.equalTo(Constant.buttonStackHeight) } } func setupLocationScrollView(locations: [Location], buttonAction: @escaping (Int, UIButton) -> Void) { locationContentView.subviews.forEach { $0.removeFromSuperview() } - locationScrollView.delegate = self + locationScrollView.delegate = self as? UIScrollViewDelegate var lastButton: UIButton? @@ -244,10 +200,14 @@ final class FilterBottomSheetView: UIView { let button = createStyledButton(title: location.main) button.tag = index - button.addAction(UIAction { _ in + button.addTarget(self, action: #selector(locationButtonTapped(_:)), for: .touchUpInside) + + // actionHandler 클로저 저장 + button.layer.setValue(index, forKey: "buttonIndex") + objc_setAssociatedObject(button, &AssociatedKeys.actionHandler, { [weak self] in buttonAction(index, button) - self.updateMainLocationSelection(index) - }, for: .touchUpInside) + self?.updateMainLocationSelection(index) + }, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) locationContentView.addSubview(button) @@ -269,6 +229,17 @@ final class FilterBottomSheetView: UIView { } } } + + private struct AssociatedKeys { + static var actionHandler = "actionHandler" + } + + @objc private func locationButtonTapped(_ sender: UIButton) { + if let actionHandler = objc_getAssociatedObject(sender, &AssociatedKeys.actionHandler) as? () -> Void { + actionHandler() + } + } + func updateCategoryButtonSelection(_ category: String) { categoryCollectionView.subviews.forEach { subview in if let stackView = subview as? UIStackView { @@ -291,6 +262,19 @@ final class FilterBottomSheetView: UIView { } } + func updateContentVisibility(isCategorySelected: Bool) { + self.locationScrollView.isHidden = isCategorySelected + self.balloonBackgroundView.isHidden = isCategorySelected + self.categoryCollectionView.isHidden = !isCategorySelected + + self.locationScrollView.alpha = isCategorySelected ? 0 : 1 + self.balloonBackgroundView.alpha = isCategorySelected ? 0 : 1 + self.categoryCollectionView.alpha = isCategorySelected ? 1 : 0 + + let newHeight = isCategorySelected ? 170 : self.balloonBackgroundView.calculateHeight() + self.balloonHeightConstraint?.update(offset: newHeight) + } + private func createCategoryButton(title: String, isSelected: Bool) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) @@ -313,94 +297,73 @@ final class FilterBottomSheetView: UIView { return button } - func updateContentVisibility(isCategorySelected: Bool) { - UIView.animate(withDuration: 0.3) { - self.locationScrollView.alpha = isCategorySelected ? 0 : 1 - self.balloonBackgroundView.alpha = isCategorySelected ? 0 : 1 - self.categoryCollectionView.alpha = isCategorySelected ? 1 : 0 - - self.locationScrollView.isHidden = isCategorySelected - self.balloonBackgroundView.isHidden = isCategorySelected - self.categoryCollectionView.isHidden = !isCategorySelected - - let newHeight = isCategorySelected ? 170 : self.balloonBackgroundView.calculateHeight() - self.balloonHeightConstraint?.update(offset: newHeight) - - self.layoutIfNeeded() - } - } - - - - private func createStyledButton(title: String, isSelected: Bool = false) -> PPButton { let button = PPButton( style: .secondary, text: title, - font: .KorFont(style: .medium, size: 13), + font: .korFont(style: isSelected ? .bold : .medium, size: 13), cornerRadius: 18 ) - button.setBackgroundColor(.w100, for: .normal) - button.setTitleColor(.g400, for: .normal) + button.setBackgroundColor(isSelected ? .blu500 : .w100, for: .normal) + button.setTitleColor(isSelected ? .w100 : .g400, for: .normal) + button.layer.borderWidth = isSelected ? 0 : 1 button.layer.borderColor = UIColor.g200.cgColor - button.layer.borderWidth = 1 - - if isSelected { - button.setBackgroundColor(.blu500, for: .normal) - button.setTitleColor(.w100, for: .normal) - button.layer.borderWidth = 0 - } - button.contentEdgeInsets = UIEdgeInsets(top: 9, left: 16, bottom: 9, right: 16) + button.titleLabel?.setLineHeightText( + text: title, + font: .korFont(style: isSelected ? .bold : .medium, size: 13), + lineHeight: 1.2 + ) + return button } func updateMainLocationSelection(_ index: Int) { locationContentView.subviews.enumerated().forEach { (idx, view) in guard let button = view as? PPButton else { return } - if idx == index { - button.setBackgroundColor(.blu500, for: .normal) - button.setTitleColor(.w100, for: .normal) - button.layer.borderWidth = 0 - button.titleLabel?.font = .KorFont(style: .bold, size: 13) - } else { - button.setBackgroundColor(.w100, for: .normal) - button.setTitleColor(.g400, for: .normal) - button.layer.borderColor = UIColor.g200.cgColor - button.titleLabel?.font = .KorFont(style: .medium, size: 13) - button.layer.borderWidth = 1 - } + + let isSelected = idx == index + button.setBackgroundColor(isSelected ? .blu500 : .w100, for: .normal) + button.setTitleColor(isSelected ? .w100 : .g400, for: .normal) + button.layer.borderWidth = isSelected ? 0 : 1 + button.layer.borderColor = UIColor.g200.cgColor + + // 버튼의 타이틀 레이블에 setLineHeightText 적용 + let title = button.currentTitle ?? "" + button.titleLabel?.setLineHeightText( + text: title, + font: .korFont(style: isSelected ? .bold : .medium, size: 13), + lineHeight: 1.2 + ) } + } func updateBalloonHeight(isHidden: Bool, dynamicHeight: CGFloat = 160) { - UIView.animate(withDuration: 0.3) { - self.balloonBackgroundView.alpha = isHidden ? 0 : 1 - self.balloonHeightConstraint?.update(offset: isHidden ? 0 : dynamicHeight) - self.layoutIfNeeded() - } + balloonBackgroundView.alpha = isHidden ? 0 : 1 + balloonBackgroundView.isHidden = isHidden + balloonHeightConstraint?.update(offset: isHidden ? 0 : dynamicHeight) + self.layoutIfNeeded() } - func updateBalloonPosition(for button: UIButton) { - guard let window = button.window else { return } - - let buttonFrameInWindow = button.convert(button.bounds, to: window) - let balloonFrameInWindow = balloonBackgroundView.convert(balloonBackgroundView.bounds, to: window) + DispatchQueue.main.async { + guard let window = button.window else { return } - let buttonCenterX = buttonFrameInWindow.midX + let buttonFrameInWindow = button.convert(button.bounds, to: window) + let balloonFrameInWindow = self.balloonBackgroundView.convert(self.balloonBackgroundView.bounds, to: window) - let relativeX = buttonCenterX - balloonFrameInWindow.minX + let buttonCenterX = buttonFrameInWindow.midX + let relativeX = buttonCenterX - balloonFrameInWindow.minX + let position = relativeX / self.balloonBackgroundView.bounds.width + let minPosition: CGFloat = 0.1 + let maxPosition: CGFloat = 0.9 + let clampedPosition = min(maxPosition, max(minPosition, position)) - let position = relativeX / balloonBackgroundView.bounds.width - - let minPosition: CGFloat = 0.1 // 왼쪽 여백 - let maxPosition: CGFloat = 0.9 // 오른쪽 여백 - let clampedPosition = min(maxPosition, max(minPosition, position)) - - balloonBackgroundView.arrowPosition = clampedPosition - balloonBackgroundView.setNeedsDisplay() + self.balloonBackgroundView.arrowPosition = clampedPosition + self.balloonBackgroundView.setNeedsDisplay() + } } private func updateBalloonPositionAccurately(for button: PPButton) { @@ -409,33 +372,51 @@ final class FilterBottomSheetView: UIView { balloonBackgroundView.arrowPosition = arrowPosition balloonBackgroundView.setNeedsDisplay() } + + private func updateSelectedButtonPosition() { + guard let selectedButton = locationContentView.subviews.first(where: { view in + guard let button = view as? PPButton else { return false } + return button.backgroundColor == .blu500 + }) as? PPButton else { return } + + updateBalloonPosition(for: selectedButton) + } } +// MARK: - Extensions extension FilterBottomSheetView { func update(locationText: String?, categoryText: String?) { var filters: [String] = [] if let locationText = locationText, !locationText.isEmpty { - filters.append(locationText) + let locations = locationText + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + filters += locations } + if let categoryText = categoryText, !categoryText.isEmpty { - filters.append(categoryText) + let categories = categoryText + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + filters += categories } filterChipsView.updateChips(with: filters) } -} +} extension FilterBottomSheetView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { - // 선택된 버튼 찾기 - guard let selectedButton = locationContentView.subviews.first(where: { view in - guard let button = view as? PPButton else { return false } - return button.backgroundColor == .blu500 - }) as? PPButton else { return } - - // 스크롤 중에도 실시간으로 위치 업데이트 - DispatchQueue.main.async { [weak self] in - self?.updateBalloonPosition(for: selectedButton) + updateSelectedButtonPosition() + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + updateSelectedButtonPosition() + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + updateSelectedButtonPosition() } } } diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetViewController.swift similarity index 83% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetViewController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetViewController.swift index c398fc99..b5261aa8 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterBottomSheetViewController.swift @@ -1,8 +1,8 @@ -import UIKit -import SnapKit -import RxSwift -import RxCocoa import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import UIKit final class FilterBottomSheetViewController: UIViewController, View { typealias Reactor = FilterBottomSheetReactor @@ -13,6 +13,8 @@ final class FilterBottomSheetViewController: UIViewController, View { var onSave: ((FilterData) -> Void)? var onDismiss: (() -> Void)? private var bottomConstraint: Constraint? + private var containerHeightConstraint: Constraint? + let containerView = FilterBottomSheetView() private var containerViewBottomConstraint: NSLayoutConstraint? private var savedLocation: String? @@ -23,8 +25,6 @@ final class FilterBottomSheetViewController: UIViewController, View { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.4) view.alpha = 0 - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) - view.addGestureRecognizer(tapGesture) return view }() // MARK: - Initialization @@ -38,24 +38,50 @@ final class FilterBottomSheetViewController: UIViewController, View { } // MARK: - Lifecycle - override func viewDidLoad() { super.viewDidLoad() setupLayout() setupGestures() setupCollectionView() + containerView.filterChipsView.onRemoveChip = { [weak self] removedOption in guard let self = self, let reactor = self.reactor else { return } - if reactor.currentState.selectedCategories.contains(removedOption) { + + let isCategory = reactor.currentState.selectedCategories.contains(removedOption) + let isSubRegion = reactor.currentState.selectedSubRegions.contains(removedOption) + + if isCategory { reactor.action.onNext(.toggleCategory(removedOption)) - } else if reactor.currentState.selectedSubRegions.contains(removedOption) { + } else if isSubRegion { reactor.action.onNext(.toggleSubRegion(removedOption)) } - } -// let tapOutsideGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapOutside)) -// tapOutsideGesture.cancelsTouchesInView = false -// self.view.addGestureRecognizer(tapOutsideGesture) + DispatchQueue.main.async { + let activeSegment = reactor.currentState.activeSegment + + if isCategory && activeSegment == 1 { + self.containerView.categoryCollectionView.reloadData() + } else if isSubRegion && activeSegment == 0 { + if let selectedIndex = reactor.currentState.selectedLocationIndex { + let location = reactor.currentState.locations[selectedIndex] + self.containerView.balloonBackgroundView.configure( + for: location.main, + subRegions: location.sub, + selectedRegions: reactor.currentState.selectedSubRegions, + selectionHandler: { [weak self] subRegion in + self?.reactor?.action.onNext(.toggleSubRegion(subRegion)) + }, + allSelectionHandler: { [weak self] in + self?.reactor?.action.onNext(.toggleAllSubRegions) + } + ) + } + } + + self.updateContainerHeight() + self.containerView.updateContentVisibility(isCategorySelected: activeSegment == 1) + } + } } @@ -71,7 +97,7 @@ final class FilterBottomSheetViewController: UIViewController, View { view.addSubview(containerView) containerView.snp.makeConstraints { make in make.left.right.equalToSuperview() - make.height.equalTo(UIScreen.main.bounds.height * 0.7) + containerHeightConstraint = make.height.greaterThanOrEqualTo(400).constraint bottomConstraint = make.bottom.equalToSuperview().offset(UIScreen.main.bounds.height).constraint } @@ -114,14 +140,11 @@ final class FilterBottomSheetViewController: UIViewController, View { } ) - }) .map { Reactor.Action.resetFilters } .bind(to: reactor.action) .disposed(by: disposeBag) - - containerView.saveButton.rx.tap .bind { [weak self] _ in guard let self = self, let reactor = self.reactor else { return } @@ -138,7 +161,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } .disposed(by: disposeBag) - containerView.closeButton.rx.tap .bind { [weak self] _ in guard let self = self, let reactor = self.reactor else { return } @@ -151,7 +173,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } .disposed(by: disposeBag) - // 5. 탭 변경 reactor.state.map { $0.activeSegment } .distinctUntilChanged() @@ -164,6 +185,9 @@ final class FilterBottomSheetViewController: UIViewController, View { self.containerView.updateBalloonHeight(isHidden: true) } self.containerView.updateContentVisibility(isCategorySelected: activeSegment == 1) + + // 여기에 컨테이너 높이 업데이트 추가 + self.updateContainerHeight() } .disposed(by: disposeBag) @@ -193,7 +217,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } .disposed(by: disposeBag) - let locationAndSubRegions = reactor.state .map { ($0.selectedLocationIndex, $0.selectedSubRegions) } .distinctUntilChanged { prev, curr in @@ -209,7 +232,6 @@ final class FilterBottomSheetViewController: UIViewController, View { guard let self = self, let reactor = self.reactor else { return } let (selectedIndexOptional, selectedSubRegions) = data - guard let selectedIndex = selectedIndexOptional, selectedIndex >= 0, selectedIndex < reactor.currentState.locations.count else { return } @@ -227,7 +249,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } ) - if let button = self.containerView.locationContentView.subviews[selectedIndex] as? UIButton { self.containerView.updateBalloonPosition(for: button) } @@ -266,8 +287,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } .disposed(by: disposeBag) - - reactor.state.map { $0.selectedSubRegions + $0.selectedCategories } .distinctUntilChanged() .bind { [weak self] selectedOptions in @@ -288,7 +307,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } .disposed(by: disposeBag) - reactor.state.map { $0.isSaveEnabled } .distinctUntilChanged() .observe(on: MainScheduler.instance) @@ -363,12 +381,36 @@ final class FilterBottomSheetViewController: UIViewController, View { self.view.layoutIfNeeded() } } + func updateContainerHeight() { + let contentHeight: CGFloat + + if containerView.segmentedControl.selectedSegmentIndex == 0 { + // 지역탭일 때 + contentHeight = containerView.balloonBackgroundView.calculateHeight() + + containerView.filterChipsView.frame.height + + containerView.segmentedControl.frame.height + + containerView.saveButton.frame.height + 100 // 패딩 및 여유 높이 + } else { + // 카테고리탭일 때 + contentHeight = containerView.categoryCollectionView.contentSize.height + + containerView.filterChipsView.frame.height + + containerView.segmentedControl.frame.height + + containerView.saveButton.frame.height + 100 + } + + // 최소 400, 최대는 화면 높이의 80%로 제한 + let finalHeight = min(max(contentHeight, 400), UIScreen.main.bounds.height * 0.8) + containerHeightConstraint?.update(offset: finalHeight) + + // 컨테이너 크기 변경 후 레이아웃 업데이트 + view.layoutIfNeeded() + } private func setupGestures() { - // dimmedView에만 탭 제스처를 설정하고 다른 제스처와의 충돌을 방지 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) + tapGesture.delegate = self dimmedView.addGestureRecognizer(tapGesture) - dimmedView.isUserInteractionEnabled = true // 확실히 활성화 + dimmedView.isUserInteractionEnabled = true // 패닝 제스처는 유지 let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)) @@ -386,7 +428,6 @@ final class FilterBottomSheetViewController: UIViewController, View { let index = reactor.currentState.locations.firstIndex(where: { $0.main == locations }) { reactor.action.onNext(.selectLocation(index)) - } // 4. 필터 칩 뷰 업데이트 @@ -403,8 +444,6 @@ final class FilterBottomSheetViewController: UIViewController, View { } } - - func hideBottomSheet() { UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn) { self.dimmedView.alpha = 0 @@ -478,3 +517,13 @@ extension FilterBottomSheetViewController: UICollectionViewDelegateFlowLayout { reactor?.action.onNext(.toggleCategory(category)) } } +extension FilterBottomSheetViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if gestureRecognizer.view == dimmedView { + // 딤드 영역에서만 터치 인식 + let touchPoint = touch.location(in: view) + return !containerView.frame.contains(touchPoint) + } + return true + } +} diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterCell.swift similarity index 100% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterCell.swift index f5783bfa..5c8981b7 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterCell.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class FilterCell: UICollectionViewCell { // MARK: - Properties diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChip.swift similarity index 97% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChip.swift index 0aa4964c..ec0fa088 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChip.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class FilterChip: UIButton { enum Style { @@ -72,7 +72,7 @@ final class FilterChip: UIButton { } let rightPadding: CGFloat = closeButton.isHidden ? 12 : 34 - contentEdgeInsets = UIEdgeInsets(top: 7, left: 12, bottom: 7, right: rightPadding) + contentEdgeInsets = UIEdgeInsets(top: 5, left: 12, bottom: 7, right: rightPadding) } // MARK: - Actions diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChipsView.swift similarity index 97% rename from Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChipsView.swift index 68aa555a..03c81149 100644 --- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FillterSheetView/FilterChipsView.swift @@ -1,4 +1,7 @@ import UIKit + +import DesignSystem + import SnapKit final class FilterChipsView: UIView { @@ -63,7 +66,7 @@ final class FilterChipsView: UIView { make.top.equalTo(titleLabel.snp.bottom).offset(12) make.leading.trailing.equalToSuperview() make.bottom.equalToSuperview().offset(-8) - make.height.equalTo(44) + make.height.greaterThanOrEqualTo(44).priority(.high) } emptyStateLabel.snp.makeConstraints { make in @@ -97,7 +100,7 @@ final class FilterChipsView: UIView { let removedFilter = filters[index] filters.remove(at: index) updateUI() - + // 콜백 호출 onRemoveChip?(removedFilter) } diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/FullScreenMapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/FullScreenMapViewController.swift new file mode 100644 index 00000000..16449153 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/FullScreenMapViewController.swift @@ -0,0 +1,254 @@ +import CoreLocation +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure + +import NMapsMap +import RxCocoa +import RxSwift +import SnapKit + +class FullScreenMapViewController: MapViewController { + // MARK: - Properties + private var initialStore: MapPopUpStore? + private var isFullScreenMode = true // 풀스크린 모드 플래그 추가 + private var markerLocked = false // 마커 상태 잠금 플래그 + private var initialMarker: NMFMarker? + + // MARK: - Initialization + init(store: MapPopUpStore?, existingMarker: NMFMarker? = nil) { + self.initialStore = store + self.initialMarker = existingMarker + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupFullScreenUI() + setupNavigation() +// configureInitialMapPosition() + self.navigationController?.navigationBar.isHidden = false + Logger.log("💡 초기 위치 구성 직전: initialStore=\(String(describing: initialStore?.name))", category: .debug) + configureInitialMapPosition() + + Logger.log("✅ FullScreenMapViewController - viewDidLoad 완료", category: .debug) + + mainView.mapView.touchDelegate = self + } + + private func setupNavigation() { + navigationItem.title = "찾아가는 길" + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.shadowColor = .clear + appearance.backgroundColor = .white + appearance.titleTextAttributes = [ + .foregroundColor: UIColor.black, + .font: UIFont.systemFont(ofSize: 15, weight: .regular) + ] + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance + navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(named: "bakcbutton")?.withRenderingMode(.alwaysOriginal), + style: .plain, + target: self, + action: #selector(backButtonTapped) + ) + navigationItem.leftBarButtonItem?.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } + + @objc private func backButtonTapped() { + dismiss(animated: true) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: false) + tabBarController?.tabBar.isHidden = true + markerLocked = true // 마커 상태 잠금 + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setNavigationBarHidden(false, animated: false) + tabBarController?.tabBar.isHidden = false + navigationItem.title = "찾아가는 길" + } + + // MARK: - Setup + private func setupFullScreenUI() { + mainView.filterChips.isHidden = true + mainView.listButton.isHidden = true + mainView.locationButton.isHidden = true + mainView.searchInput.isHidden = true + carouselView.isHidden = false + + mainView.mapView.snp.remakeConstraints { make in + make.edges.equalToSuperview() + } + + carouselView.snp.remakeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(140) + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16) + } + } + + private func configureInitialMapPosition() { + guard let store = initialStore else { return } + + let position = NMGLatLng(lat: store.latitude, lng: store.longitude) + + let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: 15.0) + mainView.mapView.cancelTransitions() + cameraUpdate.animation = .none + mainView.mapView.moveCamera(cameraUpdate) + + if let existingMarker = initialMarker { + // 기존 마커가 맵뷰에 설정되어 있지 않으면 설정 + if existingMarker.mapView == nil { + existingMarker.mapView = mainView.mapView + } + + // 명시적으로 TapMarker 스타일 적용 (selected 매개변수는 무시됨) + existingMarker.iconImage = NMFOverlayImage(name: "TapMarker") + existingMarker.width = 44 + existingMarker.height = 44 + existingMarker.anchor = CGPoint(x: 0.5, y: 1.0) + + currentMarker = existingMarker + } else { + // 새 마커 생성 시에도 TapMarker 적용 + let marker = NMFMarker() + marker.position = position + marker.iconImage = NMFOverlayImage(name: "TapMarker") + marker.width = 44 + marker.height = 44 + marker.anchor = CGPoint(x: 0.5, y: 1.0) + marker.userInfo = ["storeData": store] + marker.mapView = mainView.mapView + currentMarker = marker + } + + // 마커 잠금 설정 + markerLocked = true + + // 캐러셀 설정 + currentCarouselStores = [store] + carouselView.updateCards([store]) + carouselView.isHidden = false + } + + override func bind(reactor: MapReactor) { + super.bind(reactor: reactor) + + // 캐러셀 상태 관찰 + carouselView.rx.observe(Bool.self, "isHidden") + .distinctUntilChanged() + .subscribe(onNext: { [weak self] isHidden in + if let isHidden = isHidden, isHidden == true, self?.isFullScreenMode == true { + // 풀스크린 모드에서 캐러셀이 숨겨진 경우 다시 표시 + DispatchQueue.main.async { + self?.carouselView.isHidden = false + } + } + }) + .disposed(by: disposeBag) + } + + // 마커 스타일 업데이트 함수 - 항상 TapMarker로만 설정하도록 수정 + private func fullScreenUpdateMarkerStyle(marker: NMFMarker, selected: Bool) { + // 선택 여부와 상관없이 항상 TapMarker + marker.width = 44 + marker.height = 44 + marker.iconImage = NMFOverlayImage(name: "TapMarker") + marker.anchor = CGPoint(x: 0.5, y: 1.0) + } + + override func updateMarkerStyle(marker: NMFMarker, selected: Bool, isCluster: Bool, count: Int = 1, regionName: String = "") { + // 풀스크린 모드에서는 항상 TapMarker 스타일 적용 + if isFullScreenMode && markerLocked { + marker.width = 44 + marker.height = 44 + marker.iconImage = NMFOverlayImage(name: "TapMarker") + marker.anchor = CGPoint(x: 0.5, y: 1.0) + + if count > 1 { + marker.captionText = "\(count)" + } else { + marker.captionText = "" + } + return + } + + super.updateMarkerStyle(marker: marker, selected: selected, isCluster: isCluster, count: count, regionName: regionName) + } + + override func handleSingleStoreTap(_ marker: NMFMarker, store: MapPopUpStore) -> Bool { + isMovingToMarker = true + markerLocked = true + + if let previousMarker = currentMarker, previousMarker != marker { + fullScreenUpdateMarkerStyle(marker: previousMarker, selected: false) + } + + marker.iconImage = NMFOverlayImage(name: "TapMarker") + marker.width = 44 + marker.height = 44 + fullScreenUpdateMarkerStyle(marker: marker, selected: true) + currentMarker = marker + + currentCarouselStores = [store] + carouselView.updateCards([store]) + carouselView.isHidden = false + mainView.setStoreCardHidden(false, animated: true) + + let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position, zoomTo: 15.0) + mainView.mapView.cancelTransitions() + cameraUpdate.animation = .none + mainView.mapView.moveCamera(cameraUpdate) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.isMovingToMarker = false + } + + return true + } + + // 맵뷰 탭 처리 오버라이드 + override func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { + return + } + + // 카메라 이동 시작 시 호출 + override func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) { + if isFullScreenMode && markerLocked { + return + } + super.mapView(mapView, cameraWillChangeByReason: reason, animated: animated) + } + + // 카메라 이동 중 호출 + override func mapView(_ mapView: NMFMapView, cameraIsChangingByReason reason: Int) { + if isFullScreenMode && markerLocked { + // 기존 동작을 방지하고 풀스크린 동작 수행 + return + } + super.mapView(mapView, cameraIsChangingByReason: reason) + } + + override func handleRegionalClusterTap(_ marker: NMFMarker, clusterData: ClusterMarkerData) -> Bool { + return false + } + + override func handleMicroClusterTap(_ marker: NMFMarker, storeArray: [MapPopUpStore]) -> Bool { + return false + } +} diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideReactor.swift similarity index 91% rename from Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideReactor.swift index 089cb950..2bff7fda 100644 --- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideReactor.swift @@ -1,8 +1,12 @@ -import ReactorKit -import RxSwift import CoreLocation import UIKit +import DomainInterface +import Infrastructure + +import ReactorKit +import RxSwift + final class MapGuideReactor: Reactor { // MARK: - Actions enum Action { @@ -42,15 +46,15 @@ final class MapGuideReactor: Reactor { let initialState: State private let popUpStoreId: Int64 - private let directionRepository: MapDirectionRepository + private let mapDirectionRepository: MapDirectionRepository // MARK: - Init init( popUpStoreId: Int64, - repository: MapDirectionRepository = DefaultMapDirectionRepository(provider: ProviderImpl()) + mapDirectionRepository: MapDirectionRepository ) { self.popUpStoreId = popUpStoreId - self.directionRepository = repository + self.mapDirectionRepository = mapDirectionRepository self.initialState = State() } @@ -70,7 +74,7 @@ final class MapGuideReactor: Reactor { return Observable.just(.navigateBack) case .viewDidLoad(let id): - return directionRepository.getPopUpDirection(popUpStoreId: id) + return mapDirectionRepository.getPopUpDirection(popUpStoreId: id) .map { response -> [Mutation] in return [ .setMap(CLLocationCoordinate2D(latitude: response.latitude, longitude: response.longitude)), @@ -117,11 +121,8 @@ final class MapGuideReactor: Reactor { return Observable.just(.showToast("지원하지 않는 맵 앱입니다.")) } - Logger.log(message: "🗺 맵 앱 열기 시도: \(urlScheme)", category: .debug) - if let url = URL(string: urlScheme) { if UIApplication.shared.canOpenURL(url) { - Logger.log(message: "✅ \(appType) 앱 실행", category: .debug) UIApplication.shared.open(url, options: [:], completionHandler: nil) return Observable.empty() } else { @@ -129,7 +130,7 @@ final class MapGuideReactor: Reactor { if appType.lowercased() == "apple" { return Observable.just(.showToast("애플 지도 앱을 열 수 없습니다.")) } else { - Logger.log(message: "❌ \(appType) 앱 미설치 - 앱스토어로 이동", category: .debug) + Logger.log("❌ \(appType) 앱 미설치 - 앱스토어로 이동", category: .debug) if let appStoreURL = URL(string: appStoreUrl) { UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) } diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideViewController.swift new file mode 100644 index 00000000..d583d286 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/FindMap/MapGuideView/MapGuideViewController.swift @@ -0,0 +1,397 @@ +import CoreLocation +import UIKit + +import DomainInterface +import Infrastructure + +import NMapsMap +import ReactorKit +import RxSwift +import SnapKit + +final class MapGuideViewController: UIViewController, View { + // MARK: - Properties + var disposeBag = DisposeBag() + private let popupStoreIdentifier: Int64 + private var currentCarouselStoreList: [MapPopUpStore] = [] // 현재 선택된 스토어 목록 + + // MARK: - UI Components + + private let dimmingView: UIView = { + let viewInstance = UIView() + viewInstance.backgroundColor = UIColor.gray.withAlphaComponent(0.7) + viewInstance.alpha = 0 + return viewInstance + }() + + private let modalCardView: UIView = { + let viewInstance = UIView() + viewInstance.backgroundColor = .white + viewInstance.layer.cornerRadius = 16 + viewInstance.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + viewInstance.layer.shadowColor = UIColor.black.cgColor + viewInstance.layer.shadowOpacity = 0.1 + viewInstance.layer.shadowOffset = .zero + viewInstance.layer.shadowRadius = 8 + return viewInstance + }() + + private let titleLabel: UILabel = { + let labelInstance = UILabel() + labelInstance.text = "찾아가는 길" + labelInstance.font = UIFont.boldSystemFont(ofSize: 17) + labelInstance.textColor = .black + return labelInstance + }() + + private let closeButton: UIButton = { + let buttonInstance = UIButton(type: .system) + let image = UIImage(named: "icon_xmark")?.withRenderingMode(.alwaysOriginal) + buttonInstance.setImage(image, for: .normal) + return buttonInstance + }() + + private let mapView: NMFMapView = { + let mapViewInstance = NMFMapView() + mapViewInstance.layer.borderWidth = 1 + mapViewInstance.layer.borderColor = UIColor.g100.cgColor + mapViewInstance.layer.cornerRadius = 12 + return mapViewInstance + }() + + private let expandButton: UIButton = { + let buttonInstance = UIButton() + buttonInstance.setImage(UIImage(named: "Expandable"), for: .normal) + buttonInstance.backgroundColor = UIColor.white + buttonInstance.layer.cornerRadius = 16 + buttonInstance.clipsToBounds = true + return buttonInstance + }() + + private let promptLabel: UILabel = { + let labelInstance = UILabel() + labelInstance.text = "지도 앱으로\n바로 찾아볼까요?" + labelInstance.font = UIFont.systemFont(ofSize: 15, weight: .medium) + labelInstance.textColor = .darkGray + labelInstance.numberOfLines = 2 + return labelInstance + }() + + private let naverButton: UIButton = { + let buttonInstance = UIButton() + buttonInstance.setImage(UIImage(named: "naver"), for: .normal) + buttonInstance.layer.cornerRadius = 24 + buttonInstance.layer.borderWidth = 1 + buttonInstance.layer.borderColor = UIColor.g100.cgColor + buttonInstance.clipsToBounds = true + return buttonInstance + }() + + private let kakaoButton: UIButton = { + let buttonInstance = UIButton() + buttonInstance.setImage(UIImage(named: "kakao"), for: .normal) + buttonInstance.layer.cornerRadius = 24 + buttonInstance.layer.borderWidth = 1 + buttonInstance.layer.borderColor = UIColor.g100.cgColor + buttonInstance.clipsToBounds = true + return buttonInstance + }() + + private let appleButton: UIButton = { + let buttonInstance = UIButton() + buttonInstance.setImage(UIImage(named: "AppleMap"), for: .normal) + buttonInstance.layer.cornerRadius = 24 + buttonInstance.layer.borderWidth = 1 + buttonInstance.layer.borderColor = UIColor.g100.cgColor + buttonInstance.clipsToBounds = true + return buttonInstance + }() + + private var modalCardBottomConstraint: Constraint? + + // MARK: - Initializer + init(popUpStoreId: Int64) { + self.popupStoreIdentifier = popUpStoreId + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .overFullScreen // 모달 스타일 설정 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupTapGesture() // 탭 제스처 설정 추가 + presentModalCard() + } + + // MARK: - Gesture Setup + /// 딤드 영역 탭 제스처 설정 + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapOnDimmingView)) + dimmingView.addGestureRecognizer(tapGesture) + dimmingView.isUserInteractionEnabled = true // 중요: 상호작용 활성화 + } + + /// 딤드 영역 탭 처리: 탭 위치가 모달 카드 영역이 아닌 경우에만 닫기 + @objc private func handleTapOnDimmingView(_ sender: UITapGestureRecognizer) { + let tapLocation = sender.location(in: view) + if !modalCardView.frame.contains(tapLocation) { + dismissModalCard() + } + } + + // MARK: - ReactorKit Binding + func bind(reactor: MapGuideReactor) { + reactor.action.onNext(.viewDidLoad(self.popupStoreIdentifier)) + + // 닫기 버튼 + closeButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.dismissModalCard() + }) + .disposed(by: disposeBag) + + // 지도 앱 열기 + naverButton.rx.tap + .map { Reactor.Action.openMapApp("naver") } + .bind(to: reactor.action) + .disposed(by: disposeBag) + kakaoButton.rx.tap + .map { Reactor.Action.openMapApp("kakao") } + .bind(to: reactor.action) + .disposed(by: disposeBag) + appleButton.rx.tap + .map { Reactor.Action.openMapApp("apple") } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + // 확장 버튼 탭 처리 및 지도 풀스크린 전환 + expandButton.rx.tap + .throttle(.milliseconds(300), scheduler: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + guard let strongSelf = self else { return } + + let mapReactorInstance = MapReactor( + mapUseCase: DIContainer.resolve(MapUseCase.self), + mapDirectionRepository: DIContainer.resolve(MapDirectionRepository.self) + ) + + if let selectedStore = strongSelf.currentCarouselStoreList.first { + mapReactorInstance.action.onNext(.didSelectItem(selectedStore)) + + // 현재 지도에 표시된 마커 생성 또는 가져오기 + let markerInstance = NMFMarker() + markerInstance.position = NMGLatLng(lat: selectedStore.latitude, lng: selectedStore.longitude) + markerInstance.iconImage = NMFOverlayImage(name: "TapMarker") + markerInstance.width = 44 + markerInstance.height = 44 + markerInstance.anchor = CGPoint(x: 0.5, y: 1.0) + markerInstance.userInfo = ["storeData": selectedStore] + + // 풀스크린 지도 뷰 컨트롤러에 선택된 마커 정보 전달 + let fullScreenMapViewController = FullScreenMapViewController(store: selectedStore, existingMarker: markerInstance) + fullScreenMapViewController.reactor = mapReactorInstance + + let navigationController = UINavigationController(rootViewController: fullScreenMapViewController) + navigationController.modalPresentationStyle = .fullScreen + strongSelf.present(navigationController, animated: true) + } else { + mapReactorInstance.action.onNext(.viewDidLoad(strongSelf.popupStoreIdentifier)) + + mapReactorInstance.state + .map { $0.searchResult } + .distinctUntilChanged() + .compactMap { $0 } + .take(1) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { store in + let markerInstance = NMFMarker() + markerInstance.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + markerInstance.iconImage = NMFOverlayImage(name: "TapMarker") + markerInstance.width = 44 + markerInstance.height = 44 + markerInstance.anchor = CGPoint(x: 0.5, y: 1.0) + markerInstance.userInfo = ["storeData": store] + + let fullScreenMapViewController = FullScreenMapViewController(store: store, existingMarker: markerInstance) + fullScreenMapViewController.reactor = mapReactorInstance + + let navigationController = UINavigationController(rootViewController: fullScreenMapViewController) + navigationController.modalPresentationStyle = .fullScreen + strongSelf.present(navigationController, animated: true) + }) + .disposed(by: strongSelf.disposeBag) + } + }) + .disposed(by: disposeBag) + + // 목적지 좌표에 따른 마커 및 카메라 설정 + reactor.state + .map { $0.destinationCoordinate } + .compactMap { $0 } + .subscribe(onNext: { [weak self] coordinate in + self?.setupMarker(at: coordinate) + }) + .disposed(by: disposeBag) + + // searchResult로 현재 캐러셀 스토어 목록 업데이트 + reactor.state + .map { $0.searchResult } + .distinctUntilChanged() + .compactMap { $0 } + .subscribe(onNext: { [weak self] store in + self?.currentCarouselStoreList = [store] + }) + .disposed(by: disposeBag) + + // Dismiss 처리 + reactor.state + .map { $0.shouldDismiss } + .distinctUntilChanged() + .filter { $0 } + .subscribe(onNext: { [weak self] _ in + self?.dismissModalCard() + }) + .disposed(by: disposeBag) + } + + // MARK: - UI Setup + private func setupUI() { + view.backgroundColor = .clear // 배경색을 clear로 설정하여 항상 딤드 뷰가 보이도록 함 + view.addSubview(dimmingView) + dimmingView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + view.addSubview(modalCardView) + modalCardView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(408) + self.modalCardBottomConstraint = make.bottom.equalToSuperview().offset(408).constraint + } + + let topContainerView = UIView() + modalCardView.addSubview(topContainerView) + topContainerView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.leading.trailing.equalToSuperview() + make.height.equalTo(44) + } + + topContainerView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().offset(20) + } + + topContainerView.addSubview(closeButton) + closeButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(16) + make.width.height.equalTo(24) + } + + modalCardView.addSubview(mapView) + mapView.snp.makeConstraints { make in + make.top.equalTo(topContainerView.snp.bottom).offset(20) + make.leading.trailing.equalToSuperview().inset(20) + make.height.equalTo(240) // 높이 약간 조정 + } + + modalCardView.addSubview(expandButton) + expandButton.snp.makeConstraints { make in + make.bottom.equalTo(mapView.snp.bottom).offset(-10) + make.trailing.equalTo(mapView.snp.trailing).offset(-10) + make.width.height.equalTo(32) + } + + let bottomContainerView = UIView() + modalCardView.addSubview(bottomContainerView) + bottomContainerView.snp.makeConstraints { make in + make.top.equalTo(mapView.snp.bottom).offset(20) + make.leading.trailing.equalToSuperview().inset(20) + make.height.equalTo(44) + make.bottom.equalTo(modalCardView.snp.bottom).inset(60) + } + + bottomContainerView.addSubview(promptLabel) + promptLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.centerY.equalToSuperview() + } + + let applicationStackView = UIStackView(arrangedSubviews: [naverButton, kakaoButton, appleButton]) + applicationStackView.axis = .horizontal + applicationStackView.alignment = .center + applicationStackView.spacing = 16 + applicationStackView.distribution = .fillEqually + + bottomContainerView.addSubview(applicationStackView) + applicationStackView.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.centerY.equalToSuperview() + [naverButton, kakaoButton, appleButton].forEach { button in + button.snp.makeConstraints { constraint in + constraint.size.equalTo(CGSize(width: 48, height: 48)) + } + } + } + } + + private func presentModalCard() { + self.dimmingView.alpha = 1 + + UIView.animate( + withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut + ) { + self.modalCardBottomConstraint?.update(offset: 0) + self.view.layoutIfNeeded() + } + } + + private func setupMarker(at coordinate: CLLocationCoordinate2D) { + mapView.subviews.forEach { subview in + if subview is NMFMarker { + subview.removeFromSuperview() + } + } + + // 새 마커 생성 및 설정 + let markerInstance = NMFMarker() + markerInstance.position = NMGLatLng(lat: coordinate.latitude, lng: coordinate.longitude) + markerInstance.iconImage = NMFOverlayImage(name: "TapMarker") + markerInstance.width = 44 + markerInstance.height = 44 + markerInstance.anchor = CGPoint(x: 0.5, y: 1.0) + + // 먼저 마커를 지도에 추가 + markerInstance.mapView = mapView + + // 카메라 위치 업데이트 + let cameraUpdate = NMFCameraUpdate( + scrollTo: NMGLatLng(lat: coordinate.latitude, lng: coordinate.longitude), + zoomTo: 15.0 + ) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + mapView.moveCamera(cameraUpdate) + } + + private func dismissModalCard() { + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { + self.dimmingView.alpha = 0 + self.modalCardBottomConstraint?.update(offset: 408) + self.view.layoutIfNeeded() + } completion: { _ in + self.dismiss(animated: false) + } + } +} diff --git a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/MapPopupCarouselView.swift similarity index 92% rename from Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/MapPopupCarouselView.swift index cd217101..8c1a17f7 100644 --- a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/MapPopupCarouselView.swift @@ -1,6 +1,9 @@ import UIKit -import SnapKit + +import DomainInterface + import FloatingPanel +import SnapKit final class MapPopupCarouselView: UICollectionView { // 스크롤 멈췄을 때의 콜백 @@ -13,11 +16,11 @@ final class MapPopupCarouselView: UICollectionView { let centerX = self.contentOffset.x + self.bounds.width / 2 - for i in 0..) { - let layout = self.collectionViewLayout as! UICollectionViewFlowLayout + guard let layout = self.collectionViewLayout as? UICollectionViewFlowLayout else { return } let itemWidth = layout.itemSize.width let spacing = layout.minimumLineSpacing @@ -143,10 +145,11 @@ extension MapPopupCarouselView: UICollectionViewDataSource, UICollectionViewDele } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = dequeueReusableCell( + guard let cell = dequeueReusableCell( withReuseIdentifier: PopupCardCell.identifier, for: indexPath - ) as! PopupCardCell + ) as? PopupCardCell + else { return UICollectionViewCell() } cell.configure(with: popupCards[indexPath.item]) return cell } diff --git a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/PopupCardCell.swift similarity index 99% rename from Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/PopupCardCell.swift index 4d1ee89d..7da20209 100644 --- a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapPopupCardView/PopupCardCell.swift @@ -1,6 +1,8 @@ import UIKit + +import DomainInterface + import SnapKit -import Kingfisher final class PopupCardCell: UICollectionViewCell { static let identifier = "PopupCardCell" @@ -22,8 +24,6 @@ final class PopupCardCell: UICollectionViewCell { configureUI() } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -86,7 +86,6 @@ final class PopupCardCell: UICollectionViewCell { } } - private func configureUI() { contentView.backgroundColor = UIColor.white categoryLabel.textColor = .systemBlue diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapMarker.swift similarity index 72% rename from Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapMarker.swift index a90eb377..e6b9f007 100644 --- a/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapMarker.swift @@ -1,13 +1,15 @@ import UIKit + +import DesignSystem + +import NMapsMap import SnapKit -import GoogleMaps final class MapMarker: UIView { // MARK: - Components private(set) var isSelected: Bool = false var currentInput: Input? - private let markerImageView: UIImageView = { let imageView = UIImageView() imageView.image = UIImage(named: "Marker") @@ -66,7 +68,7 @@ final class MapMarker: UIView { // MARK: - Init init() { - super.init(frame: CGRect(x: 0, y: 0, width: 80, height: 32)) + super.init(frame: CGRect(x: 0, y: 0, width: 32, height: 32)) setUpConstraints() } @@ -87,10 +89,8 @@ private extension MapMarker { labelStackView.addArrangedSubview(countLabel) countBadgeView.addSubview(badgeCountLabel) - self.snp.makeConstraints { make in - make.width.equalTo(200) - make.height.equalTo(70) - } + // 고정된 크기 제약조건 제거 + // 대신 내부 컨텐츠에 맞게 크기가 조정되도록 변경 markerImageView.snp.makeConstraints { make in make.centerX.equalToSuperview() @@ -100,12 +100,12 @@ private extension MapMarker { clusterContainer.snp.makeConstraints { make in make.center.equalToSuperview() - make.height.equalTo(24) - make.width.equalTo(80) + make.height.equalTo(32) + make.width.greaterThanOrEqualTo(80) } labelStackView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)) + make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)) } countBadgeView.snp.makeConstraints { make in @@ -141,10 +141,14 @@ private extension MapMarker { countBadgeView.snp.remakeConstraints { make in make.width.height.equalTo(20) - make.top.equalTo(markerImageView.snp.top).offset(isSelected ? 0 : -4) - make.right.equalTo(markerImageView.snp.right).offset(isSelected ? 0 : 4) + if isSelected { + make.top.equalTo(markerImageView.snp.top).offset(-4) + make.right.equalTo(markerImageView.snp.right).offset(4) + } else { + make.top.equalTo(markerImageView.snp.top).offset(-4) + make.right.equalTo(markerImageView.snp.right).offset(4) + } } - self.layoutIfNeeded() CATransaction.commit() } @@ -181,8 +185,6 @@ extension MapMarker: Inputable { CATransaction.commit() } - - private func setupClusterMarker(_ input: Input) { markerImageView.isHidden = true clusterContainer.isHidden = false @@ -191,8 +193,10 @@ extension MapMarker: Inputable { regionLabel.text = input.regionName countLabel.text = " \(input.count)" + // 클러스터 마커 크기 계산 - 텍스트 내용에 맞게 동적으로 조정 + labelStackView.layoutIfNeeded() let stackSize = labelStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) - let requiredWidth = stackSize.width + 24 + let requiredWidth = max(stackSize.width + 24, 80) // 최소 너비 보장 clusterContainer.snp.remakeConstraints { make in make.center.equalToSuperview() @@ -203,18 +207,33 @@ extension MapMarker: Inputable { labelStackView.snp.remakeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)) } + + self.frame.size = CGSize(width: requiredWidth, height: 32) } private func setupSingleMarker(_ input: Input) { markerImageView.isHidden = false clusterContainer.isHidden = true + updateMarkerImage(isSelected: input.isSelected) + let size = input.isSelected ? 44 : 32 + if input.count > 1 { countBadgeView.isHidden = false badgeCountLabel.text = "\(input.count)" + + countBadgeView.snp.remakeConstraints { make in + make.width.height.equalTo(20) + make.top.equalTo(markerImageView.snp.top).offset(input.isSelected ? 0 : -4) + make.right.equalTo(markerImageView.snp.right).offset(input.isSelected ? 2 : 4) + } + + self.frame.size = CGSize(width: size + 8, height: size) } else { countBadgeView.isHidden = true + + self.frame.size = CGSize(width: size, height: size) } } } @@ -223,7 +242,31 @@ extension MapMarker { var imageView: UIImageView { return markerImageView } + + func asImage() -> UIImage? { + if let input = currentInput { + if input.isCluster { + self.layoutIfNeeded() + let clusterSize = clusterContainer.bounds.size + self.frame = CGRect(x: 0, y: 0, width: clusterSize.width + 8, height: clusterSize.height + 8) + } else { + let size = input.isSelected ? 44 : 32 + let extraWidth = (input.count > 1) ? 10 : 0 + self.frame = CGRect(x: 0, y: 0, width: size + extraWidth, height: size + 4) + } + } + + self.layoutIfNeeded() + UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale) + defer { UIGraphicsEndImageContext() } + if let context = UIGraphicsGetCurrentContext() { + self.layer.render(in: context) + return UIGraphicsGetImageFromCurrentImageContext() + } + return nil + } } + extension MapMarker.Input: Equatable { static func == (lhs: MapMarker.Input, rhs: MapMarker.Input) -> Bool { return lhs.isSelected == rhs.isSelected && diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapReactor.swift similarity index 75% rename from Poppool/Poppool/Presentation/Map/MapView/MapReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapReactor.swift index 8603c691..d5d1c543 100644 --- a/Poppool/Poppool/Presentation/Map/MapView/MapReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapReactor.swift @@ -1,6 +1,10 @@ +import CoreLocation + +import DomainInterface +import Infrastructure + import ReactorKit import RxSwift -import CoreLocation final class MapReactor: Reactor { // MARK: - Reactor @@ -15,6 +19,7 @@ final class MapReactor: Reactor { case fetchCategories case updateBothFilters(locations: [String], categories: [String]) // 새로 추가 case didSelectItem(MapPopUpStore) + case fetchAllStores case refreshMarkers(northEastLat: Double, northEastLon: Double, southWestLat: Double, southWestLon: Double) case viewportChanged( northEastLat: Double, @@ -41,7 +46,7 @@ final class MapReactor: Reactor { case setSelectedStore(MapPopUpStore) // 선택된 스토어 상태 case setViewportStores([MapPopUpStore]) case setError(Error?) - case setCategoryMapping([String: Int64]) + case setCategoryMapping([String: Int]) } struct State { @@ -59,16 +64,16 @@ final class MapReactor: Reactor { var selectedStore: MapPopUpStore? = nil // 선택된 스토어 var viewportStores: [MapPopUpStore] = [] var error: Error? = nil - var categoryMapping: [String: Int64] = [:] + var categoryMapping: [String: Int] = [:] } let initialState: State - private let useCase: MapUseCase - private let directionRepository: MapDirectionRepository + private let mapUseCase: MapUseCase + private let mapDirectionRepository: MapDirectionRepository - init(useCase: MapUseCase, directionRepository: MapDirectionRepository) { - self.useCase = useCase - self.directionRepository = directionRepository + init(mapUseCase: MapUseCase, mapDirectionRepository: MapDirectionRepository) { + self.mapUseCase = mapUseCase + self.mapDirectionRepository = mapDirectionRepository self.initialState = State() } private func store(_ store: MapPopUpStore, matches filter: String) -> Bool { @@ -89,17 +94,14 @@ final class MapReactor: Reactor { func mutate(action: Action) -> Observable { switch action { case .fetchCategories: - Logger.log(message: "카테고리 매핑", category: .debug) - return useCase.fetchCategories() + return mapUseCase.fetchCategories() .map { categories in - let mapping = categories.reduce(into: [String: Int64]()) { dict, category in + let mapping = categories.reduce(into: [String: Int]()) { dict, category in dict[category.category] = category.categoryId } - Logger.log(message: "생성된 카테고리 매핑: \(mapping)", category: .debug) return .setCategoryMapping(mapping) } .catch { error in - Logger.log(message: "카테고리 매핑 생성 중 오류: \(error.localizedDescription)", category: .error) return .just(.setError(error)) } @@ -109,7 +111,7 @@ final class MapReactor: Reactor { return .concat([ .just(.setSearchResults([])), .just(.setLoading(true)), - useCase.searchStores(query: query, categories: categoryIDs) + mapUseCase.searchStores(query: query, categories: categoryIDs) .flatMap { results -> Observable in if results.isEmpty { return .just(.setToastMessage("검색 결과가 없습니다.")) @@ -124,18 +126,9 @@ final class MapReactor: Reactor { let categoryIDs = currentState.selectedCategoryFilters .compactMap { currentState.categoryMapping[$0] } - Logger.log( - message: """ - 지도 영역이 변경되었습니다: - 📍 선택된 카테고리: \(currentState.selectedCategoryFilters) - 🔢 변환된 카테고리 ID: \(categoryIDs) - """, - category: .debug - ) - return .concat([ .just(.setLoading(true)), - useCase.fetchStoresInBounds( + mapUseCase.fetchStoresInBounds( northEastLat: northEastLat, northEastLon: northEastLon, southWestLat: southWestLat, @@ -166,8 +159,6 @@ final class MapReactor: Reactor { .just(.setLoading(false)) ]) - - case let .updateBothFilters(locations, categories): return .concat([ .just(.setLocationFilters(locations)), @@ -198,7 +189,7 @@ final class MapReactor: Reactor { .compactMap { currentState.categoryMapping[$0] } return Observable.concat([ Observable.just(.setLoading(true)), - useCase.fetchStoresInBounds( + mapUseCase.fetchStoresInBounds( northEastLat: northEastLat, northEastLon: northEastLon, southWestLat: southWestLat, @@ -213,20 +204,6 @@ final class MapReactor: Reactor { Observable.just(.setLoading(false)) ]) - case let .updateBothFilters(locations, categories): - Logger.log( - message: """ - Updating both filters: - - Locations: \(locations) - - Categories: \(categories) - """, - category: .debug - ) - return .concat([ - .just(.setLocationFilters(locations)), - .just(.setCategoryFilters(categories)) - ]) - case let .clearFilters(type): switch type { case .location: @@ -242,40 +219,19 @@ final class MapReactor: Reactor { } case .viewDidLoad(let id): - return directionRepository.getPopUpDirection(popUpStoreId: id) + return mapDirectionRepository.getPopUpDirection(popUpStoreId: id) .do( - onNext: { response in - Logger.log( - message: """ - ✅ [응답]: 요청 성공 - popUpStoreId: \(id) - - ID: \(response.id) - - 이름: \(response.name) - - 카테고리: \(response.categoryName) - - 위도: \(response.latitude), 경도: \(response.longitude) - - 주소: \(response.address) - """, - category: .network - ) + onNext: { _ in }, onError: { error in Logger.log( - message: "❌ [에러]: 요청 실패 - \(error.localizedDescription)", + "❌ [에러]: 요청 실패 - \(error.localizedDescription)", category: .error ) }, - onSubscribe: { - Logger.log( - message: "🌎 [네트워크]: 요청 보냄 - popUpStoreId: \(id)", - category: .network - ) - } + onSubscribe: { } ) - .map { dto in - let response = dto.toDomain() - Logger.log( - message: "🛠️ [도메인 매핑]: \(response)", - category: .debug - ) + .map { response in return MapPopUpStore( id: response.id, category: response.categoryName, @@ -292,20 +248,51 @@ final class MapReactor: Reactor { ) } .map { store in - Logger.log( - message: "📌 [최종 데이터]: \(store)", - category: .debug - ) return .setSearchResult(store) } + case .fetchAllStores: + // 한국 전체 영역에 대한 바운드 설정 + let koreaRegion = ( + northEast: (lat: 38.0, lon: 132.0), + southWest: (lat: 33.0, lon: 124.0) + ) + + let categoryIDs = currentState.selectedCategoryFilters + .compactMap { currentState.categoryMapping[$0] } + + return .concat([ + .just(.setLoading(true)), + mapUseCase.fetchStoresInBounds( + northEastLat: koreaRegion.northEast.lat, + northEastLon: koreaRegion.northEast.lon, + southWestLat: koreaRegion.southWest.lat, + southWestLon: koreaRegion.southWest.lon, + categories: categoryIDs + ) + .map { stores -> Mutation in + var filteredStores = stores + + let locationFilters = self.currentState.selectedLocationFilters + if !locationFilters.isEmpty { + filteredStores = stores.filter { store in + return locationFilters.contains { filter in + return self.store(store, matches: filter) + } + } + } + + return .setViewportStores(filteredStores) + } + .catch { error in .just(.setError(error)) }, + .just(.setLoading(false)) + ]) case let .didSelectItem(store): return .concat([ .just(.setSelectedStore(store)), - .just(.setViewportStores(currentState.viewportStores)), // ✅ 선택된 마커를 캐러셀에서 최우선으로 반영 + .just(.setViewportStores(currentState.viewportStores)) // ✅ 선택된 마커를 캐러셀에서 최우선으로 반영 ]) - default: return .empty() } @@ -328,18 +315,15 @@ final class MapReactor: Reactor { case let .setSearchResult(store): newState.searchResult = store - Logger.log(message: "🎯 단일 검색 결과 설정: \(store)", category: .debug) case let .setToastMessage(message): newState.toastMessage = message case let .setActiveFilter(filterType): newState.activeFilterType = filterType - Logger.log(message: "🎯 Active Filter Changed: \(String(describing: filterType))", category: .debug) case let .setLocationFilters(filters): newState.selectedLocationFilters = filters - Logger.log(message: "선택된 위치 필터가 업데이트: \(filters)", category: .debug) case let .setCategoryFilters(filters): newState.selectedCategoryFilters = filters @@ -357,14 +341,6 @@ final class MapReactor: Reactor { newState.selectedCategoryFilters = [] case let .updateBothFilters(locations, categories): - Logger.log( - message: """ - 💾 필터 상태 업데이트 - 📍 이전 위치 필터: \(newState.selectedLocationFilters) - 🏷️ 이전 카테고리 필터: \(newState.selectedCategoryFilters) - """, - category: .debug - ) newState.selectedLocationFilters = locations newState.selectedCategoryFilters = categories @@ -377,7 +353,7 @@ final class MapReactor: Reactor { } Logger.log( - message: """ + """ Updated viewport stores: - Total: \(updatedStores.count) - Selected Store: \(state.selectedStore?.name ?? "None") @@ -387,16 +363,14 @@ final class MapReactor: Reactor { newState.viewportStores = updatedStores - case let .setSelectedStore(store): newState.selectedStore = store - print("[DEBUG] 📍 Selected Store: \(store.name)") case let .setError(error): newState.error = error if let error = error { Logger.log( - message: """ + """ Error occurred in MapReactor: - Description: \(error.localizedDescription) - Domain: \(String(describing: (error as NSError).domain)) @@ -407,10 +381,6 @@ final class MapReactor: Reactor { } case let .setCategoryMapping(mapping): - Logger.log( - message: "카테고리 매핑 업데이트 완료: \(mapping)", - category: .debug - ) newState.categoryMapping = mapping } return newState diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapSearchInput.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapSearchInput.swift similarity index 86% rename from Poppool/Poppool/Presentation/Map/MapView/MapSearchInput.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapSearchInput.swift index f10a3973..cccf90ba 100644 --- a/Poppool/Poppool/Presentation/Map/MapView/MapSearchInput.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapSearchInput.swift @@ -1,7 +1,11 @@ import UIKit + +import DesignSystem + import ReactorKit import RxCocoa import RxSwift +import Then final class MapSearchInput: UIView, View { // MARK: - Components @@ -17,7 +21,7 @@ final class MapSearchInput: UIView, View { private let containerView: UIView = { let view = UIView() view.backgroundColor = .white - view.layer.cornerRadius = 8 + view.layer.cornerRadius = 4 return view }() @@ -28,22 +32,20 @@ final class MapSearchInput: UIView, View { return iv }() - let searchTextField: UITextField = { - let textField = UITextField() - textField.placeholder = "팝업스토어명, 지역을 입력해보세요" - textField.font = UIFont.systemFont(ofSize: 14, weight: .regular) - textField.clearButtonMode = .whileEditing - textField.textColor = .g400 - textField.returnKeyType = .search - textField.enablesReturnKeyAutomatically = true - textField.attributedPlaceholder = NSAttributedString( + let searchTextField = UITextField().then { + $0.placeholder = "팝업스토어명, 지역을 입력해보세요" + $0.font = .korFont(style: .regular, size: 14) + $0.clearButtonMode = .whileEditing + $0.textColor = .g400 + $0.returnKeyType = .search + $0.enablesReturnKeyAutomatically = true + $0.attributedPlaceholder = NSAttributedString( string: "팝업스토어명, 지역을 입력해보세요", attributes: [NSAttributedString.Key.foregroundColor: UIColor.g400] ) // 편집은 하지 않고, 탭으로 화면 전환을 유도 - textField.isEnabled = false - return textField - }() + $0.isEnabled = false + } // MARK: - Init init() { @@ -117,13 +119,13 @@ private extension MapSearchInput { } searchIcon.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) + make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() make.size.equalTo(20) } searchTextField.snp.makeConstraints { make in - make.leading.equalTo(searchIcon.snp.trailing).offset(8) + make.leading.equalTo(searchIcon.snp.trailing).offset(4) make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-16) } diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapView.swift similarity index 79% rename from Poppool/Poppool/Presentation/Map/MapView/MapView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapView.swift index 30d4f7c2..f5737db4 100644 --- a/Poppool/Poppool/Presentation/Map/MapView/MapView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapView.swift @@ -1,19 +1,21 @@ -import UIKit +import NMapsMap import SnapKit -import GoogleMaps +import UIKit final class MapView: UIView { // MARK: - Components - let mapView: GMSMapView = { - let camera = GMSCameraPosition(latitude: 37.5666, longitude: 126.9784, zoom: 14) - let view = GMSMapView(frame: .zero, camera: camera) - view.settings.myLocationButton = false - view.setMinZoom(7.5, maxZoom: 20) + let mapView: NMFMapView = { + let view = NMFMapView() + view.positionMode = .disabled + view.zoomLevel = 14 + + view.extent = NMGLatLngBounds( + southWest: NMGLatLng(lat: 33.0, lng: 124.0), + northEast: NMGLatLng(lat: 39.0, lng: 132.0) + ) - let southWest = CLLocationCoordinate2D(latitude: 33.0, longitude: 124.0) - let northEast = CLLocationCoordinate2D(latitude: 39.0, longitude: 132.0) - let koreaBounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast) - view.cameraTargetBounds = koreaBounds + view.minZoomLevel = 7.5 + view.maxZoomLevel = 20 return view }() @@ -52,8 +54,7 @@ final class MapView: UIView { }() var storeCard: MapPopupCarouselView = { - let view = MapPopupCarouselView() - return view + return MapPopupCarouselView() }() // MARK: - Init @@ -87,28 +88,24 @@ final class MapView: UIView { // MARK: - SetUp private extension MapView { func setUpConstraints() { - // 1. MapView 설정 addSubview(mapView) mapView.snp.makeConstraints { make in make.edges.equalToSuperview() } - // 2. Search Filter Container 설정 addSubview(searchFilterContainer) searchFilterContainer.snp.makeConstraints { make in make.top.equalToSuperview().offset(56) make.leading.trailing.equalToSuperview() } - // 3. Search Input 설정 searchFilterContainer.addSubview(searchInput) searchInput.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.trailing.equalToSuperview().inset(20) + make.top.equalTo(safeAreaLayoutGuide).inset(12) + make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(37) } - // 4. Filter Chips 설정 searchFilterContainer.addSubview(filterChips) filterChips.snp.makeConstraints { make in make.top.equalTo(searchInput.snp.bottom).offset(7) @@ -117,19 +114,16 @@ private extension MapView { make.bottom.equalToSuperview() } - // 5. Store Card 설정 addSubview(storeCard) storeCard.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(30) make.height.equalTo(137) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-24) // 수정된 부분 + make.bottom.equalTo(safeAreaLayoutGuide).offset(-24) } - // 6. Buttons 설정 addSubview(locationButton) addSubview(listButton) - // 초기 버튼 레이아웃은 updateButtonLayout()에서 설정됨 } func configureUI() { diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift new file mode 100644 index 00000000..5160514e --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MapViewController.swift @@ -0,0 +1,1609 @@ +import CoreLocation +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure +import SearchFeatureInterface + +import FloatingPanel +import NMapsMap +import ReactorKit +import RxCocoa +import RxGesture +import RxSwift +import SnapKit + +class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NMFMapViewTouchDelegate, NMFMapViewCameraDelegate, UIGestureRecognizerDelegate { + typealias Reactor = MapReactor + + fileprivate struct CoordinateKey: Hashable { + let lat: Int + let lng: Int + + init(latitude: Double, longitude: Double) { + self.lat = Int(latitude * 1_000_00) + self.lng = Int(longitude * 1_000_00) + } + } + + var currentTooltipView: UIView? + var currentTooltipStores: [MapPopUpStore] = [] + var currentTooltipCoordinate: NMGLatLng? + + // MARK: - Properties + private var storeDetailsCache: [Int64: StoreItem] = [:] + var isMovingToMarker = false + var currentCarouselStores: [MapPopUpStore] = [] + private var markerDictionary: [Int64: NMFMarker] = [:] + private var individualMarkerDictionary: [Int64: NMFMarker] = [:] + private var clusterMarkerDictionary: [String: NMFMarker] = [:] + @Dependency private var popUpAPIUseCase: PopUpAPIUseCase + private let clusteringManager = ClusteringManager() + var currentStores: [MapPopUpStore] = [] + var disposeBag = DisposeBag() + let mainView = MapView() + let carouselView = MapPopupCarouselView() + private let locationManager = CLLocationManager() + var currentMarker: NMFMarker? + private let storeListReactor = StoreListReactor( + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self) + ) + private let storeListViewController = StoreListViewController( + reactor: StoreListReactor( + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self) + ) + ) + private var listViewTopConstraint: Constraint? + private var currentFilterBottomSheet: FilterBottomSheetViewController? + private var filterChipsTopY: CGFloat = 0 + private var filterContainerBottomY: CGFloat { + let frameInView = mainView.filterChips.convert(mainView.filterChips.bounds, to: view) + return frameInView.maxY + } + + enum ModalState { + case top + case middle + case bottom + } + + private var modalState: ModalState = .bottom + private let idleSubject = PublishSubject() + private let cameraIdle = PublishSubject() + + // MARK: - Lifecycle + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + DispatchQueue.main.async { + self.view.layoutIfNeeded() + let frameInView = self.mainView.filterChips.convert(self.mainView.filterChips.bounds, to: self.view) + self.filterChipsTopY = frameInView.minY + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.tabBarController?.tabBar.isHidden = false + } + + override func viewDidLoad() { + super.viewDidLoad() + setUp() + mainView.mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) + + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + locationManager.desiredAccuracy = kCLLocationAccuracyBest + mainView.mapView.positionMode = .compass + checkLocationAuthorization() + + if let reactor = self.reactor { + reactor.action.onNext(.fetchCategories) + let koreaRegion = ( + northEast: NMGLatLng(lat: 38.0, lng: 132.0), + southWest: NMGLatLng(lat: 33.0, lng: 124.0) + ) + + reactor.action.onNext(.viewportChanged( + northEastLat: koreaRegion.northEast.lat, + northEastLon: koreaRegion.northEast.lng, + southWestLat: koreaRegion.southWest.lat, + southWestLon: koreaRegion.southWest.lng + )) + } + setupMapViewRxObservables() + + carouselView.rx.observe(Bool.self, "hidden") + .distinctUntilChanged() + .subscribe(onNext: { [weak self] isHidden in + guard let self = self, let isHidden = isHidden else { return } + self.mainView.setStoreCardHidden(isHidden, animated: true) + }) + .disposed(by: disposeBag) + + carouselView.onCardTapped = { [weak self] store in + let detailController = DetailController() + detailController.reactor = DetailReactor( + popUpID: Int64(store.id), + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: self?.popUpAPIUseCase ?? DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) + + self?.navigationController?.isNavigationBarHidden = false + self?.navigationController?.tabBarController?.tabBar.isHidden = false + + self?.navigationController?.pushViewController(detailController, animated: true) + } + + carouselView.onCardScrolled = { [weak self] pageIndex in + guard let self = self, + pageIndex >= 0, + pageIndex < self.currentCarouselStores.count else { return } + + let store = self.currentCarouselStores[pageIndex] + if let previousMarker = self.currentMarker { + self.updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false, count: 1) + } + + let markerToFocus = self.findMarkerForStore(for: store) + + if let markerToFocus = markerToFocus { + self.updateMarkerStyle(marker: markerToFocus, selected: true, isCluster: false, count: 1) + self.currentMarker = markerToFocus + let userData = markerToFocus.userInfo["storeData"] as? [MapPopUpStore] + if let storeArray = userData, storeArray.count > 1 { + if self.currentTooltipView == nil || + self.currentTooltipCoordinate?.lat != markerToFocus.position.lat || + self.currentTooltipCoordinate?.lng != markerToFocus.position.lng { + self.configureTooltip(for: markerToFocus, stores: storeArray) + } + + if let tooltipIndex = storeArray.firstIndex(where: { $0.id == store.id }) { + (self.currentTooltipView as? MarkerTooltipView)?.selectStore(at: tooltipIndex) + } + } else { + self.currentTooltipView?.removeFromSuperview() + self.currentTooltipView = nil + } + } + } + + if let reactor = self.reactor { + bindViewport(reactor: reactor) + reactor.action.onNext(.fetchCategories) + } + } + + private func setupMapViewRxObservables() { + mainView.mapView.addCameraDelegate(delegate: self) + cameraIdle + .debounce(.milliseconds(300), scheduler: MainScheduler.instance) + .map { [unowned self] in + let bounds = self.getVisibleBounds() + return MapReactor.Action.viewportChanged( + northEastLat: bounds.northEast.lat, + northEastLon: bounds.northEast.lng, + southWestLat: bounds.southWest.lat, + southWestLon: bounds.southWest.lng + ) + } + .bind(to: reactor!.action) + .disposed(by: disposeBag) + + idleSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + guard let self = self else { return } + if let marker = self.currentMarker, + let storeArray = marker.userInfo["storeData"] as? [MapPopUpStore], + storeArray.count > 1 { + if self.currentTooltipView == nil { + self.configureTooltip(for: marker, stores: storeArray) + } else { + self.updateTooltipPosition() + } + } + self.isMovingToMarker = false + }) + .disposed(by: disposeBag) + } + + private func configureTooltip(for marker: NMFMarker, stores: [MapPopUpStore]) { + self.currentTooltipView?.removeFromSuperview() + + let tooltipView = MarkerTooltipView() + tooltipView.configure(with: stores) + + tooltipView.selectStore(at: 0) + + tooltipView.onStoreSelected = { [weak self] index in + guard let self = self, index < stores.count else { return } + self.currentCarouselStores = stores + self.carouselView.updateCards(stores) + self.carouselView.scrollToCard(index: index) + + self.updateMarkerStyle(marker: marker, selected: true, isCluster: false, count: stores.count) + tooltipView.selectStore(at: index) + } + + let markerPoint = self.mainView.mapView.projection.point(from: marker.position) + let markerHeight: CGFloat = 32 + + tooltipView.frame = CGRect( + x: markerPoint.x, + y: markerPoint.y - markerHeight - tooltipView.frame.height - 14, + width: tooltipView.frame.width, + height: tooltipView.frame.height + ) + + self.mainView.addSubview(tooltipView) + self.currentTooltipView = tooltipView + self.currentTooltipStores = stores + self.currentTooltipCoordinate = marker.position + } + + // MARK: - Setup + private func setUp() { + view.addSubview(mainView) + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + view.addSubview(carouselView) + carouselView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(140) + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24) + } + carouselView.isHidden = true + mainView.mapView.touchDelegate = self + + addChild(storeListViewController) + view.addSubview(storeListViewController.view) + storeListViewController.didMove(toParent: self) + + storeListViewController.view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + listViewTopConstraint = make.top.equalToSuperview().offset(view.frame.height).constraint + } + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + storeListViewController.mainView.grabberHandle.addGestureRecognizer(panGesture) + storeListViewController.mainView.addGestureRecognizer(panGesture) + setupPanAndSwipeGestures() + + let mapViewTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMapViewTap(_:))) + mapViewTapGesture.delaysTouchesBegan = false + mainView.mapView.addGestureRecognizer(mapViewTapGesture) + mapViewTapGesture.delegate = self + } + + private let defaultZoomLevel: Double = 15.0 + private func setupPanAndSwipeGestures() { + storeListViewController.mainView.grabberHandle.rx.swipeGesture(.up) + .skip(1) + .withUnretained(self) + .subscribe { owner, _ in + Logger.log("⬆️ 위로 스와이프 감지", category: .debug) + switch owner.modalState { + case .bottom: + owner.animateToState(.middle) + case .middle: + owner.animateToState(.top) + case .top: + break + } + } + .disposed(by: disposeBag) + + storeListViewController.mainView.grabberHandle.rx.swipeGesture(.down) + .skip(1) + .withUnretained(self) + .subscribe { owner, _ in + Logger.log("⬇️ 아래로 스와이프 감지됨", category: .debug) + switch owner.modalState { + case .top: + owner.animateToState(.middle) + case .middle: + owner.animateToState(.bottom) + case .bottom: + break + } + } + .disposed(by: disposeBag) + } + + // MARK: - Bind + func bind(reactor: Reactor) { + mainView.filterChips.locationChip.rx.tap + .map { Reactor.Action.filterTapped(.location) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.filterChips.categoryChip.rx.tap + .map { Reactor.Action.filterTapped(.category) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.listButton.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + owner.animateToState(.middle) + } + .disposed(by: disposeBag) + + // 위치 버튼 + mainView.locationButton.rx.tap + .bind { [weak self] _ in + guard let self = self, + let location = self.locationManager.location else { return } + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( + lat: location.coordinate.latitude, + lng: location.coordinate.longitude + ), zoomTo: 15.0) + + self.mainView.mapView.moveCamera(cameraUpdate) + } + .disposed(by: disposeBag) + + mainView.filterChips.onRemoveLocation = { [weak self] in + guard let self = self else { return } + self.reactor?.action.onNext(.clearFilters(.location)) + let bounds = self.getVisibleBounds() + self.reactor?.action.onNext(.viewportChanged( + northEastLat: bounds.northEast.lat, + northEastLon: bounds.northEast.lng, + southWestLat: bounds.southWest.lat, + southWestLon: bounds.southWest.lng + )) + + self.clearAllMarkers() + self.clusterMarkerDictionary.values.forEach { $0.mapView = nil } + self.clusterMarkerDictionary.removeAll() + + self.carouselView.isHidden = true + self.carouselView.updateCards([]) + self.currentCarouselStores = [] + self.mainView.setStoreCardHidden(true, animated: true) + + self.updateMapWithClustering() + } + + mainView.filterChips.onRemoveCategory = { [weak self] in + guard let self = self else { return } + self.reactor?.action.onNext(.clearFilters(.category)) + let bounds = self.getVisibleBounds() + self.reactor?.action.onNext(.viewportChanged( + northEastLat: bounds.northEast.lat, + northEastLon: bounds.northEast.lng, + southWestLat: bounds.southWest.lat, + southWestLon: bounds.southWest.lng + )) + + self.resetSelectedMarker() + self.carouselView.isHidden = true + self.carouselView.updateCards([]) + self.currentCarouselStores = [] + self.mainView.setStoreCardHidden(true, animated: true) + } + + Observable.combineLatest( + reactor.state.map { $0.selectedLocationFilters }.distinctUntilChanged(), + reactor.state.map { $0.selectedCategoryFilters }.distinctUntilChanged() + ) { locationFilters, categoryFilters -> (String, String) in + let locationText: String + if locationFilters.isEmpty { + locationText = "지역선택" + } else if locationFilters.count > 1 { + locationText = "\(locationFilters[0]) 외 \(locationFilters.count - 1)개" + } else { + locationText = locationFilters[0] + } + let categoryText: String + if categoryFilters.isEmpty { + categoryText = "카테고리" + } else if categoryFilters.count > 1 { + categoryText = "\(categoryFilters[0]) 외 \(categoryFilters.count - 1)개" + } else { + categoryText = categoryFilters[0] + } + return (locationText, categoryText) + } + .observe(on: MainScheduler.instance) + .bind { [weak self] locationText, categoryText in + self?.mainView.filterChips.update( + locationText: locationText, + categoryText: categoryText + ) + } + .disposed(by: disposeBag) + + reactor.state.map { $0.activeFilterType } + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] filterType in + guard let self = self else { return } + if let filterType = filterType { + self.presentFilterBottomSheet(for: filterType) + } else { + self.dismissFilterBottomSheet() + } + }) + .disposed(by: disposeBag) + + reactor.state.map { $0.searchResult } + .distinctUntilChanged() + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .bind { [weak self] store in + guard let self = self else { return } + + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( + lat: store.latitude, + lng: store.longitude + ), zoomTo: 15.0) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + self.mainView.mapView.moveCamera(cameraUpdate) + + self.addMarker(for: store) + } + .disposed(by: disposeBag) + + mainView.searchInput.rx.tapGesture() + .when(.recognized) + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, _ in + @Dependency var factory: PopupSearchFactory + owner.navigationController?.pushViewController(factory.make(), animated: true) + }) + .disposed(by: disposeBag) + + reactor.state.map { $0.searchResults } + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .bind { [weak self] results in + guard let self = self else { return } + + self.clearAllMarkers() + self.storeListViewController.reactor?.action.onNext(.setStores([])) + self.carouselView.updateCards([]) + self.carouselView.isHidden = true + self.resetSelectedMarker() // 추가된 부분 + + if results.isEmpty { + self.mainView.setStoreCardHidden(true, animated: true) + return + } else { + self.mainView.setStoreCardHidden(false, animated: true) + } + self.addMarkers(for: results) + let storeItems = results.map { store in + StoreItem( + id: store.id, + thumbnailURL: store.mainImageUrl ?? "", + category: store.category, + title: store.name, + location: store.address, + dateRange: "\(store.startDate) ~ \(store.endDate)", + isBookmarked: false + ) + } + self.storeListViewController.reactor?.action.onNext(.setStores(storeItems)) + self.carouselView.updateCards(results) + self.carouselView.isHidden = false + self.currentCarouselStores = results + if let firstStore = results.first { + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng( + lat: firstStore.latitude, + lng: firstStore.longitude + ), zoomTo: 15.0) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + self.mainView.mapView.moveCamera(cameraUpdate) + } + } + .disposed(by: disposeBag) + } + + // MARK: - List View Control + private func toggleListView() { + UIView.animate(withDuration: 0.3) { + let middleOffset = -self.view.frame.height * 0.7 + self.listViewTopConstraint?.update(offset: middleOffset) + self.modalState = .middle + self.mainView.searchFilterContainer.backgroundColor = .clear + self.view.layoutIfNeeded() + } + } + + // 마커 추가 메서드 (NMFMarker로 변환) + func addMarker(for store: MapPopUpStore) { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + + // 마커 스타일 설정 + updateMarkerStyle(marker: marker, selected: false, isCluster: false, count: 1) + + // 중요: 마커에 직접 터치 핸들러 추가 + marker.touchHandler = { [weak self] (_) -> Bool in + guard let self = self else { return false } + // 단일 스토어 마커 처리 + return self.handleSingleStoreTap(marker, store: store) + } + + marker.mapView = mainView.mapView + markerDictionary[store.id] = marker + } + + func updateMarkerStyle( + marker: NMFMarker, + selected: Bool, + isCluster: Bool, + count: Int = 1, + regionName: String = "" + ) { + let mapMarkerView: MapMarker + if let cachedView = marker.userInfo["mapMarkerView"] as? MapMarker { + mapMarkerView = cachedView + } else { + mapMarkerView = MapMarker() + marker.userInfo["mapMarkerView"] = mapMarkerView + } + + let wasMultiMarker = (mapMarkerView.currentInput?.count ?? 0) > 1 + + let input = MapMarker.Input( + isSelected: selected, + isCluster: isCluster, + regionName: regionName, + count: count, + isMultiMarker: count > 1 && !isCluster + ) + mapMarkerView.injection(with: input) + + if let img = mapMarkerView.asImage() { + marker.iconImage = NMFOverlayImage(image: img) + marker.width = img.size.width + marker.height = img.size.height + marker.anchor = CGPoint(x: 0.5, y: isCluster ? 0.5 : 1.0) + } + } + + @objc private func handleMapViewTap(_ gesture: UITapGestureRecognizer) { + if modalState == .middle || modalState == .top { + animateToState(.bottom) + } + } + + @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + let translation = gesture.translation(in: view) + let velocity = gesture.velocity(in: view) + + switch gesture.state { + case .changed: + if let constraint = listViewTopConstraint { + let currentOffset = constraint.layoutConstraints.first?.constant ?? 0 + let newOffset = currentOffset + translation.y + + let minOffset: CGFloat = filterContainerBottomY + let maxOffset: CGFloat = view.frame.height + let clampedOffset = min(max(newOffset, minOffset), maxOffset) + + constraint.update(offset: clampedOffset) + gesture.setTranslation(.zero, in: view) + + if modalState == .top { + adjustMapViewAlpha(for: clampedOffset, minOffset: minOffset, maxOffset: maxOffset) + } + } + + case .ended: + if let constraint = listViewTopConstraint { + let currentOffset = constraint.layoutConstraints.first?.constant ?? 0 + let middleY = view.frame.height * 0.3 + let targetState: ModalState + + if velocity.y > 500 { + targetState = .bottom + } else if velocity.y < -500 { + targetState = .top + } else if currentOffset < middleY * 0.7 { + targetState = .top + } else if currentOffset < view.frame.height * 0.7 { + targetState = .middle + } else { + targetState = .bottom + } + + animateToState(targetState) + } + + default: + break + } + } + + private func adjustMapViewAlpha(for offset: CGFloat, minOffset: CGFloat, maxOffset: CGFloat) { + let middleOffset = view.frame.height * 0.3 + + if offset <= minOffset { + mainView.mapView.alpha = 0 + } else if offset >= maxOffset { + mainView.mapView.alpha = 1 + } else if offset <= middleOffset { + let progress = (offset - minOffset) / (middleOffset - minOffset) + mainView.mapView.alpha = progress + } else { + mainView.mapView.alpha = 1 + } + } + + private func updateMapViewAlpha(for offset: CGFloat, minOffset: CGFloat, maxOffset: CGFloat) { + let progress = (maxOffset - offset) / (maxOffset - minOffset) + mainView.mapView.alpha = max(0, min(progress, 1)) + } + + private func animateToState(_ state: ModalState) { + guard modalState != state else { return } + self.view.layoutIfNeeded() + + UIView.animate(withDuration: 0.3, animations: { + switch state { + case .top: + let filterChipsFrame = self.mainView.filterChips.convert( + self.mainView.filterChips.bounds, + to: self.view + ) + self.mainView.mapView.alpha = 0 + self.storeListViewController.setGrabberHandleVisible(false) + self.listViewTopConstraint?.update(offset: filterChipsFrame.maxY) + self.mainView.searchInput.setBackgroundColor(.g50) + + case .middle: + self.storeListViewController.setGrabberHandleVisible(true) + let offset = max(self.view.frame.height * 0.3, self.filterContainerBottomY) + self.listViewTopConstraint?.update(offset: offset) + self.storeListViewController.mainView.layer.cornerRadius = 20 + self.storeListViewController.mainView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.mainView.mapView.alpha = 1 + self.mainView.mapView.isHidden = false + self.mainView.searchInput.setBackgroundColor(.white) + + if let reactor = self.reactor { + reactor.action.onNext(.fetchAllStores) + + reactor.state + .map { $0.viewportStores } + .distinctUntilChanged() + .filter { !$0.isEmpty } + .take(1) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] stores in + guard let self = self else { return } + self.fetchStoreDetails(for: stores) + + Logger.log( + "✅ 전체 스토어 목록으로 리스트뷰 업데이트: \(stores.count)개", + category: .debug + ) + }) + .disposed(by: self.disposeBag) + } + + case .bottom: + self.storeListViewController.setGrabberHandleVisible(true) + self.listViewTopConstraint?.update(offset: self.view.frame.height) + self.mainView.mapView.alpha = 1 + self.mainView.mapView.isHidden = false + self.mainView.searchInput.setBackgroundColor(.white) + } + + self.view.layoutIfNeeded() + }) { _ in + self.modalState = state + } + } + + func imageFromView(_ view: UIView) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, UIScreen.main.scale) + defer { UIGraphicsEndImageContext() } + if let context = UIGraphicsGetCurrentContext() { + view.layer.render(in: context) + return UIGraphicsGetImageFromCurrentImageContext() + } + return nil + } + + // MARK: - Helper: 클러스터용 커스텀 마커 이미지 생성 (MapMarker를 사용) + func createClusterMarkerImage(regionName: String, count: Int) -> UIImage? { + let markerView = MapMarker() + let input = MapMarker.Input(isSelected: false, + isCluster: true, + regionName: regionName, + count: count, + isMultiMarker: false) + markerView.injection(with: input) + if markerView.frame == .zero { + markerView.frame = CGRect(x: 0, y: 0, width: 80, height: 32) + } + return imageFromView(markerView) + } + // MARK: - Clustering + private func updateMapWithClustering() { + let currentZoom = mainView.mapView.zoomLevel + let level = MapZoomLevel.getLevel(from: Float(currentZoom)) + CATransaction.begin() + CATransaction.setDisableActions(true) + + switch level { + case .detailed: + let newStoreIds = Set(currentStores.map { $0.id }) + let groupedDict = groupStoresByExactLocation(currentStores) + clusterMarkerDictionary.values.forEach { $0.mapView = nil } + clusterMarkerDictionary.removeAll() + + for (coordinate, storeGroup) in groupedDict { + if storeGroup.count == 1, let store = storeGroup.first { + if let existingMarker = individualMarkerDictionary[store.id] { + if existingMarker.position.lat != store.latitude || + existingMarker.position.lng != store.longitude { + existingMarker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + } + let isSelected = (existingMarker == currentMarker) + updateMarkerStyle(marker: existingMarker, selected: isSelected, isCluster: false) + } else { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + marker.anchor = CGPoint(x: 0.5, y: 1.0) + updateMarkerStyle(marker: marker, selected: false, isCluster: false) + + // 직접 터치 핸들러 추가 + marker.touchHandler = { [weak self] (_) -> Bool in + guard let self = self else { return false } + return self.handleSingleStoreTap(marker, store: store) + } + + marker.mapView = mainView.mapView + individualMarkerDictionary[store.id] = marker + } + } else { + guard let firstStore = storeGroup.first else { continue } + let markerKey = firstStore.id + if let existingMarker = individualMarkerDictionary[markerKey] { + existingMarker.userInfo = ["storeData": storeGroup] + let isSelected = (existingMarker == currentMarker) + updateMarkerStyle(marker: existingMarker, selected: isSelected, isCluster: false, count: storeGroup.count) + } else { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: firstStore.latitude, lng: firstStore.longitude) + marker.userInfo = ["storeData": storeGroup] + marker.anchor = CGPoint(x: 0.5, y: 1.0) + updateMarkerStyle(marker: marker, selected: false, isCluster: false, count: storeGroup.count) + + marker.touchHandler = { [weak self] (_) -> Bool in + guard let self = self else { return false } + return self.handleMicroClusterTap(marker, storeArray: storeGroup) + } + + marker.mapView = mainView.mapView + individualMarkerDictionary[markerKey] = marker + } + } + } + + individualMarkerDictionary = individualMarkerDictionary.filter { id, marker in + if newStoreIds.contains(id) { + return true + } else { + marker.mapView = nil + return false + } + } + + case .district, .city, .country: + individualMarkerDictionary.values.forEach { $0.mapView = nil } + individualMarkerDictionary.removeAll() + + let clusters = clusteringManager.clusterStores(currentStores, at: Float(currentZoom)) + let activeClusterKeys = Set(clusters.map { $0.cluster.name }) + + for cluster in clusters { + let clusterKey = cluster.cluster.name + var marker: NMFMarker + if let existingMarker = clusterMarkerDictionary[clusterKey] { + marker = existingMarker + if marker.position.lat != cluster.cluster.coordinate.lat || + marker.position.lng != cluster.cluster.coordinate.lng { + marker.position = NMGLatLng(lat: cluster.cluster.coordinate.lat, lng: cluster.cluster.coordinate.lng) + } + } else { + marker = NMFMarker() + clusterMarkerDictionary[clusterKey] = marker + } + + marker.position = NMGLatLng(lat: cluster.cluster.coordinate.lat, lng: cluster.cluster.coordinate.lng) + marker.userInfo = ["clusterData": cluster] + + if let clusterImage = createClusterMarkerImage(regionName: cluster.cluster.name, count: cluster.storeCount) { + marker.iconImage = NMFOverlayImage(image: clusterImage) + } else { + marker.iconImage = NMFOverlayImage(name: "cluster_marker") + } + + marker.touchHandler = { [weak self] (overlay) -> Bool in + guard let self = self, + let tappedMarker = overlay as? NMFMarker, + let clusterData = tappedMarker.userInfo["clusterData"] as? ClusterMarkerData else { + return false + } + + return self.handleRegionalClusterTap(tappedMarker, clusterData: clusterData) + } + + marker.captionText = "" + marker.anchor = CGPoint(x: 0.5, y: 0.5) + marker.mapView = mainView.mapView + } + + for (key, marker) in clusterMarkerDictionary { + if !activeClusterKeys.contains(key) { + marker.mapView = nil + clusterMarkerDictionary.removeValue(forKey: key) + } + } + } + + CATransaction.commit() + } + + private func clearAllMarkers() { + individualMarkerDictionary.values.forEach { $0.mapView = nil } + individualMarkerDictionary.removeAll() + + clusterMarkerDictionary.values.forEach { $0.mapView = nil } + clusterMarkerDictionary.removeAll() + + markerDictionary.values.forEach { $0.mapView = nil } + markerDictionary.removeAll() + } + + private func groupStoresByExactLocation(_ stores: [MapPopUpStore]) -> [CoordinateKey: [MapPopUpStore]] { + var dict = [CoordinateKey: [MapPopUpStore]]() + for store in stores { + let key = CoordinateKey(latitude: store.latitude, longitude: store.longitude) + dict[key, default: []].append(store) + } + return dict + } + + private func updateIndividualMarkers(_ stores: [MapPopUpStore]) { + var newMarkerIDs = Set() + + for store in stores { + newMarkerIDs.insert(store.id) + if let marker = individualMarkerDictionary[store.id] { + if marker.position.lat != store.latitude || marker.position.lng != store.longitude { + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + } + } else { + // 새 마커 생성 및 추가 + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + + updateMarkerStyle(marker: marker, selected: false, isCluster: false) + marker.mapView = mainView.mapView + + individualMarkerDictionary[store.id] = marker + } + } + for (id, marker) in individualMarkerDictionary { + if !newMarkerIDs.contains(id) { + marker.mapView = nil + individualMarkerDictionary.removeValue(forKey: id) + } + } + } + + private func updateClusterMarkers(_ clusters: [ClusterMarkerData]) { + for clusterData in clusters { + let clusterKey = clusterData.cluster.name + let fixedCoordinate = clusterData.cluster.coordinate + + if let marker = clusterMarkerDictionary[clusterKey] { + if marker.position.lat != fixedCoordinate.lat || marker.position.lng != fixedCoordinate.lng { + marker.position = NMGLatLng(lat: fixedCoordinate.lat, lng: fixedCoordinate.lng) + } + } else { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: fixedCoordinate.lat, lng: fixedCoordinate.lng) + marker.userInfo = ["clusterData": clusterData] + + updateMarkerStyle(marker: marker, selected: false, isCluster: true, + count: clusterData.storeCount, regionName: clusterData.cluster.name) + marker.mapView = mainView.mapView + + clusterMarkerDictionary[clusterKey] = marker + } + } + } + + func presentFilterBottomSheet(for filterType: FilterType) { + guard let reactor = self.reactor else { return } + + let sheetReactor = FilterBottomSheetReactor( + savedSubRegions: reactor.currentState.selectedLocationFilters, + savedCategories: reactor.currentState.selectedCategoryFilters + ) + let viewController = FilterBottomSheetViewController(reactor: sheetReactor) + + let initialIndex = (filterType == .location) ? 0 : 1 + viewController.containerView.segmentedControl.selectedSegmentIndex = initialIndex + sheetReactor.action.onNext(.segmentChanged(initialIndex)) + + viewController.onSave = { [weak self] filterData in + guard let self = self else { return } + self.reactor?.action.onNext(.updateBothFilters( + locations: filterData.locations, + categories: filterData.categories + )) + self.reactor?.action.onNext(.filterTapped(nil)) + + let bounds = self.getVisibleBounds() + self.reactor?.action.onNext(.viewportChanged( + northEastLat: bounds.northEast.lat, + northEastLon: bounds.northEast.lng, + southWestLat: bounds.southWest.lat, + southWestLon: bounds.southWest.lng + )) + } + + viewController.onDismiss = { [weak self] in + self?.reactor?.action.onNext(.filterTapped(nil)) + } + + viewController.modalPresentationStyle = .overFullScreen + present(viewController, animated: false) { + viewController.showBottomSheet() + } + + currentFilterBottomSheet = viewController + } + + private func dismissFilterBottomSheet() { + if let bottomSheet = currentFilterBottomSheet { + bottomSheet.hideBottomSheet() + } + currentFilterBottomSheet = nil + } + + private func addMarkers(for stores: [MapPopUpStore]) { + markerDictionary.values.forEach { $0.mapView = nil } + markerDictionary.removeAll() + + for store in stores { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + + updateMarkerStyle(marker: marker, selected: false, isCluster: false) + + marker.touchHandler = { [weak self] (_) -> Bool in + guard let self = self else { return false } + + print("검색 결과 마커 터치됨! 스토어: \(store.name)") + return self.handleSingleStoreTap(marker, store: store) + } + + marker.mapView = mainView.mapView + markerDictionary[store.id] = marker + } + } + private func updateListView(with results: [MapPopUpStore]) { + let storeItems = results.map { store in + StoreItem( + id: store.id, + thumbnailURL: store.mainImageUrl ?? "", + category: store.category, + title: store.name, + location: store.address, + dateRange: "\(store.startDate) ~ \(store.endDate)", + isBookmarked: false + ) + } + storeListViewController.reactor?.action.onNext(.setStores(storeItems)) + } + + private func showAlert(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "확인", style: .default, handler: nil)) + present(alert, animated: true, completion: nil) + } + + private func getEffectiveViewport() -> NMGLatLngBounds { + let bounds = getVisibleBounds() + + if carouselView.isHidden { + return NMGLatLngBounds(southWest: bounds.southWest, northEast: bounds.northEast) + } + + let carouselTopY = carouselView.frame.minY + let leftPoint = CGPoint(x: 0, y: carouselTopY) + let rightPoint = CGPoint(x: view.frame.width, y: carouselTopY) + + let leftCoordinate = mainView.mapView.projection.latlng(from: leftPoint) + let rightCoordinate = mainView.mapView.projection.latlng(from: rightPoint) + + let adjustedSouthWest = NMGLatLng( + lat: max(leftCoordinate.lat, rightCoordinate.lat), + lng: bounds.southWest.lng + ) + + return NMGLatLngBounds( + southWest: adjustedSouthWest, + northEast: bounds.northEast + ) + } + private func getVisibleBounds() -> (northEast: NMGLatLng, southWest: NMGLatLng) { + let mapBounds = mainView.mapView.contentBounds + + let northEast = NMGLatLng(lat: mapBounds.northEastLat, lng: mapBounds.northEastLng) + let southWest = NMGLatLng(lat: mapBounds.southWestLat, lng: mapBounds.southWestLng) + + return (northEast: northEast, southWest: southWest) + } + + // MARK: - Location + private func checkLocationAuthorization() { + switch locationManager.authorizationStatus { + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + case .authorizedWhenInUse, .authorizedAlways: + locationManager.startUpdatingLocation() + mainView.mapView.positionMode = .direction // 내 위치 트래킹 모드 활성화 + case .denied, .restricted: + Logger.log( + "위치 서비스가 비활성화되었습니다. 설정에서 권한을 확인해주세요.", + category: .error + ) + mainView.mapView.positionMode = .disabled + @unknown default: + break + } + } + + private func updateTooltipPosition() { + guard let marker = currentMarker, let tooltip = currentTooltipView else { return } + let markerPoint = mainView.mapView.projection.point(from: marker.position) + var markerCenter = markerPoint + + markerCenter.y = markerPoint.y - 20 + + let offsetX: CGFloat = -10 + let offsetY: CGFloat = -10 + + tooltip.frame.origin = CGPoint( + x: markerCenter.x + offsetX, + y: markerCenter.y - tooltip.frame.height - offsetY + ) + } + + private func resetSelectedMarker() { + if let currentMarker = currentMarker { + updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false) + } + + // 툴팁 제거 + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + currentTooltipStores = [] + currentTooltipCoordinate = nil + carouselView.isHidden = true + carouselView.updateCards([]) + currentCarouselStores = [] + + // 현재 마커 참조 제거 + self.currentMarker = nil + } + + private func updateMarkersForCluster(stores: [MapPopUpStore]) { + // 전체 개별 및 클러스터 마커 제거 + for marker in individualMarkerDictionary.values { + marker.mapView = nil + } + individualMarkerDictionary.removeAll() + + for marker in clusterMarkerDictionary.values { + marker.mapView = nil + } + clusterMarkerDictionary.removeAll() + + // 클러스터에 포함된 스토어들만 새 마커 추가 + for store in stores { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + marker.anchor = CGPoint(x: 0.5, y: 1.0) + + updateMarkerStyle(marker: marker, selected: false, isCluster: false) + + // 직접 터치 핸들러 추가 + marker.touchHandler = { [weak self] (_) -> Bool in + guard let self = self else { return false } + + print("클러스터 내 마커 터치됨! 스토어: \(store.name)") + return self.handleSingleStoreTap(marker, store: store) + } + + marker.mapView = mainView.mapView + individualMarkerDictionary[store.id] = marker + } + } + + private func findMarkerForStore(for store: MapPopUpStore) -> NMFMarker? { + for marker in individualMarkerDictionary.values { + if let singleStore = marker.userInfo["storeData"] as? MapPopUpStore, singleStore.id == store.id { + return marker + } + if let storeGroup = marker.userInfo["storeData"] as? [MapPopUpStore], + storeGroup.contains(where: { $0.id == store.id }) { + return marker + } + } + for marker in clusterMarkerDictionary.values { + if let clusterData = marker.userInfo["clusterData"] as? ClusterMarkerData, + clusterData.cluster.stores.contains(where: { $0.id == store.id }) { + return marker + } + } + return nil + } + + private func fetchStoreDetails(for stores: [MapPopUpStore]) { + guard !stores.isEmpty else { return } + let initialStoreItems = stores.map { store in + StoreItem( + id: store.id, + thumbnailURL: store.mainImageUrl ?? "", + category: store.category, + title: store.name, + location: store.address, + dateRange: "\(store.startDate ?? "") ~ \(store.endDate ?? "")", + isBookmarked: false + ) + } + self.storeListViewController.reactor?.action.onNext(.setStores(initialStoreItems)) + stores.forEach { store in + self.popUpAPIUseCase.getPopUpDetail( + commentType: "NORMAL", + popUpStoredId: store.id, + isViewCount: true + ) + .asObservable() + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] detail in + self?.storeListViewController.reactor?.action.onNext(.updateStoreBookmark( + id: store.id, + isBookmarked: detail.bookmarkYn + )) + }) + .disposed(by: disposeBag) + } + } + + func bindViewport(reactor: MapReactor) { + let cameraObservable = PublishSubject() + + cameraObservable + .throttle(.milliseconds(200), scheduler: MainScheduler.instance) + .map { [unowned self] _ -> MapReactor.Action in + let bounds = self.getVisibleBounds() + return .viewportChanged( + northEastLat: bounds.northEast.lat, + northEastLon: bounds.northEast.lng, + southWestLat: bounds.southWest.lat, + southWestLon: bounds.southWest.lng + ) + } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .map { $0.viewportStores } + .distinctUntilChanged() + .filter { !$0.isEmpty } + .take(1) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] stores in + guard let self = self else { return } + + if let location = self.locationManager.location { + self.findAndShowNearestStore(from: location) + } else if let firstStore = stores.first, + let marker = self.findMarkerForStore(for: firstStore) { + _ = self.handleSingleStoreTap(marker, store: firstStore) + } + + self.currentStores = stores + self.updateMapWithClustering() + }) + .disposed(by: disposeBag) + + reactor.state + .map { $0.viewportStores } + .distinctUntilChanged() + .throttle(.milliseconds(200), scheduler: MainScheduler.instance) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] stores in + guard let self = self else { return } + + let effectiveViewport = self.getEffectiveViewport() + let bounds = self.getVisibleBounds() + + let visibleStores = stores.filter { store in + let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude) + return NMGLatLngBounds(southWest: bounds.southWest, northEast: bounds.northEast).contains(storePosition) + } + self.currentStores = visibleStores + + let currentZoom = self.mainView.mapView.zoomLevel + let level = MapZoomLevel.getLevel(from: Float(currentZoom)) + + if level == .detailed && !visibleStores.isEmpty { + let effectiveStores = visibleStores.filter { store in + let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude) + return effectiveViewport.contains(storePosition) + } + + self.currentCarouselStores = visibleStores + self.carouselView.updateCards(visibleStores) + self.carouselView.isHidden = false + self.mainView.setStoreCardHidden(false, animated: true) + + if let currentMarker = self.currentMarker { + if let currentStore = currentMarker.userInfo["storeData"] as? MapPopUpStore, + let index = visibleStores.firstIndex(where: { $0.id == currentStore.id }) { + self.carouselView.scrollToCard(index: index) + } else if let storeArray = currentMarker.userInfo["storeData"] as? [MapPopUpStore], + let firstStore = storeArray.first, + let index = visibleStores.firstIndex(where: { $0.id == firstStore.id }) { + self.carouselView.scrollToCard(index: index) + } else { + // 선택된 마커가 현재 뷰포트에 없는 경우 + self.updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false) + self.currentMarker = nil + + // 첫 번째 스토어의 마커를 선택 상태로 설정 + if let firstStore = visibleStores.first, + let marker = self.findMarkerForStore(for: firstStore) { + self.updateMarkerStyle(marker: marker, selected: true, isCluster: false) + self.currentMarker = marker + } + + self.carouselView.scrollToCard(index: 0) + } + } else { + if let firstStore = visibleStores.first, + let marker = self.findMarkerForStore(for: firstStore) { + self.updateMarkerStyle(marker: marker, selected: true, isCluster: false) + self.currentMarker = marker + } + self.carouselView.scrollToCard(index: 0) + } + } else { + // 클러스터 레벨이거나 마커가 없는 경우 + self.carouselView.isHidden = true + self.carouselView.updateCards([]) + self.currentCarouselStores = [] + self.mainView.setStoreCardHidden(true, animated: true) + + if level == .detailed && visibleStores.isEmpty { + // 개별 마커 레벨인데 마커가 없는 경우 토스트 표시 + self.showNoMarkersToast() + } + } + + self.updateMapWithClustering() + }) + .disposed(by: disposeBag) + } + + private func findAndShowNearestStore(from location: CLLocation) { + guard !currentStores.isEmpty else { + Logger.log("현재위치 표기할 스토어가 없습니다", category: .debug) + return + } + + resetSelectedMarker() + + let nearestStore = currentStores.min { store1, store2 in + let location1 = CLLocation(latitude: store1.latitude, longitude: store1.longitude) + let location2 = CLLocation(latitude: store2.latitude, longitude: store2.longitude) + return location.distance(from: location1) < location.distance(from: location2) + } + + if let store = nearestStore, let marker = findMarkerForStore(for: store) { + _ = handleSingleStoreTap(marker, store: store) + } else if let store = nearestStore { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: store.latitude, lng: store.longitude) + marker.userInfo = ["storeData": store] + marker.anchor = CGPoint(x: 0.5, y: 1.0) + + updateMarkerStyle(marker: marker, selected: true, isCluster: false) + marker.mapView = mainView.mapView + + individualMarkerDictionary[store.id] = marker + currentMarker = marker + carouselView.updateCards([store]) + currentCarouselStores = [store] + carouselView.scrollToCard(index: 0) + mainView.setStoreCardHidden(false, animated: true) + } + } + + // MARK: - Marker Handling + func handleSingleStoreTap(_ marker: NMFMarker, store: MapPopUpStore) -> Bool { + isMovingToMarker = true + + if let previousMarker = currentMarker { + updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false) + } + updateMarkerStyle(marker: marker, selected: true, isCluster: false) + currentMarker = marker + if currentCarouselStores.isEmpty || !currentCarouselStores.contains(where: { $0.id == store.id }) { + let bounds = getVisibleBounds() + + let visibleStores = currentStores.filter { store in + let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude) + return NMGLatLngBounds(southWest: bounds.southWest, northEast: bounds.northEast).contains(storePosition) + } + + if !visibleStores.isEmpty { + currentCarouselStores = visibleStores + carouselView.updateCards(visibleStores) + + if let index = visibleStores.firstIndex(where: { $0.id == store.id }) { + carouselView.scrollToCard(index: index) + } + } else { + currentCarouselStores = [store] + carouselView.updateCards([store]) + } + } else { + if let index = currentCarouselStores.firstIndex(where: { $0.id == store.id }) { + carouselView.scrollToCard(index: index) + } + } + + carouselView.isHidden = false + mainView.setStoreCardHidden(false, animated: true) + + if let storeArray = marker.userInfo["storeData"] as? [MapPopUpStore], storeArray.count > 1 { + configureTooltip(for: marker, stores: storeArray) + if let index = storeArray.firstIndex(where: { $0.id == store.id }) { + (currentTooltipView as? MarkerTooltipView)?.selectStore(at: index) + } + } else { + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + } + + isMovingToMarker = false + return true + } + + func handleRegionalClusterTap(_ marker: NMFMarker, clusterData: ClusterMarkerData) -> Bool { + + let currentZoom = mainView.mapView.zoomLevel + let currentLevel = MapZoomLevel.getLevel(from: Float(currentZoom)) + switch currentLevel { + case .city: + let districtZoomLevel: Double = 10.0 + let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position, zoomTo: districtZoomLevel) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + mainView.mapView.moveCamera(cameraUpdate) + + case .district: + let detailedZoomLevel: Double = 12.0 + let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position, zoomTo: detailedZoomLevel) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + mainView.mapView.moveCamera(cameraUpdate) + default: + print("기타") + } + updateMarkersForCluster(stores: clusterData.cluster.stores) + carouselView.updateCards(clusterData.cluster.stores) + carouselView.isHidden = false + self.currentCarouselStores = clusterData.cluster.stores + return true + } + + func handleMicroClusterTap(_ marker: NMFMarker, storeArray: [MapPopUpStore]) -> Bool { + if currentMarker == marker { + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + currentTooltipStores = [] + currentTooltipCoordinate = nil + + carouselView.isHidden = true + carouselView.updateCards([]) + currentCarouselStores = [] + updateMarkerStyle(marker: marker, selected: false, isCluster: false, count: storeArray.count) + + currentMarker = nil + isMovingToMarker = false + return false + } + + isMovingToMarker = true + + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + + if let previousMarker = currentMarker { + updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false) + } + + updateMarkerStyle(marker: marker, selected: true, isCluster: false, count: storeArray.count) + currentMarker = marker + + currentCarouselStores = storeArray + carouselView.updateCards(storeArray) + carouselView.isHidden = false + carouselView.scrollToCard(index: 0) + + mainView.setStoreCardHidden(false, animated: true) + let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.3 + mainView.mapView.moveCamera(cameraUpdate) + if storeArray.count > 1 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + guard let self = self else { return } + self.configureTooltip(for: marker, stores: storeArray) + self.isMovingToMarker = false + } + } + + return true + } + + private func showNoMarkersToast() { + Logger.log("현재 지도 영역에 표시할 마커가 없습니다", category: .debug) + } + } + + // MARK: - CLLocationManagerDelegate + extension MapViewController { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + + currentMarker?.mapView = nil + currentMarker = nil + carouselView.isHidden = true + currentCarouselStores = [] + + let position = NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude) + let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: 15.0) + mainView.mapView.moveCamera(cameraUpdate) { [weak self] _ in + guard let self = self else { return } + self.findAndShowNearestStore(from: location) + } + + locationManager.stopUpdatingLocation() + } + } + + // MARK: - NMFMapViewTouchDelegate + extension MapViewController { + func mapView(_ mapView: NMFMapView, didTap marker: NMFMarker) -> Bool { + if let clusterData = marker.userInfo["clusterData"] as? ClusterMarkerData { + return handleRegionalClusterTap(marker, clusterData: clusterData) + } else if let storeArray = marker.userInfo["storeData"] as? [MapPopUpStore] { + if storeArray.count > 1 { + return handleMicroClusterTap(marker, storeArray: storeArray) + } else if let singleStore = storeArray.first { + return handleSingleStoreTap(marker, store: singleStore) + } + } else if let singleStore = marker.userInfo["storeData"] as? MapPopUpStore { + return handleSingleStoreTap(marker, store: singleStore) + } + return false + } + + func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { + guard !isMovingToMarker else { return } + + // 선택된 마커 초기화 + if let currentMarker = currentMarker { + updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false) + self.currentMarker = nil + } + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + currentTooltipStores = [] + currentTooltipCoordinate = nil + carouselView.isHidden = true + carouselView.updateCards([]) + self.currentCarouselStores = [] + mainView.setStoreCardHidden(true, animated: true) + updateMapWithClustering() + } + } + + // MARK: - NMFMapViewCameraDelegate + extension MapViewController { + func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) { + if reason == NMFMapChangedByGesture && !isMovingToMarker { + resetSelectedMarker() + } + } + func mapView(_ mapView: NMFMapView, cameraIsChangingByReason reason: Int) { + if !isMovingToMarker { + currentTooltipView?.removeFromSuperview() + currentTooltipView = nil + currentTooltipStores = [] + updateMapWithClustering() + carouselView.isHidden = true + carouselView.updateCards([]) + currentCarouselStores = [] + } + } + func mapView(_ mapView: NMFMapView, cameraDidChangeByReason reason: Int, animated: Bool) { + if let marker = self.currentMarker, + let storeArray = marker.userInfo["storeData"] as? [MapPopUpStore], + storeArray.count > 1 { + if self.currentTooltipView == nil { + self.configureTooltip(for: marker, stores: storeArray) + } else { + self.updateTooltipPosition() + } + } + self.isMovingToMarker = false + idleSubject.onNext(()) + cameraIdle.onNext(()) + } + } + // MARK: - UIGestureRecognizerDelegate + extension MapViewController { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + let touchPoint = touch.location(in: view) + if modalState != .bottom { + let listViewY = storeListViewController.view.frame.minY + if touchPoint.y > listViewY { + return false + } + } + + return true + } + } +extension NMGLatLngBounds { + func contains(_ point: NMGLatLng) -> Bool { + let southWestLat = self.southWest.lat + let southWestLng = self.southWest.lng + let northEastLat = self.northEast.lat + let northEastLng = self.northEast.lng + + return point.lat >= southWestLat && + point.lat <= northEastLat && + point.lng >= southWestLng && + point.lng <= northEastLng + } +} diff --git a/Poppool/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MarkerTooltipView.swift similarity index 99% rename from Poppool/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MarkerTooltipView.swift index 936b8848..ed8ce855 100644 --- a/Poppool/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/MapView/MarkerTooltipView.swift @@ -1,4 +1,7 @@ import UIKit + +import DomainInterface + import SnapKit final class MarkerTooltipView: UIView, UIGestureRecognizerDelegate { diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListCell.swift similarity index 98% rename from Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListCell.swift index e8e5e003..bdfb71b8 100644 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListCell.swift @@ -1,7 +1,10 @@ import UIKit -import SnapKit -import RxSwift + +import DesignSystem + import ReactorKit +import RxSwift +import SnapKit final class StoreListCell: UICollectionViewCell { static let identifier = "StoreListCell" @@ -48,7 +51,7 @@ final class StoreListCell: UICollectionViewCell { private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12, text: "") label.textColor = .g400 - label.numberOfLines = 2 + label.numberOfLines = 2 return label }() diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListReactor.swift similarity index 91% rename from Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListReactor.swift index 864d2426..d335352d 100644 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListReactor.swift @@ -1,16 +1,18 @@ -import ReactorKit -import RxSwift import Foundation -import RxCocoa +import DomainInterface +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift final class StoreListReactor: Reactor { // MARK: - Reactor - private let userAPIUseCase: UserAPIUseCaseImpl - private let popUpAPIUseCase: PopUpAPIUseCaseImpl + private let userAPIUseCase: UserAPIUseCase + private let popUpAPIUseCase: PopUpAPIUseCase private let bookmarkStateRelay = PublishRelay<(Int64, Bool)>() - // private var currentPage = 0 // private let pageSize = 10 // private var hasMorePages = true @@ -25,7 +27,6 @@ final class StoreListReactor: Reactor { case clearFilters(FilterType) case updateStoreBookmark(id: Int64, isBookmarked: Bool) // 추가 - } enum Mutation { @@ -51,15 +52,14 @@ final class StoreListReactor: Reactor { var initialState: State init( - userAPIUseCase: UserAPIUseCaseImpl = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())), - popUpAPIUseCase: PopUpAPIUseCaseImpl = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) + userAPIUseCase: UserAPIUseCase, + popUpAPIUseCase: PopUpAPIUseCase ) { self.userAPIUseCase = userAPIUseCase self.popUpAPIUseCase = popUpAPIUseCase self.initialState = State() } - // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -80,13 +80,14 @@ final class StoreListReactor: Reactor { // Int64 → Int32 변환 필요 guard let idInt32 = Int32(exactly: store.id) else { - Logger.log(message: "ID 값이 Int32 범위를 초과했습니다: \(store.id)", category: .error) + Logger.log("ID 값이 Int32 범위를 초과했습니다: \(store.id)", category: .error) return .empty() } - let bookmarkRequest = popUpAPIUseCase.getPopUpDetail( + return popUpAPIUseCase.getPopUpDetail( commentType: "NORMAL", - popUpStoredId: Int64(idInt32) // Int32 → Int64 변환 + popUpStoredId: Int64(idInt32), // Int32 → Int64 변환 + isViewCount: true ) .flatMap { detail -> Observable in if detail.bookmarkYn != store.isBookmarked { @@ -102,10 +103,6 @@ final class StoreListReactor: Reactor { ])) } - return bookmarkRequest - - - // // case let .setStores(storeItems): // return Observable.from(storeItems) @@ -134,7 +131,8 @@ final class StoreListReactor: Reactor { guard let self = self else { return .empty() } return self.popUpAPIUseCase.getPopUpDetail( commentType: "NORMAL", - popUpStoredId: store.id + popUpStoredId: store.id, + isViewCount: false ) .map { detail in var updatedStore = store @@ -160,7 +158,6 @@ final class StoreListReactor: Reactor { } return .empty() - case let .filterTapped(filterType): return .just(.setActiveFilter(filterType)) @@ -182,7 +179,6 @@ final class StoreListReactor: Reactor { } } - func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -196,7 +192,7 @@ final class StoreListReactor: Reactor { newState.stores[index].isBookmarked = isBookmarked Logger.log( - message: """ + """ 북마크 상태 변경: - 스토어명: \(store.title) - ID: \(store.id) @@ -206,10 +202,9 @@ final class StoreListReactor: Reactor { ) } - case let .showBookmarkToast(isBookmarked): if currentState.stores.isEmpty { - break + break } newState.shouldShowBookmarkToast = isBookmarked @@ -236,11 +231,8 @@ final class StoreListReactor: Reactor { bookmarkStateRelay.accept((storeId, isBookmarked)) } - - } - // MARK: - Model struct StoreItem { let id: Int64 diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListSection.swift similarity index 100% rename from Poppool/Poppool/Presentation/Map/StoreListView/StoreListSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListSection.swift diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListView.swift similarity index 100% rename from Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListView.swift index 63f88bc5..79f01619 100644 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListView.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class StoreListView: UIView { // MARK: - Components diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListViewController.swift similarity index 87% rename from Poppool/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListViewController.swift index e8fb9c69..92dba9d9 100644 --- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Map/StoreListView/StoreListViewController.swift @@ -1,10 +1,14 @@ import UIKit -import SnapKit -import RxCocoa -import RxSwift -import ReactorKit + +import DomainInterface +import Infrastructure + import FloatingPanel +import ReactorKit +import RxCocoa import RxDataSources +import RxSwift +import SnapKit final class StoreListViewController: UIViewController, View { typealias Reactor = StoreListReactor @@ -56,15 +60,16 @@ final class StoreListViewController: UIViewController, View { func bind(reactor: Reactor) { let dataSource = RxCollectionViewSectionedReloadDataSource( - configureCell: { [weak self] ds, cv, indexPath, item in - guard let self = self else { return UICollectionViewCell() } - let cell = cv.dequeueReusableCell( - withReuseIdentifier: StoreListCell.identifier, - for: indexPath - ) as! StoreListCell + configureCell: { [weak self] _, cv, indexPath, item in + guard let self = self, + let cell = cv.dequeueReusableCell( + withReuseIdentifier: StoreListCell.identifier, + for: indexPath + ) as? StoreListCell + else { return UICollectionViewCell() } cell.injection(with: .init( - thumbnailURL: item.thumbnailURL, + thumbnailURL: item.thumbnailURL, category: item.category, title: item.title, location: item.location, @@ -115,7 +120,13 @@ final class StoreListViewController: UIViewController, View { let store = owner.reactor?.currentState.stores[indexPath.item] else { return } let detailController = DetailController() - detailController.reactor = DetailReactor(popUpID: Int64(store.id)) + detailController.reactor = DetailReactor( + popUpID: Int64(store.id), + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) owner.navigationController?.isNavigationBarHidden = false owner.navigationController?.tabBarController?.tabBar.isHidden = false @@ -124,7 +135,6 @@ final class StoreListViewController: UIViewController, View { }) .disposed(by: disposeBag) - // 4) viewWillAppear -> viewDidLoad // rx.viewWillAppear // .map { _ in Reactor.Action.viewDidLoad } @@ -155,7 +165,7 @@ final class StoreListViewController: UIViewController, View { // .compactMap { $0 } // .bind(to: reactor.action) // .disposed(by: disposeBag) - + } private func presentFilterBottomSheet(for filterType: FilterType) { @@ -167,7 +177,7 @@ final class StoreListViewController: UIViewController, View { viewController.containerView.segmentedControl.selectedSegmentIndex = initialIndex sheetReactor.action.onNext(.segmentChanged(initialIndex)) - viewController.onSave = { [weak self] selectedOptions in + viewController.onSave = { [weak self] _ in guard let self = self else { return } // 닫기 self.reactor?.action.onNext(.filterTapped(nil)) @@ -180,13 +190,13 @@ final class StoreListViewController: UIViewController, View { viewController.modalPresentationStyle = .overFullScreen present(viewController, animated: false) { - viewController.showBottomSheet() +// viewController.showBottomSheet() } } private func dismissFilterBottomSheet() { if let sheet = presentedViewController as? FilterBottomSheetViewController { - sheet.hideBottomSheet() + sheet.dismiss(animated: true) } } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageController.swift index 9e52be99..4c7cecc7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageController.swift @@ -1,26 +1,21 @@ -// -// BlockUserManageController.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class BlockUserManageController: BaseViewController, View { - + typealias Reactor = BlockUserManageReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = BlockUserManageView() - + private var sections: [any Sectionable] = [] } @@ -30,7 +25,7 @@ extension BlockUserManageController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -52,12 +47,12 @@ private extension BlockUserManageController { mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( BlockUserListSectionCell.self, forCellWithReuseIdentifier: BlockUserListSectionCell.identifiers ) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) @@ -75,12 +70,12 @@ extension BlockUserManageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -98,11 +93,11 @@ extension BlockUserManageController: UICollectionViewDelegate, UICollectionViewD func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift index f714226b..b750673d 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift @@ -1,42 +1,38 @@ -// -// BlockUserManageReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit +import DesignSystem +import DomainInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class BlockUserManageReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case blockButtonTapped(row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isEmptyList: Bool = true } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -50,16 +46,17 @@ final class BlockUserManageReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var countSection = CommentListTitleSection(inputDataList: []) private var listSection = BlockUserListSection(inputDataList: []) private var spcing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -87,7 +84,7 @@ final class BlockUserManageReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -99,7 +96,7 @@ final class BlockUserManageReactor: Reactor { newState.isEmptyList = listSection.isEmpty return newState } - + func getSection() -> [any Sectionable] { return [ spcing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift index 28e01604..f5ceb6a5 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift @@ -1,26 +1,21 @@ -// -// BlockUserListSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit +import DesignSystem + import RxSwift struct BlockUserListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = BlockUserListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift index f784f85b..947a3d99 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift @@ -1,21 +1,16 @@ -// -// BlockUserListSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class BlockUserListSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 18 @@ -23,29 +18,28 @@ final class BlockUserListSectionCell: UICollectionViewCell { view.contentMode = .scaleAspectFill return view }() - + private let nickNameLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let blockButton: UIButton = { let button = UIButton() button.layer.cornerRadius = 4 return button }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -60,18 +54,18 @@ private extension BlockUserListSectionCell { make.size.equalTo(36) make.leading.centerY.equalToSuperview() } - + contentView.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(profileImageView.snp.trailing).offset(12) } - + contentView.addSubview(blockButton) blockButton.snp.makeConstraints { make in make.width.equalTo(75) make.height.equalTo(32) - + make.centerY.trailing.equalToSuperview() } } @@ -84,19 +78,19 @@ extension BlockUserListSectionCell: Inputable { var userID: String? var isBlocked: Bool } - + func injection(with input: Input) { profileImageView.setPPImage(path: input.profileImagePath) - nickNameLabel.setLineHeightText(text: input.nickName, font: .KorFont(style: .bold, size: 14)) + nickNameLabel.setLineHeightText(text: input.nickName, font: .korFont(style: .bold, size: 14)) if input.isBlocked { blockButton.setTitle("차단완료", for: .normal) - blockButton.titleLabel?.font = .KorFont(style: .medium, size: 13) + blockButton.titleLabel?.font = .korFont(style: .medium, size: 13) blockButton.backgroundColor = .re600 blockButton.setTitleColor(.w100, for: .normal) blockButton.layer.borderWidth = 0 } else { blockButton.setTitle("차단해제", for: .normal) - blockButton.titleLabel?.font = .KorFont(style: .medium, size: 13) + blockButton.titleLabel?.font = .korFont(style: .medium, size: 13) blockButton.backgroundColor = .w100 blockButton.setTitleColor(.g300, for: .normal) blockButton.layer.borderWidth = 1 diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift index f217bc90..97277402 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift @@ -1,28 +1,22 @@ -// -// BlockUserManageView.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit +import DesignSystem + import SnapKit final class BlockUserManageView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "차단한 사용자 관리", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "차단한 사용자 관리", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + let emptyLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "차단한 사용자가 없어요") label.textColor = .g400 @@ -33,7 +27,7 @@ final class BlockUserManageView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -41,19 +35,19 @@ final class BlockUserManageView: UIView { // MARK: - SetUp private extension BlockUserManageView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + self.addSubview(emptyLabel) emptyLabel.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(137) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift index 413df27f..03425945 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift @@ -1,34 +1,29 @@ -// -// MyPageBookmarkController.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageBookmarkController: BaseViewController, View { - + typealias Reactor = MyPageBookmarkReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageBookmarkView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() - + private var currentPageIndex: Int = 0 - + private var maxPageIndex: Int = 0 - + private var viewType: String? } @@ -38,7 +33,7 @@ extension MyPageBookmarkController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -64,7 +59,7 @@ private extension MyPageBookmarkController { mainView.contentCollectionView.register( ListCountButtonSectionCell.self, forCellWithReuseIdentifier: ListCountButtonSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( PopUpCardSectionCell.self, forCellWithReuseIdentifier: PopUpCardSectionCell.identifiers @@ -84,7 +79,7 @@ extension MyPageBookmarkController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -92,7 +87,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -100,7 +95,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.emptyButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -108,7 +103,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.countButtonView.dropdownButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -116,7 +111,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.contentCollectionView.rx.gesture(.swipe(direction: .up)) .skip(1) .withUnretained(self) @@ -139,12 +134,12 @@ extension MyPageBookmarkController { } owner.currentPageIndex += 1 owner.mainView.contentCollectionView.scrollToItem(at: .init(row: owner.currentPageIndex, section: 1), at: .top, animated: true) - + } } } .disposed(by: disposeBag) - + mainView.contentCollectionView.rx.gesture(.swipe(direction: .down)) .skip(1) .withUnretained(self) @@ -157,7 +152,7 @@ extension MyPageBookmarkController { } } .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -166,13 +161,13 @@ extension MyPageBookmarkController { owner.mainView.contentCollectionView.isHidden = state.isEmptyCase owner.mainView.emptyLabel.isHidden = !state.isEmptyCase owner.mainView.emptyButton.isHidden = !state.isEmptyCase - owner.mainView.countButtonView.buttonTitleLabel.setLineHeightText(text: state.buttonTitle, font: .KorFont(style: .regular, size: 13)) - owner.mainView.countButtonView.countLabel.setLineHeightText(text: "총 \(state.count)개", font: .KorFont(style: .regular, size: 13)) - + owner.mainView.countButtonView.buttonTitleLabel.setLineHeightText(text: state.buttonTitle, font: .korFont(style: .regular, size: 13)) + owner.mainView.countButtonView.countLabel.setLineHeightText(text: "총 \(state.count)개", font: .korFont(style: .regular, size: 13)) + if state.buttonTitle != owner.viewType { owner.mainView.contentCollectionView.scrollsToTop = true } - + if state.buttonTitle == "크게보기" { owner.mainView.contentCollectionView.isScrollEnabled = false if owner.viewType == "모아서보기" { @@ -182,7 +177,7 @@ extension MyPageBookmarkController { } else { owner.mainView.contentCollectionView.isScrollEnabled = true } - + owner.maxPageIndex = Int(state.count) owner.viewType = state.buttonTitle } @@ -195,11 +190,11 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -212,7 +207,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? DetailSimilarSectionCell { cell.bookMarkButton.rx.tap .map { Reactor.Action.bookMarkButtonTapped(row: indexPath.row) } @@ -221,7 +216,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -230,7 +225,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa reactor?.action.onNext(.changePage) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 1 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift index a9ffdfee..6a89a5ee 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift @@ -1,18 +1,15 @@ -// -// MyPageBookmarkReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageBookmarkReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -23,7 +20,7 @@ final class MyPageBookmarkReactor: Reactor { case emptyButtonTapped(controller: BaseViewController) case bookMarkButtonTapped(row: Int) } - + enum Mutation { case loadView case skip @@ -32,7 +29,7 @@ final class MyPageBookmarkReactor: Reactor { case presentModal(controller: BaseViewController) case moveToSuggestScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false @@ -40,9 +37,9 @@ final class MyPageBookmarkReactor: Reactor { var count: Int32 = 0 var buttonTitle: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var isLoading: Bool = false @@ -51,9 +48,9 @@ final class MyPageBookmarkReactor: Reactor { private var currentPage: Int32 = 0 private var size: Int32 = 10 private var viewType: String = "크게보기" - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -67,17 +64,18 @@ final class MyPageBookmarkReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var listSection = RecentPopUpSection(inputDataList: []) private var cardListSection = PopUpCardSection(inputDataList: []) private var spacing12Section = SpacingSection(inputDataList: [.init(spacing: 12)]) private var spacing150Section = SpacingSection(inputDataList: [.init(spacing: 150)]) - + // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -165,7 +163,7 @@ final class MyPageBookmarkReactor: Reactor { } } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -178,7 +176,13 @@ final class MyPageBookmarkReactor: Reactor { controller.navigationController?.popViewController(animated: true) case .moveToDetailScene(let controller, let row): let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: listSection.inputDataList[row].id) + nextController.reactor = DetailReactor( + popUpID: listSection.inputDataList[row].id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) case .presentModal(let controller): let nextController = BookMarkPopUpViewTypeModalController() @@ -195,7 +199,11 @@ final class MyPageBookmarkReactor: Reactor { .disposed(by: nextController.disposeBag) case .moveToSuggestScene(let controller): let nextController = HomeListController() - nextController.reactor = HomeListReactor(popUpType: .curation) + nextController.reactor = HomeListReactor( + popUpType: .curation, + userAPIUseCase: userAPIUseCase, + homeAPIUseCase: DIContainer.resolve(HomeAPIUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) } newState.sections = getSection() @@ -204,7 +212,7 @@ final class MyPageBookmarkReactor: Reactor { newState.buttonTitle = viewType return newState } - + func getSection() -> [any Sectionable] { return [ spacing12Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift index e6cb99f7..e6d6d13b 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift @@ -1,23 +1,18 @@ -// -// MyPageBookmarkView.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyPageBookmarkView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "찜한 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "찜한 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -25,39 +20,38 @@ final class MyPageBookmarkView: UIView { view.isPrefetchingEnabled = true return view }() - + let emptyLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "앗! 아직 찜해둔 팝업이 없어요") label.textColor = .g400 label.isHidden = true return label }() - + let countButtonView: CountButtonView = { - let view = CountButtonView() - return view + return CountButtonView() }() - + let emptyButton: UIButton = { let button = UIButton() let buttonTitle = NSAttributedString( string: "추천 팝업 보러가기", attributes: [ - .font : UIFont.KorFont(style: .regular, size: 13)!, - .underlineStyle : NSUnderlineStyle.single.rawValue, - .foregroundColor : UIColor.g1000 + .font: UIFont.korFont(style: .regular, size: 13), + .underlineStyle: NSUnderlineStyle.single.rawValue, + .foregroundColor: UIColor.g1000 ] ) button.setAttributedTitle(buttonTitle, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -65,7 +59,7 @@ final class MyPageBookmarkView: UIView { // MARK: - SetUp private extension MyPageBookmarkView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in @@ -82,13 +76,13 @@ private extension MyPageBookmarkView { make.top.equalTo(countButtonView.snp.bottom).offset(16) make.leading.trailing.bottom.equalToSuperview() } - + self.addSubview(emptyLabel) emptyLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalToSuperview().inset(245) } - + self.addSubview(emptyButton) emptyButton.snp.makeConstraints { make in make.top.equalTo(emptyLabel.snp.bottom).offset(26) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift index b36c7257..b566d1f0 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift @@ -1,44 +1,37 @@ -// -// CountButtonView.swift -// Poppool -// -// Created by SeoJunYoung on 1/16/25. -// - import UIKit +import DesignSystem + import SnapKit final class CountButtonView: UIView { - + // MARK: - Components let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g400 return label }() - + private let dropDownImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + let buttonTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropdownButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,27 +39,27 @@ final class CountButtonView: UIView { // MARK: - SetUp private extension CountButtonView { - + func setUpConstraints() { self.addSubview(countLabel) countLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + dropdownButton.addSubview(dropDownImageView) dropDownImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.trailing.bottom.equalToSuperview() } - + dropdownButton.addSubview(buttonTitleLabel) buttonTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalTo(dropDownImageView.snp.leading).offset(-6) make.centerY.equalToSuperview() } - + self.addSubview(dropdownButton) dropdownButton.snp.makeConstraints { make in make.trailing.equalToSuperview() diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift index d71ea811..77da9b6e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift @@ -1,26 +1,21 @@ -// -// PopUpCardSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem + import RxSwift struct PopUpCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = PopUpCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift index 8812d5b0..7409806f 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift @@ -1,18 +1,13 @@ -// -// PopUpCardSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit -import SnapKit -import RxSwift +import DesignSystem + import RxCocoa +import RxSwift +import SnapKit final class PopUpCardSectionCell: UICollectionViewCell { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -20,35 +15,34 @@ final class PopUpCardSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) - label.font = .EngFont(style: .regular, size: 11) + label.font = .engFont(style: .regular, size: 11) label.textColor = .g1000 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.numberOfLines = 2 return label }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.textColor = .g400 return label }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let trailingView: UIView = UIView() - + var disposeBag = DisposeBag() - + // MARK: - init override init(frame: CGRect) { super.init(frame: frame) @@ -59,40 +53,40 @@ final class PopUpCardSectionCell: UICollectionViewCell { addHolesToCell() setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(roundedRect: contentView.bounds, cornerRadius: 4) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let leftHoleCenter = CGPoint(x: contentView.bounds.minX, y: 423) let rightHoleCenter = CGPoint(x: contentView.bounds.maxX, y: 423) - + // 구멍을 만드는 경로 생성 (반지름 6) let leftHolePath = UIBezierPath(arcCenter: leftHoleCenter, radius: 12, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: true) let rightHolePath = UIBezierPath(arcCenter: rightHoleCenter, radius: 12, startAngle: .pi / 2, endAngle: -.pi / 2, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(leftHolePath) fullPath.append(rightHolePath) fullPath.usesEvenOddFillRule = true - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd holeLayer.fillColor = UIColor.black.cgColor trailingView.layer.mask = holeLayer - + // 그림자 Layer let shadowLayer = CAShapeLayer() shadowLayer.path = fullPath.cgPath @@ -109,7 +103,7 @@ final class PopUpCardSectionCell: UICollectionViewCell { // MARK: - SetUp private extension PopUpCardSectionCell { func setUpConstraints() { - + contentView.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -119,26 +113,26 @@ private extension PopUpCardSectionCell { make.top.leading.trailing.equalToSuperview() make.height.equalTo(423) } - + trailingView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(36) make.top.trailing.equalToSuperview().inset(20) } - + trailingView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(24) make.centerX.equalToSuperview() } - + trailingView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(dateLabel.snp.bottom).offset(20) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(44) } - + trailingView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) @@ -156,16 +150,16 @@ extension PopUpCardSectionCell: Inputable { var address: String? var isBookMark: Bool } - + func injection(with input: Input) { let date = input.date ?? "" imageView.setPPImage(path: input.imagePath) - dateLabel.setLineHeightText(text: date, font: .EngFont(style: .regular, size: 13)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) + dateLabel.setLineHeightText(text: date, font: .engFont(style: .regular, size: 13)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) titleLabel.textAlignment = .center - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .regular, size: 14)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .regular, size: 14)) addressLabel.textAlignment = .center - + if input.isBookMark { bookMarkButton.setImage(UIImage(named: "icon_bookmark_fill"), for: .normal) } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift index 7b4378b6..4fad8477 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift @@ -1,18 +1,13 @@ -// -// PopUpCardView.swift -// Poppool -// -// Created by SeoJunYoung on 1/22/25. -// - import UIKit -import SnapKit -import RxSwift +import DesignSystem + import RxCocoa +import RxSwift +import SnapKit final class PopUpCardView: UIView { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -20,36 +15,34 @@ final class PopUpCardView: UIView { view.clipsToBounds = true return view }() - + let contentView: UIView = UIView() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) - label.font = .EngFont(style: .regular, size: 11) + label.font = .engFont(style: .regular, size: 11) label.textColor = .g1000 return label }() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 12) - return label + return PPLabel(style: .bold, fontSize: 12) }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.textColor = .g400 return label }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let trailingView: UIView = UIView() - + var disposeBag = DisposeBag() - + // MARK: - init init() { super.init(frame: .zero) @@ -59,40 +52,40 @@ final class PopUpCardView: UIView { trailingView.layer.cornerRadius = 4 setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func layoutSubviews() { super.layoutSubviews() addHolesToCell() } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(roundedRect: bounds, cornerRadius: 4) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let leftHoleCenter = CGPoint(x: bounds.minX, y: 423) let rightHoleCenter = CGPoint(x: bounds.maxX, y: 423) - + // 구멍을 만드는 경로 생성 (반지름 6) let leftHolePath = UIBezierPath(arcCenter: leftHoleCenter, radius: 12, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: true) let rightHolePath = UIBezierPath(arcCenter: rightHoleCenter, radius: 12, startAngle: .pi / 2, endAngle: -.pi / 2, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(leftHolePath) fullPath.append(rightHolePath) fullPath.usesEvenOddFillRule = true - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd holeLayer.fillColor = UIColor.black.cgColor trailingView.layer.mask = holeLayer - + // 그림자 Layer let shadowLayer = CAShapeLayer() shadowLayer.path = fullPath.cgPath @@ -109,7 +102,7 @@ final class PopUpCardView: UIView { // MARK: - SetUp private extension PopUpCardView { func setUpConstraints() { - + self.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -119,25 +112,25 @@ private extension PopUpCardView { make.top.leading.trailing.equalToSuperview() make.height.equalTo(423) } - + trailingView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(36) make.top.trailing.equalToSuperview().inset(20) } - + trailingView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(24) make.centerX.equalToSuperview() } - + trailingView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(dateLabel.snp.bottom).offset(20) } - + trailingView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) @@ -156,14 +149,14 @@ extension PopUpCardView: Inputable { var address: String? var isBookMark: Bool } - + func injection(with input: Input) { let date = input.date ?? "" imageView.setPPImage(path: input.imagePath) - dateLabel.setLineHeightText(text: date, font: .EngFont(style: .regular, size: 13)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .regular, size: 14)) - + dateLabel.setLineHeightText(text: date, font: .engFont(style: .regular, size: 13)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .regular, size: 14)) + if input.isBookMark { bookMarkButton.setImage(UIImage(named: "icon_bookmark_fill"), for: .normal) } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift index 9616ae96..73cf0f6f 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift @@ -1,25 +1,20 @@ -// -// BookMarkPopUpViewTypeModalController.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class BookMarkPopUpViewTypeModalController: BaseViewController, View { - + typealias Reactor = BookMarkPopUpViewTypeModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = BookMarkPopUpViewTypeModalView() } @@ -48,13 +43,13 @@ extension BookMarkPopUpViewTypeModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.sortedSegmentControl.rx.selectedSegmentIndex .skip(1) .map { Reactor.Action.selectedSegmentControl(row: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -62,7 +57,7 @@ extension BookMarkPopUpViewTypeModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -70,7 +65,7 @@ extension BookMarkPopUpViewTypeModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +87,7 @@ extension BookMarkPopUpViewTypeModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(250) } @@ -106,4 +101,3 @@ extension BookMarkPopUpViewTypeModalController: PanModalPresentable { return 20 } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift index 8bd33888..e2298095 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift @@ -1,16 +1,11 @@ -// -// BookMarkPopUpViewTypeModalReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class BookMarkPopUpViewTypeModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -18,13 +13,13 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { case saveButtonTapped(controller: BaseViewController) case xmarkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setSelectedIndex(row: Int) case dismissScene(controller: BaseViewController, isSave: Bool) } - + struct State { var isSetView: Bool = false var originSortedCode: String @@ -32,18 +27,17 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { var saveButtonIsEnabled: Bool = false var isSave: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - + // MARK: - init init(sortedCode: String) { self.initialState = State(originSortedCode: sortedCode) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,7 +51,7 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { return Observable.just(.dismissScene(controller: controller, isSave: false)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isSetView = false diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift index 8604d089..f2542b86 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift @@ -1,44 +1,36 @@ -// -// BookMarkPopUpViewTypeModalView.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem + import SnapKit final class BookMarkPopUpViewTypeModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["크게보기","모아서보기"]) - return control + return PPSegmentedControl(type: .base, segments: ["크게보기", "모아서보기"]) }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,33 +38,33 @@ final class BookMarkPopUpViewTypeModalView: UIView { // MARK: - SetUp private extension BookMarkPopUpViewTypeModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(sortedSegmentControl) sortedSegmentControl.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQController.swift index dbdb455d..8a9620f8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQController.swift @@ -1,24 +1,19 @@ -// -// FAQController.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class FAQController: BaseViewController, View { - + typealias Reactor = FAQReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = FAQView() private var sections: [any Sectionable] = [] } @@ -29,7 +24,7 @@ extension FAQController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -51,11 +46,11 @@ private extension FAQController { mainView.contentCollectionView.register( MyPageMyCommentTitleSectionCell.self, forCellWithReuseIdentifier: MyPageMyCommentTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageListSectionCell.self, forCellWithReuseIdentifier: MyPageListSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( FAQDropdownSectionCell.self, forCellWithReuseIdentifier: FAQDropdownSectionCell.identifiers @@ -78,12 +73,12 @@ extension FAQController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -99,11 +94,11 @@ extension FAQController: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -118,7 +113,7 @@ extension FAQController: UICollectionViewDelegate, UICollectionViewDataSource { } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let cell = collectionView.cellForItem(at: indexPath) as? MyPageListSectionCell { reactor?.action.onNext(.mailInquiryCellTapped(controller: self)) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQReactor.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQReactor.swift index 1f32d7e2..f1d6158f 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/FAQReactor.swift @@ -1,18 +1,13 @@ -// -// FAQReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class FAQReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,22 +15,22 @@ final class FAQReactor: Reactor { case backButtonTapped(controller: BaseViewController) case mailInquiryCellTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) case moveToMailApp(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -80,7 +75,7 @@ final class FAQReactor: Reactor { title: "고객센터 상담은 어디서 할 수 있나요?", content: "[마이페이지 > 고객문의 > 메일로 문의]에서 할 수 있으며, 주말, 공휴일을 제외한 평일 오전 9시부터 오후 6시까지 운영해요.", isOpen: false - ), + ) ]) private let qnaTitleSection = MyPageMyCommentTitleSection(inputDataList: [.init(title: "직접 문의하기")]) @@ -94,7 +89,7 @@ final class FAQReactor: Reactor { init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -109,7 +104,7 @@ final class FAQReactor: Reactor { return Observable.just(.moveToMailApp(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -128,27 +123,27 @@ final class FAQReactor: Reactor { } return newState } - + func showMailAppRecoveryAlert(controller: BaseViewController) { - + let alert = UIAlertController( title: "'Mail' 앱을 복원하겠습니까?", message: "계속하려면 App Store에서 'Mail' 앱을\n다운로드하십시오", preferredStyle: .alert ) - + alert.addAction(UIAlertAction(title: "App Store로 이동", style: .default, handler: { _ in // 📌 App Store의 메일 앱 복구 페이지 열기 if let mailAppURL = URL(string: "itms-apps://itunes.apple.com/app/id1108187098") { UIApplication.shared.open(mailAppURL, options: [:], completionHandler: nil) } })) - + alert.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil)) - + controller.present(alert, animated: true, completion: nil) } - + func getSection() -> [any Sectionable] { return [ spacing24Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift index 715c815e..8e160d4e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift @@ -1,26 +1,21 @@ -// -// FAQDropdownSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import RxSwift struct FAQDropdownSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = FAQDropdownSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct FAQDropdownSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift index b8910b60..06543581 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift @@ -1,49 +1,42 @@ -// -// FAQDropdownSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class FAQDropdownSectionCell: UICollectionViewCell { - + // MARK: - Components let contentStackView: UIStackView = { let view = UIStackView() view.axis = .vertical return view }() - + let listContentButton = UIButton() - + let qLabel: UILabel = { let label = UILabel() - label.setLineHeightText(text: "Q", font: .EngFont(style: .bold, size: 16), lineHeight: 1) + label.setLineHeightText(text: "Q", font: .engFont(style: .bold, size: 16), lineHeight: 1) label.textColor = .blu500 return label }() - + let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropDownImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + let dropContentView: UIView = { let view = UIView() view.backgroundColor = .pb4 return view }() - + let dropContentLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 @@ -51,16 +44,16 @@ final class FAQDropdownSectionCell: UICollectionViewCell { }() var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -74,7 +67,7 @@ private extension FAQDropdownSectionCell { contentStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + listContentButton.snp.makeConstraints { make in make.height.equalTo(59).priority(.high) } @@ -94,7 +87,7 @@ private extension FAQDropdownSectionCell { make.centerY.equalToSuperview() make.trailing.equalToSuperview().inset(20) } - + dropContentView.addSubview(dropContentLabel) dropContentLabel.snp.makeConstraints { make in make.top.bottom.equalToSuperview().inset(16) @@ -112,10 +105,10 @@ extension FAQDropdownSectionCell: Inputable { var content: String? var isOpen: Bool } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 14)) - dropContentLabel.setLineHeightText(text: input.content, font: .KorFont(style: .regular, size: 14), lineHeight: 1.5) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .medium, size: 14)) + dropContentLabel.setLineHeightText(text: input.content, font: .korFont(style: .regular, size: 14), lineHeight: 1.5) dropContentLabel.lineBreakStrategy = .hangulWordPriority dropContentLabel.textColor = .g600 if input.isOpen { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQView.swift index 0ecc6ab2..bec56ebf 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/FAQ/View/FAQView.swift @@ -1,35 +1,30 @@ -// -// FAQView.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import SnapKit final class FAQView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "고객문의", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "고객문의", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,13 +32,13 @@ final class FAQView: UIView { // MARK: - SetUp private extension FAQView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageController.swift index 14b45244..a555a37c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageController.swift @@ -1,48 +1,43 @@ -// -// MyPageController.swift -// Poppool -// -// Created by SeoJunYoung on 12/30/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageController: BaseViewController, View { - + typealias Reactor = MyPageReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageView() - + private let headerView: UIView = { let view = UIView() view.backgroundColor = .w100 view.alpha = 0 return view }() - + private let settingButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_gear_white"), for: .normal) - button.tintColor = .g1000 + button.tintColor = .g1000 return button }() - + private var sections: [any Sectionable] = [] private var commentCellTapped: PublishSubject = .init() private var listCellTapped: PublishSubject = .init() - + private var isBrightImage: Bool = false - + private var scrollAlpha: CGFloat = 0 - + } // MARK: - Life Cycle @@ -51,7 +46,7 @@ extension MyPageController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = false @@ -61,7 +56,7 @@ extension MyPageController { // MARK: - SetUp private extension MyPageController { func setUp() { - + if let layout = reactor?.compositionalLayout { mainView.contentCollectionView.collectionViewLayout = layout } @@ -74,19 +69,19 @@ private extension MyPageController { mainView.contentCollectionView.register( MyPageProfileSectionCell.self, forCellWithReuseIdentifier: MyPageProfileSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageMyCommentTitleSectionCell.self, forCellWithReuseIdentifier: MyPageMyCommentTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageCommentSectionCell.self, forCellWithReuseIdentifier: MyPageCommentSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageListSectionCell.self, forCellWithReuseIdentifier: MyPageListSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageLogoutSectionCell.self, forCellWithReuseIdentifier: MyPageLogoutSectionCell.identifiers @@ -95,13 +90,13 @@ private extension MyPageController { mainView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + view.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(44) } - + view.addSubview(settingButton) settingButton.snp.makeConstraints { make in make.trailing.equalTo(headerView.snp.trailing).inset(16) @@ -118,7 +113,7 @@ extension MyPageController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + commentCellTapped .withUnretained(self) .map { (owner, index) in @@ -126,7 +121,7 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + listCellTapped .withUnretained(self) .map { (owner, title) in @@ -134,7 +129,7 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + settingButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -142,18 +137,17 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.settingButton.isHidden = !state.isLogin owner.sections = state.sections owner.mainView.contentCollectionView.reloadData() - } .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -171,7 +165,7 @@ extension MyPageController { owner.settingButton.tintColor = .w100 owner.systemStatusBarIsDark.accept(false) } - + } } } @@ -185,18 +179,18 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? MyPageProfileSectionCell { let originHeight = 162 + 49 + 44 + view.safeAreaInsets.top cell.updateHeight(height: originHeight) @@ -209,7 +203,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? MyPageMyCommentTitleSectionCell { cell.button.rx.tap .withUnretained(self) @@ -219,7 +213,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? MyPageLogoutSectionCell { cell.logoutButton.rx.tap .map { Reactor.Action.logoutButtonTapped } @@ -228,7 +222,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let cell = collectionView.cellForItem(at: indexPath) as? MyPageCommentSectionCell { commentCellTapped.onNext(indexPath.row) @@ -238,7 +232,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource listCellTapped.onNext(title) } } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { if let cell = mainView.contentCollectionView.cellForItem(at: IndexPath(row: 0, section: 0)) as? MyPageProfileSectionCell { let contentOffsetY = scrollView.contentOffset.y diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageReactor.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageReactor.swift index b15851c9..70fa8dbb 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/MyPageReactor.swift @@ -1,14 +1,12 @@ -// -// MyPageReactor.swift -// Poppool -// -// Created by SeoJunYoung on 12/30/24. -// - import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageReactor: Reactor { @@ -45,7 +43,7 @@ final class MyPageReactor: Reactor { var initialState: State var disposeBag = DisposeBag() - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) + private let userAPIUseCase: UserAPIUseCase lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in @@ -103,7 +101,8 @@ final class MyPageReactor: Reactor { var isAdmin: Bool = false // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } @@ -129,7 +128,7 @@ final class MyPageReactor: Reactor { ) ] // 내가 댓글 단 팝업 리스트 - owner.commentSection.inputDataList = response.myCommentedPopUpList.map { + owner.commentSection.inputDataList = response.myCommentedPopUpList.map { .init(popUpImagePath: $0.mainImageUrl, title: $0.popUpStoreName, popUpID: $0.popUpStoreId) } if !owner.commentSection.inputDataList.isEmpty { @@ -180,13 +179,17 @@ final class MyPageReactor: Reactor { case .moveToProfileEditScene(let controller): let nextController = ProfileEditController() - nextController.reactor = ProfileEditReactor() + nextController.reactor = ProfileEditReactor( + userAPIUseCase: userAPIUseCase, + signUpAPIUseCase: DIContainer.resolve(SignUpAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) case .logout: - let service = KeyChainService() - let _ = service.deleteToken(type: .accessToken) - let _ = service.deleteToken(type: .refreshToken) + @Dependency var keyChainService: KeyChainService + _ = keyChainService.deleteToken(type: .accessToken) + _ = keyChainService.deleteToken(type: .refreshToken) ToastMaker.createToast(message: "로그아웃 되었어요") DispatchQueue.main.async { [weak self] in self?.action.onNext(.viewWillAppear) @@ -207,7 +210,7 @@ final class MyPageReactor: Reactor { case .apply: nextController.dismiss(animated: true) { let reasonController = WithdrawlReasonController() - reasonController.reactor = WithdrawlReasonReactor() + reasonController.reactor = WithdrawlReasonReactor(userAPIUseCase: self.userAPIUseCase) controller?.navigationController?.pushViewController(reasonController, animated: true) } case .cancel: @@ -220,12 +223,12 @@ final class MyPageReactor: Reactor { case "차단한 사용자 관리": let nextController = BlockUserManageController() - nextController.reactor = BlockUserManageReactor() + nextController.reactor = BlockUserManageReactor(userAPIUseCase: userAPIUseCase) controller.navigationController?.pushViewController(nextController, animated: true) case "공지사항": let nextController = MyPageNoticeController() - nextController.reactor = MyPageNoticeReactor() + nextController.reactor = MyPageNoticeReactor(userAPIUseCase: userAPIUseCase) controller.navigationController?.pushViewController(nextController, animated: true) case "고객문의": @@ -235,12 +238,12 @@ final class MyPageReactor: Reactor { case "찜한 팝업": let nextController = MyPageBookmarkController() - nextController.reactor = MyPageBookmarkReactor() + nextController.reactor = MyPageBookmarkReactor(userAPIUseCase: userAPIUseCase) controller.navigationController?.pushViewController(nextController, animated: true) case "최근 본 팝업": let nextController = MyPageRecentController() - nextController.reactor = MyPageRecentReactor() + nextController.reactor = MyPageRecentReactor(userAPIUseCase: userAPIUseCase) controller.navigationController?.pushViewController(nextController, animated: true) case "약관": @@ -254,30 +257,35 @@ final class MyPageReactor: Reactor { case .moveToPopUpDetailScene(let controller, let row): let nextController = DetailController() let popUpID = commentSection.inputDataList[row].popUpID - nextController.reactor = DetailReactor(popUpID: popUpID) + nextController.reactor = DetailReactor( + popUpID: popUpID, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) case .moveToLoginScene(let controller): let nextController = SubLoginController() - nextController.reactor = SubLoginReactor() + nextController.reactor = SubLoginReactor( + authAPIUseCase: DIContainer.resolve(AuthAPIUseCase.self), + kakaoLoginUseCase: DIContainer.resolve(KakaoLoginUseCase.self), + appleLoginUseCase: DIContainer.resolve(AppleLoginUseCase.self) + ) let navigationController = UINavigationController(rootViewController: nextController) navigationController.modalPresentationStyle = .fullScreen controller.present(navigationController, animated: true) - case .moveToMyCommentScene(let controller): let nextController = MyCommentController() - nextController.reactor = MyCommentReactor() + nextController.reactor = MyCommentReactor(userAPIUseCase: userAPIUseCase) controller.navigationController?.pushViewController(nextController, animated: true) - case .moveToAdminScene(let controller): // 관리자 VC let nickname = profileSection.inputDataList.first?.nickName ?? "" - let adminVC = AdminViewController(nickname: nickname) - adminVC.reactor = AdminReactor( - useCase: DefaultAdminUseCase( - repository: DefaultAdminRepository(provider: ProviderImpl()) - ) - ) + let adminUseCase = DIContainer.resolve(AdminUseCase.self) + let adminVC = AdminViewController(nickname: nickname, adminUseCase: adminUseCase) + adminVC.reactor = AdminReactor(adminUseCase: adminUseCase) controller.navigationController?.pushViewController(adminVC, animated: true) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift index 274f90fb..8a85d6c5 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift @@ -1,26 +1,21 @@ -// -// MyPageCommentSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit +import DesignSystem + import RxSwift struct MyPageCommentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageCommentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(68), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift index 2f17bf4c..f97564cb 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift @@ -1,17 +1,12 @@ -// -// MyPageCommentSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyPageCommentSectionCell: UICollectionViewCell { - + // MARK: - Components private let firstBackgroundView: UIView = { let view = UIView() @@ -20,20 +15,20 @@ final class MyPageCommentSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let imageBackgroundView: UIView = { let view = UIView() view.backgroundColor = .w100 view.layer.cornerRadius = 31 return view }() - + private let gradientView: AnimatedGradientView = { let view = AnimatedGradientView() view.isHidden = true return view }() - + private let imageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 28 @@ -41,20 +36,19 @@ final class MyPageCommentSectionCell: UICollectionViewCell { view.contentMode = .scaleAspectFill return view }() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 11) - return label + return PPLabel(style: .regular, fontSize: 11) }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -68,24 +62,24 @@ private extension MyPageCommentSectionCell { make.leading.trailing.top.equalToSuperview() make.height.equalTo(68) } - + firstBackgroundView.addSubview(gradientView) gradientView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + firstBackgroundView.addSubview(imageBackgroundView) imageBackgroundView.snp.makeConstraints { make in make.size.equalTo(62) make.center.equalToSuperview() } - + imageBackgroundView.addSubview(imageView) imageView.snp.makeConstraints { make in make.size.equalTo(56) make.center.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() @@ -100,12 +94,12 @@ extension MyPageCommentSectionCell: Inputable { var popUpID: Int64 var isFirstCell: Bool = false } - + func injection(with input: Input) { imageView.setPPImage(path: input.popUpImagePath) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 11)) titleLabel.textAlignment = .center - + if input.isFirstCell { gradientView.isHidden = false } else { @@ -115,39 +109,39 @@ extension MyPageCommentSectionCell: Inputable { } class AnimatedGradientView: UIView { - + private let gradientLayer = CAGradientLayer() - + override init(frame: CGRect) { super.init(frame: frame) setupGradient() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupGradient() } - + private func setupGradient() { // 초기 그라디언트 색상 설정 gradientLayer.colors = [ UIColor.init(hexCode: "#1570FC").cgColor, UIColor.init(hexCode: "#00E6BD").cgColor ] - + gradientLayer.startPoint = CGPoint(x: 0, y: 0) gradientLayer.endPoint = CGPoint(x: 1, y: 1) gradientLayer.frame = bounds layer.insertSublayer(gradientLayer, at: 0) - + animateGradient() } - + override func layoutSubviews() { super.layoutSubviews() gradientLayer.frame = bounds // 레이아웃 변경 시 반영 } - + private func animateGradient() { let animation = CABasicAnimation(keyPath: "colors") animation.fromValue = gradientLayer.colors @@ -158,7 +152,7 @@ class AnimatedGradientView: UIView { animation.duration = 1 // 색이 부드럽게 바뀌는 시간 animation.autoreverses = true // 원래 색으로 돌아가게 함 animation.repeatCount = .infinity // 무한 반복 - + gradientLayer.add(animation, forKey: "colorChange") } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift index b31885e8..12063019 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift @@ -1,26 +1,21 @@ -// -// MyPageListSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit +import DesignSystem + import RxSwift struct MyPageListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct MyPageListSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift index faa94ae6..f04cd680 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift @@ -1,39 +1,33 @@ -// -// MyPageListSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyPageListSectionCell: UICollectionViewCell { - + // MARK: - Components let titleLabel = UILabel() - + private let rightImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") return view }() - + private let subTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -47,13 +41,13 @@ private extension MyPageListSectionCell { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + contentView.addSubview(rightImageView) rightImageView.snp.makeConstraints { make in make.trailing.centerY.equalToSuperview() make.size.equalTo(22) } - + contentView.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.centerY.trailing.equalToSuperview() @@ -66,17 +60,17 @@ extension MyPageListSectionCell: Inputable { var title: String? var subTitle: String? } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 15)) - + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 15)) + if input.subTitle == nil { rightImageView.isHidden = false subTitleLabel.isHidden = true } else { rightImageView.isHidden = true subTitleLabel.isHidden = false - subTitleLabel.setLineHeightText(text: input.subTitle, font: .KorFont(style: .regular, size: 13)) + subTitleLabel.setLineHeightText(text: input.subTitle, font: .korFont(style: .regular, size: 13)) subTitleLabel.textColor = . blu500 } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift index be5246e8..39ca5dfb 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift @@ -1,26 +1,21 @@ -// -// MyPageLogoutSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/4/25. -// - import UIKit +import DesignSystem + import RxSwift struct MyPageLogoutSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageLogoutSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct MyPageLogoutSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift similarity index 80% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift index dc46e212..ffffb3fc 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift @@ -1,36 +1,30 @@ -// -// MyPageLogoutSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/4/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyPageLogoutSectionCell: UICollectionViewCell { - + // MARK: - Components let logoutButton: PPButton = { - let button = PPButton(style: .secondary, text: "로그아웃") - return button + return PPButton(style: .secondary, text: "로그아웃") }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -51,7 +45,7 @@ extension MyPageLogoutSectionCell: Inputable { struct Input { } - + func injection(with input: Input) { } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift index e87b6626..8d198680 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift @@ -1,26 +1,21 @@ -// -// MyPageMyCommentTitleSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit +import DesignSystem + import RxSwift struct MyPageMyCommentTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageMyCommentTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct MyPageMyCommentTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift similarity index 73% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift index f9820f1c..e2c3bb94 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift @@ -1,40 +1,33 @@ -// -// MyPageMyCommentTitleSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/2/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyPageMyCommentTitleSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -48,7 +41,7 @@ private extension MyPageMyCommentTitleSectionCell { titleLabel.snp.makeConstraints { make in make.centerY.leading.equalToSuperview() } - + contentView.addSubview(button) button.snp.makeConstraints { make in make.centerY.trailing.equalToSuperview() @@ -61,17 +54,17 @@ extension MyPageMyCommentTitleSectionCell: Inputable { var title: String? var buttonTitle: String? } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) - + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) + if input.buttonTitle != nil { let buttonTitle = NSAttributedString( string: input.buttonTitle ?? "", attributes: [ - .font : UIFont.KorFont(style: .regular, size: 13)!, - .underlineStyle : NSUnderlineStyle.single.rawValue, - .foregroundColor : UIColor.g600 + .font: UIFont.korFont(style: .regular, size: 13), + .underlineStyle: NSUnderlineStyle.single.rawValue, + .foregroundColor: UIColor.g600 ] ) button.setAttributedTitle(buttonTitle, for: .normal) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift similarity index 81% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift index 8276831c..36598dff 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift @@ -1,26 +1,21 @@ -// -// MyPageProfileSection.swift -// Poppool -// -// Created by SeoJunYoung on 12/31/24. -// - import UIKit +import DesignSystem + import RxSwift struct MyPageProfileSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageProfileSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +30,7 @@ struct MyPageProfileSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift index c7edecae..33b14aa4 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift @@ -1,40 +1,31 @@ -// -// MyPageProfileSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 12/31/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyPageProfileSectionCell: UICollectionViewCell { - + // MARK: - Components private let backGroundTrailingView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let backGroundImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + lazy var blurView: UIVisualEffectView = { let blurEffect = UIBlurEffect(style: .regular) - let view = UIVisualEffectView(effect: blurEffect) - return view + return UIVisualEffectView(effect: blurEffect) }() - + private let profileView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 32 @@ -42,73 +33,70 @@ final class MyPageProfileSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let bottomView: UIView = { let view = UIView() view.backgroundColor = .w100 return view }() - + let nickNameLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let descriptionLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let bottomHoleView: UIView = { let view = UIView() view.backgroundColor = .w100 view.alpha = 0 return view }() - + private let loginView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let loginLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18, text: "나에게 딱 맞는\n팝업스토어 만나러 가기") label.textColor = .w100 label.numberOfLines = 2 return label }() - + let loginButton: UIButton = { let button = UIButton() button.setTitle("로그인/회원가입", for: .normal) button.backgroundColor = .w10 - button.titleLabel?.font = .KorFont(style: .medium, size: 13) + button.titleLabel?.font = .korFont(style: .medium, size: 13) button.setTitleColor(.w100, for: .normal) button.layer.cornerRadius = 4 return button }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() addHolesToCell() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() } - + var cellHeight: Constraint? var containerTopInset: Constraint? - + var isBright: PublishSubject = .init() } @@ -121,62 +109,62 @@ private extension MyPageProfileSectionCell { make.edges.equalToSuperview() cellHeight = make.height.equalTo(162 + 49 + 44).priority(.high).constraint } - + backGroundTrailingView.addSubview(backGroundImageView) backGroundImageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.size.equalTo(UIScreen.main.bounds.width) } - + backGroundTrailingView.addSubview(blurView) blurView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + backGroundTrailingView.addSubview(profileView) profileView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(162) containerTopInset = make.top.equalToSuperview().constraint } - + backGroundTrailingView.addSubview(bottomView) bottomView.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview() make.height.equalTo(49) } - + backGroundTrailingView.addSubview(loginView) loginView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(162) make.bottom.equalTo(bottomView.snp.top) } - + profileView.addSubview(profileImageView) profileImageView.snp.makeConstraints { make in make.size.equalTo(64) make.top.equalToSuperview().inset(17) make.leading.equalToSuperview().inset(20) } - + profileView.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in make.centerY.equalTo(profileImageView) make.leading.equalTo(profileImageView.snp.trailing).offset(10) } - + profileView.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(profileImageView.snp.bottom).offset(25) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(bottomHoleView) bottomHoleView.snp.makeConstraints { make in make.edges.equalTo(bottomView) } - + contentView.addSubview(loginButton) loginButton.snp.makeConstraints { make in make.width.equalTo(120) @@ -184,32 +172,32 @@ private extension MyPageProfileSectionCell { make.bottom.equalToSuperview().inset(97) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(loginLabel) loginLabel.snp.makeConstraints { make in make.bottom.equalTo(loginButton.snp.top).offset(-16) make.leading.equalToSuperview().inset(20) } } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(rect: bounds) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let holeCenter = CGPoint(x: bounds.maxX / 2, y: bounds.minY) - + // 구멍을 만드는 경로 생성 (반지름 6) let holePath = UIBezierPath(arcCenter: holeCenter, radius: 12, startAngle: 0, endAngle: .pi, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(holePath) - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd - + // 구멍을 추가하는 서브 레이어로 삽입 bottomView.layer.mask = holeLayer } @@ -222,7 +210,7 @@ extension MyPageProfileSectionCell: Inputable { var nickName: String? var description: String? } - + func injection(with input: Input) { if input.isLogin { profileView.isHidden = false @@ -230,8 +218,8 @@ extension MyPageProfileSectionCell: Inputable { loginLabel.isHidden = true loginButton.isHidden = true blurView.isHidden = false - nickNameLabel.setLineHeightText(text: input.nickName, font: .KorFont(style: .bold, size: 16)) - descriptionLabel.setLineHeightText(text: input.description ?? "", font: .KorFont(style: .light, size: 11)) + nickNameLabel.setLineHeightText(text: input.nickName, font: .korFont(style: .bold, size: 16)) + descriptionLabel.setLineHeightText(text: input.description ?? "", font: .korFont(style: .light, size: 11)) backGroundImageView.image = nil backGroundImageView.setPPImage(path: input.profileImagePath) profileImageView.setPPImage(path: input.profileImagePath) { [weak self] in @@ -255,19 +243,19 @@ extension MyPageProfileSectionCell: Inputable { loginLabel.isHidden = false loginButton.isHidden = false blurView.isHidden = true - + } } - + func updateHeight(height: CGFloat) { cellHeight?.update(offset: height) layoutIfNeeded() } - + func updateAlpha(alpha: CGFloat) { bottomHoleView.alpha = alpha } - + func updateContentTopInset(inset: CGFloat) { containerTopInset?.update(offset: inset) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageView.swift index 967bc557..88645b9a 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Main/View/MyPageView.swift @@ -1,16 +1,11 @@ -// -// MyPageView.swift -// Poppool -// -// Created by SeoJunYoung on 12/30/24. -// - import UIKit +import DesignSystem + import SnapKit final class MyPageView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) @@ -22,7 +17,7 @@ final class MyPageView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -30,7 +25,7 @@ final class MyPageView: UIView { // MARK: - SetUp private extension MyPageView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift index d886c21b..2dd1b715 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift @@ -1,28 +1,23 @@ -// -// MyCommentController.swift -// Poppool -// -// Created by SeoJunYoung on 1/8/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyCommentController: BaseViewController, View { - + typealias Reactor = MyCommentReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyCommentView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -46,7 +41,7 @@ private extension MyCommentController { } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers @@ -59,7 +54,7 @@ private extension MyCommentController { MyCommentedPopUpGridSectionCell.self, forCellWithReuseIdentifier: MyCommentedPopUpGridSectionCell.identifiers ) - + view.backgroundColor = .g50 view.addSubview(mainView) mainView.snp.makeConstraints { make in @@ -75,7 +70,7 @@ extension MyCommentController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -83,7 +78,7 @@ extension MyCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -91,7 +86,7 @@ extension MyCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -107,19 +102,18 @@ extension MyCommentController: UICollectionViewDelegate, UICollectionViewDataSou func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift index bc297806..b036fcdf 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift @@ -1,44 +1,41 @@ -// -// MyCommentReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/8/25. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyCommentReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case listTapped(controller: BaseViewController, row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case skip case moveToDetailScene(controller: BaseViewController, row: Int) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -52,17 +49,18 @@ final class MyCommentReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var listCountSection = CommentListTitleSection(inputDataList: []) private var listSection = MyCommentedPopUpGridSection(inputDataList: []) private var spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private var spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - + // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -93,7 +91,7 @@ final class MyCommentReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -106,14 +104,20 @@ final class MyCommentReactor: Reactor { case .moveToDetailScene(let controller, let row): let popUpID = listSection.inputDataList[row].popUpID let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: popUpID) + nextController.reactor = DetailReactor( + popUpID: popUpID, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) case .moveToRecentScene(let controller): controller.navigationController?.popViewController(animated: true) } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift index 2b287e57..1972644e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift @@ -1,26 +1,21 @@ -// -// ListCountButtonSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit +import DesignSystem + import RxSwift struct ListCountButtonSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ListCountButtonSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct ListCountButtonSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift index de479782..2f31bed1 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift @@ -1,53 +1,46 @@ -// -// ListCountButtonSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class ListCountButtonSectionCell: UICollectionViewCell { - + // MARK: - Components - + private let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g400 return label }() - + private let dropDownImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + private let buttonTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropdownButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -62,20 +55,20 @@ private extension ListCountButtonSectionCell { make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + dropdownButton.addSubview(dropDownImageView) dropDownImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.trailing.bottom.equalToSuperview() } - + dropdownButton.addSubview(buttonTitleLabel) buttonTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalTo(dropDownImageView.snp.leading).offset(-6) make.centerY.equalToSuperview() } - + self.addSubview(dropdownButton) dropdownButton.snp.makeConstraints { make in make.trailing.equalToSuperview() @@ -89,9 +82,9 @@ extension ListCountButtonSectionCell: Inputable { var count: Int64 var buttonTitle: String? } - + func injection(with input: Input) { - countLabel.setLineHeightText(text: "총 \(input.count)개", font: .KorFont(style: .regular, size: 13)) - buttonTitleLabel.setLineHeightText(text: input.buttonTitle, font: .KorFont(style: .regular, size: 13)) + countLabel.setLineHeightText(text: "총 \(input.count)개", font: .korFont(style: .regular, size: 13)) + buttonTitleLabel.setLineHeightText(text: input.buttonTitle, font: .korFont(style: .regular, size: 13)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift index 25bc49e9..31a5aec7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift @@ -1,35 +1,30 @@ -// -// MyCommentView.swift -// Poppool -// -// Created by SeoJunYoung on 1/8/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyCommentView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "내가 코멘트한 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "내가 코멘트한 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,13 +32,13 @@ final class MyCommentView: UIView { // MARK: - SetUp private extension MyCommentView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift index 3b41bf85..4e811a51 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift @@ -1,26 +1,21 @@ -// -// MyCommentedPopUpGridSection.swift -// Poppool -// -// Created by SeoJunYoung on 2/6/25. -// - import UIKit +import DesignSystem + import RxSwift struct MyCommentedPopUpGridSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyCommentedPopUpGridSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 8) / 2), @@ -34,7 +29,7 @@ struct MyCommentedPopUpGridSection: Sectionable { ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(8) - + // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift index 29a04bdf..19cd3a8f 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift @@ -1,17 +1,12 @@ -// -// MyCommentedPopUpGridSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 2/6/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class MyCommentedPopUpGridSectionCell: UICollectionViewCell { - + // MARK: - Components private let contentImageView: UIImageView = { let view = UIImageView() @@ -19,32 +14,31 @@ final class MyCommentedPopUpGridSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let titleLabel: UILabel = { let label = UILabel() label.textColor = .blu500 return label }() - + private let contentLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let dateLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -60,25 +54,25 @@ private extension MyCommentedPopUpGridSectionCell { contentView.layer.shadowOpacity = 1 contentView.layer.shadowRadius = 8 contentView.layer.shadowOffset = CGSize(width: 0, height: 2) - + contentView.addSubview(contentImageView) contentImageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(contentView.bounds.width) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(contentImageView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(contentLabel) contentLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(contentLabel.snp.bottom).offset(8) @@ -97,12 +91,12 @@ extension MyCommentedPopUpGridSectionCell: Inputable { var startDate: String? var endDate: String? } - + func injection(with input: Input) { contentImageView.setPPImage(path: input.imageURL) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 11)) - contentLabel.setLineHeightText(text: input.content, font: .KorFont(style: .medium, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 11)) + contentLabel.setLineHeightText(text: input.content, font: .korFont(style: .medium, size: 11)) contentLabel.numberOfLines = 2 - dateLabel.setLineHeightText(text: "\(input.startDate ?? "") ~ \(input.endDate ?? "")", font: .EngFont(style: .regular, size: 11)) + dateLabel.setLineHeightText(text: "\(input.startDate ?? "") ~ \(input.endDate ?? "")", font: .engFont(style: .regular, size: 11)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift index b107a650..02455b41 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift @@ -1,25 +1,20 @@ -// -// MyCommentSortedModalController.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class MyCommentSortedModalController: BaseViewController, View { - + typealias Reactor = MyCommentSortedModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyCommentSortedModalView() } @@ -48,13 +43,13 @@ extension MyCommentSortedModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.sortedSegmentControl.rx.selectedSegmentIndex .skip(1) .map { Reactor.Action.selectedSegmentControl(row: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -62,7 +57,7 @@ extension MyCommentSortedModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -70,7 +65,7 @@ extension MyCommentSortedModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +87,7 @@ extension MyCommentSortedModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(250) } @@ -106,4 +101,3 @@ extension MyCommentSortedModalController: PanModalPresentable { return 20 } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift index ec940c78..9e71d6b0 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift @@ -1,16 +1,11 @@ -// -// MyCommentSortedModalReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyCommentSortedModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -18,13 +13,13 @@ final class MyCommentSortedModalReactor: Reactor { case saveButtonTapped(controller: BaseViewController) case xmarkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setSelectedIndex(row: Int) case dismissScene(controller: BaseViewController, isSave: Bool) } - + struct State { var isSetView: Bool = false var originSortedCode: String @@ -32,18 +27,17 @@ final class MyCommentSortedModalReactor: Reactor { var saveButtonIsEnabled: Bool = false var isSave: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - + // MARK: - init init(sortedCode: String) { self.initialState = State(originSortedCode: sortedCode) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,7 +51,7 @@ final class MyCommentSortedModalReactor: Reactor { return Observable.just(.dismissScene(controller: controller, isSave: false)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isSetView = false diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift similarity index 76% rename from Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift index 625953d9..d3e2af1d 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift @@ -1,44 +1,36 @@ -// -// MyCommentSortedModalView.swift -// Poppool -// -// Created by SeoJunYoung on 1/12/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyCommentSortedModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["최신순","반응순"]) - return control + return PPSegmentedControl(type: .base, segments: ["최신순", "반응순"]) }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,33 +38,33 @@ final class MyCommentSortedModalView: UIView { // MARK: - SetUp private extension MyCommentSortedModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(sortedSegmentControl) sortedSegmentControl.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift similarity index 84% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift index abcfb3d0..b2324a50 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift @@ -1,24 +1,19 @@ -// -// MyPageNoticeDetailController.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageNoticeDetailController: BaseViewController, View { - + typealias Reactor = MyPageNoticeDetailReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageNoticeDetailView() } @@ -48,7 +43,7 @@ extension MyPageNoticeDetailController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -56,13 +51,13 @@ extension MyPageNoticeDetailController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - owner.mainView.titleLabel.setLineHeightText(text: state.title, font: .KorFont(style: .bold, size: 18)) - owner.mainView.dateLabel.setLineHeightText(text: state.date, font: .EngFont(style: .regular, size: 14)) - owner.mainView.contentLabel.setLineHeightText(text: state.content, font: .KorFont(style: .regular, size: 14)) + owner.mainView.titleLabel.setLineHeightText(text: state.title, font: .korFont(style: .bold, size: 18)) + owner.mainView.dateLabel.setLineHeightText(text: state.date, font: .engFont(style: .regular, size: 14)) + owner.mainView.contentLabel.setLineHeightText(text: state.content, font: .korFont(style: .regular, size: 14)) } .disposed(by: disposeBag) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift index 7fc3d1c1..aa430f77 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift @@ -1,49 +1,49 @@ -// -// MyPageNoticeDetailReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// +import DesignSystem +import DomainInterface import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageNoticeDetailReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) } - + struct State { var title: String? var date: String? var content: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var title: String? var date: String? var content: String? var noticeID: Int64 - - let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) + + let userAPIUseCase: UserAPIUseCase // MARK: - init - init(noticeID: Int64) { + init( + noticeID: Int64, + userAPIUseCase: UserAPIUseCase + ) { self.noticeID = noticeID + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -60,7 +60,7 @@ final class MyPageNoticeDetailReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift index 2ae6a1d2..145a3cf0 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift @@ -1,26 +1,20 @@ -// -// MyPageNoticeDetailView.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyPageNoticeDetailView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "공지사항", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "공지사항", font: .korFont(style: .regular, size: 15)) return view }() - + let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() let dateLabel: UILabel = { let label = UILabel() @@ -32,16 +26,16 @@ final class MyPageNoticeDetailView: UIView { label.numberOfLines = 0 return label }() - + private let scrollView: UIScrollView = UIScrollView() private let contentView: UIView = UIView() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -49,26 +43,26 @@ final class MyPageNoticeDetailView: UIView { // MARK: - SetUp private extension MyPageNoticeDetailView { - + func setUpConstraints() { self.backgroundColor = .g50 self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(scrollView) scrollView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + scrollView.addSubview(contentView) contentView.snp.makeConstraints { make in make.edges.equalToSuperview() make.width.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift index f09947a0..0fcce206 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift @@ -1,28 +1,23 @@ -// -// MyPageNoticeController.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageNoticeController: BaseViewController, View { - + typealias Reactor = MyPageNoticeReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageNoticeView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -32,7 +27,7 @@ extension MyPageNoticeController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,11 +45,11 @@ private extension MyPageNoticeController { mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( NoticeListSectionCell.self, forCellWithReuseIdentifier: NoticeListSectionCell.identifiers @@ -70,7 +65,7 @@ private extension MyPageNoticeController { // MARK: - Methods extension MyPageNoticeController { func bind(reactor: Reactor) { - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,12 +73,12 @@ extension MyPageNoticeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -91,7 +86,7 @@ extension MyPageNoticeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -107,11 +102,11 @@ extension MyPageNoticeController: UICollectionViewDelegate, UICollectionViewData func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -120,7 +115,7 @@ extension MyPageNoticeController: UICollectionViewDelegate, UICollectionViewData guard let reactor = reactor else { return cell } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift index 5b24879d..7a920ceb 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift @@ -1,41 +1,37 @@ -// -// MyPageNoticeReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem +import DomainInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageNoticeReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case listCellTapped(controller: BaseViewController, row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToDetailScene(controller: BaseViewController, row: Int) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -53,12 +49,12 @@ final class MyPageNoticeReactor: Reactor { private var listSection = NoticeListSection(inputDataList: []) private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -78,7 +74,7 @@ final class MyPageNoticeReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -86,14 +82,17 @@ final class MyPageNoticeReactor: Reactor { newState.sections = getSection() case .moveToDetailScene(let controller, let row): let nextController = MyPageNoticeDetailController() - nextController.reactor = MyPageNoticeDetailReactor(noticeID: listSection.inputDataList[row].noticeID) + nextController.reactor = MyPageNoticeDetailReactor( + noticeID: listSection.inputDataList[row].noticeID, + userAPIUseCase: userAPIUseCase + ) controller.navigationController?.pushViewController(nextController, animated: true) case .moveToRecentScene(let controller): controller.navigationController?.popViewController(animated: true) } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift index cb08ae4f..0880ac21 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift @@ -1,35 +1,30 @@ -// -// MyPageNoticeView.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyPageNoticeView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "공지사항", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "공지사항", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,18 +32,18 @@ final class MyPageNoticeView: UIView { // MARK: - SetUp private extension MyPageNoticeView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift index c95268f7..e29ef831 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift @@ -1,26 +1,21 @@ -// -// NoticeListSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit +import DesignSystem + import RxSwift struct NoticeListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = NoticeListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +32,7 @@ struct NoticeListSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift similarity index 82% rename from Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift index 7f6eede7..134965d9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift @@ -1,29 +1,23 @@ -// -// NoticeListSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/13/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class NoticeListSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 14) - return label + return PPLabel(style: .medium, fontSize: 14) }() - + private let dateLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + private let arrowImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") @@ -31,12 +25,12 @@ final class NoticeListSectionCell: UICollectionViewCell { }() let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -50,14 +44,13 @@ private extension NoticeListSectionCell { make.leading.equalToSuperview() make.top.equalToSuperview().inset(20) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.bottom.equalToSuperview().inset(20) } - - + contentView.addSubview(arrowImageView) arrowImageView.snp.makeConstraints { make in make.size.equalTo(22) @@ -73,9 +66,9 @@ extension NoticeListSectionCell: Inputable { var date: String? var noticeID: Int64 } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 14)) - dateLabel.setLineHeightText(text: input.date, font: .EngFont(style: .regular, size: 12)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .medium, size: 14)) + dateLabel.setLineHeightText(text: input.date, font: .engFont(style: .regular, size: 12)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift index a17e4108..f0f36fb3 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift @@ -1,27 +1,22 @@ -// -// CategoryEditModalController.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CategoryEditModalController: BaseViewController, View { - + typealias Reactor = CategoryEditModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CategoryEditModalView() - + private var sections: [any Sectionable] = [] } @@ -42,7 +37,7 @@ private extension CategoryEditModalController { mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self mainView.contentCollectionView.register(TagSectionCell.self, forCellWithReuseIdentifier: TagSectionCell.identifiers) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) @@ -57,7 +52,7 @@ extension CategoryEditModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -65,7 +60,7 @@ extension CategoryEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -73,7 +68,7 @@ extension CategoryEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -90,19 +85,18 @@ extension CategoryEditModalController: UICollectionViewDelegate, UICollectionVie func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { reactor?.action.onNext(.cellTapped(row: indexPath.row)) } @@ -113,7 +107,7 @@ extension CategoryEditModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(360) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift similarity index 84% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift index f4b209f7..e2bb0ff5 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift @@ -1,18 +1,14 @@ -// -// CategoryEditModalReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit +import DesignSystem +import DomainInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CategoryEditModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,24 +16,24 @@ final class CategoryEditModalReactor: Reactor { case cellTapped(row: Int) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController, isEdit: Bool) } - + struct State { var sections: [any Sectionable] = [] - var originSelectedID: [Int64] + var originSelectedID: [Int] var saveButtonIsEnable: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - private let originSelectedID: [Int64] - + private let originSelectedID: [Int] + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -51,17 +47,23 @@ final class CategoryEditModalReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - - private var signUpUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - private var userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) + + private var signUpUseCase: SignUpAPIUseCase + private var userAPIUseCase: UserAPIUseCase private var tagSection = TagSection(inputDataList: []) - + // MARK: - init - init(selectedID: [Int64]) { + init( + selectedID: [Int], + userAPIUseCase: UserAPIUseCase, + signUpAPIUseCase: SignUpAPIUseCase + ) { self.originSelectedID = selectedID + self.userAPIUseCase = userAPIUseCase + self.signUpUseCase = signUpAPIUseCase self.initialState = State(originSelectedID: selectedID) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -86,15 +88,15 @@ final class CategoryEditModalReactor: Reactor { } return Observable.just(.loadView) case .saveButtonTapped(let controller): - var addList: [Int64] = [] - var keepList: [Int64] = [] - var deleteList: [Int64] = [] + var addList: [Int] = [] + var keepList: [Int] = [] + var deleteList: [Int] = [] let currentArray = tagSection.inputDataList.filter { $0.isSelected == true }.compactMap { $0.id } - for i in currentArray { - if originSelectedID.contains(i) { - keepList.append(i) + for index in currentArray { + if originSelectedID.contains(index) { + keepList.append(index) } else { - addList.append(i) + addList.append(index) } } deleteList = originSelectedID.filter { !currentArray.contains($0) } @@ -106,7 +108,7 @@ final class CategoryEditModalReactor: Reactor { .andThen(Observable.just(.moveToRecentScene(controller: controller, isEdit: true))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state let originArray = originSelectedID.sorted(by: <) @@ -118,7 +120,7 @@ final class CategoryEditModalReactor: Reactor { if isEdit { ToastMaker.createToast(message: "수정사항을 반영했어요")} controller.dismiss(animated: true) } - + if currentArray.isEmpty { newState.saveButtonIsEnable = false } else { @@ -126,7 +128,7 @@ final class CategoryEditModalReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [tagSection] } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift similarity index 81% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift index 13be5401..6e7bf640 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift @@ -1,45 +1,38 @@ -// -// CategoryEditModalView.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit +import DesignSystem + import SnapKit final class CategoryEditModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "관심 카테고리를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "관심 카테고리를 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.isScrollEnabled = false return view }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -47,28 +40,28 @@ final class CategoryEditModalView: UIView { // MARK: - SetUp private extension CategoryEditModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(32) make.leading.trailing.equalToSuperview() make.height.equalTo(195) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift index e4467054..b2a5a0a9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift @@ -1,25 +1,20 @@ -// -// InfoEditModalController.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class InfoEditModalController: BaseViewController, View { - + typealias Reactor = InfoEditModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = InfoEditModalView() } @@ -51,12 +46,12 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.genderSegmentControl.rx.selectedSegmentIndex .map { Reactor.Action.changeGender(index: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.ageButton.button.rx.tap .withUnretained(self) .map { (owner, _) in @@ -64,7 +59,7 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -72,7 +67,7 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +87,7 @@ extension InfoEditModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(390) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift index 252ca754..fc3b890d 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift @@ -1,18 +1,14 @@ -// -// InfoEditModalReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit +import DesignSystem +import DomainInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class InfoEditModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -22,7 +18,7 @@ final class InfoEditModalReactor: Reactor { case changeAge(age: Int32) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setGender(index: Int) @@ -30,32 +26,37 @@ final class InfoEditModalReactor: Reactor { case moveToAgeSelectedScene(controller: BaseViewController) case moveToRecentScene(controller: BaseViewController, isEdit: Bool) } - + struct State { var age: Int32 var gender: String? var isLoadView: Bool = true var saveButtonEnable: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + var originAge: Int32 var originGender: String? var currentAge: Int32 = 0 var currentGender: String? - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) + + private let userAPIUseCase: UserAPIUseCase // MARK: - init - init(age: Int32, gender: String?) { + init( + age: Int32, + gender: String?, + userAPIUseCase: UserAPIUseCase + ) { self.originAge = age self.originGender = gender + self.userAPIUseCase = userAPIUseCase self.initialState = State(age: age, gender: gender) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -74,7 +75,7 @@ final class InfoEditModalReactor: Reactor { .andThen(Observable.just(.moveToRecentScene(controller: controller, isEdit: true))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isLoadView = false @@ -96,7 +97,6 @@ final class InfoEditModalReactor: Reactor { .disposed(by: nextController.disposeBag) } - case .moveToRecentScene(let controller, let isEdit): if isEdit { ToastMaker.createToast(message: "수정사항을 반영했어요") } controller.dismiss(animated: true) @@ -106,7 +106,7 @@ final class InfoEditModalReactor: Reactor { case .setAge(let age): newState.age = age } - + if newState.gender == originGender && newState.age == originAge { newState.saveButtonEnable = false } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift index 0d48161e..99298377 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift @@ -1,59 +1,48 @@ -// -// InfoEditModalView.swift -// Poppool -// -// Created by SeoJunYoung on 1/10/25. -// - import UIKit +import DesignSystem + import SnapKit final class InfoEditModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "사용자 정보를 설정해주세요.") - return label + return PPLabel(style: .bold, fontSize: 18, text: "사용자 정보를 설정해주세요.") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + private let genderTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "성별") - return label + return PPLabel(style: .regular, fontSize: 13, text: "성별") }() - + let genderSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["남성", "여성", "선택안함"]) - return control + return PPSegmentedControl(type: .base, segments: ["남성", "여성", "선택안함"]) }() - + private let ageTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "나이") - return label + return PPLabel(style: .regular, fontSize: 13, text: "나이") }() - + let ageButton: AgeSelectedButton = { - let label = AgeSelectedButton() - return label + return AgeSelectedButton() }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,46 +50,46 @@ final class InfoEditModalView: UIView { // MARK: - SetUp private extension InfoEditModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(genderTitleLabel) genderTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) } - + self.addSubview(genderSegmentControl) genderSegmentControl.snp.makeConstraints { make in make.top.equalTo(genderTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(ageTitleLabel) ageTitleLabel.snp.makeConstraints { make in make.top.equalTo(genderSegmentControl.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) } - + self.addSubview(ageButton) ageButton.snp.makeConstraints { make in make.top.equalTo(ageTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(72) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift index 3b71886f..4fddc408 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift @@ -1,28 +1,24 @@ -// -// ProfileEditController.swift -// Poppool -// -// Created by SeoJunYoung on 1/4/25. -// - -import UIKit import PhotosUI +import UIKit + +import DesignSystem +import Infrastructure -import SnapKit -import RxCocoa -import RxSwift import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class ProfileEditController: BaseViewController, View { - + typealias Reactor = ProfileEditReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = ProfileEditView() - + var isFirstResponse: Bool = true } @@ -32,7 +28,7 @@ extension ProfileEditController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -53,60 +49,60 @@ private extension ProfileEditController { // MARK: - Methods extension ProfileEditController { func bind(reactor: Reactor) { - + mainView.rx.tapGesture() .withUnretained(self) .subscribe { (owner, _) in owner.mainView.endEditing(true) } .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.text .skip(1) .debounce(.milliseconds(300), scheduler: MainScheduler.asyncInstance) .map { Reactor.Action.changeNickName(nickName: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.controlEvent(.editingDidBegin) .map { Reactor.Action.beginNickName } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.controlEvent(.editingDidEnd) .map { Reactor.Action.endNickName } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.text .distinctUntilChanged() .skip(1) .map { Reactor.Action.changeIntro(intro: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.didBeginEditing .map { Reactor.Action.beginIntro } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.didEndEditing .map { Reactor.Action.endIntro } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.profileImageButton.rx.tap .withUnretained(self) .subscribe(onNext: { (owner, _) in owner.showActionSheet() }) .disposed(by: disposeBag) - + mainView.categoryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -114,7 +110,7 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.infoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -122,18 +118,17 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .map { Reactor.Action.saveButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - - + mainView.nickNameDuplicatedCheckButton.rx.tap .map { Reactor.Action.nickNameCheckButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -141,18 +136,18 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in guard let originProfileData = state.originProfileData else { return } - + if owner.isFirstResponse { owner.mainView.profileImageView.setPPImage(path: originProfileData.profileImageUrl) owner.mainView.nickNameTextField.text = originProfileData.nickname owner.mainView.introTextView.text = originProfileData.intro } - + let categoryTitleList = originProfileData.interestCategoryList.map { $0.category } let categoryTitle: String if let firstTitle = categoryTitleList.first { @@ -163,12 +158,11 @@ extension ProfileEditController { categoryTitle = "" } owner.mainView.categoryButton.subTitleLabel - .setLineHeightText(text: categoryTitle, font: .KorFont(style: .regular, size: 13), lineHeight: 1) + .setLineHeightText(text: categoryTitle, font: .korFont(style: .regular, size: 13), lineHeight: 1) let userInfoTitle = "\(originProfileData.gender ?? "")・\(originProfileData.age)세" owner.mainView.infoButton.subTitleLabel - .setLineHeightText(text: userInfoTitle, font: .KorFont(style: .regular, size: 13) ,lineHeight: 1) - - + .setLineHeightText(text: userInfoTitle, font: .korFont(style: .regular, size: 13), lineHeight: 1) + // NickName TextField 설정 owner.mainView.nickNameTextFieldTrailingView.layer.borderColor = state.nickNameState.borderColor?.cgColor owner.mainView.nickNameClearButton.isHidden = state.nickNameState.isHiddenClearButton @@ -178,7 +172,7 @@ extension ProfileEditController { owner.mainView.nickNameTextField.textColor = state.nickNameState.textFieldTextColor owner.mainView.nickNameTextCountLabel.text = "\(owner.mainView.nickNameTextField.text?.count ?? 0) / 10자" owner.mainView.nickNameDuplicatedCheckButton.isEnabled = state.nickNameState.duplicatedCheckButtonIsEnabled - + // Intro TextView 설정 owner.mainView.introTextCountLabel.text = "\(owner.mainView.introTextView.text?.count ?? 0) / 30자" owner.mainView.introTextTrailingView.layer.borderColor = state.introState.borderColor?.cgColor @@ -187,7 +181,7 @@ extension ProfileEditController { owner.mainView.introTextCountLabel.textColor = state.introState.textColor owner.mainView.introTextView.textColor = state.introState.textFieldTextColor owner.mainView.introPlaceHolderLabel.isHidden = state.introState.placeHolderIsHidden - + owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable owner.isFirstResponse = false } @@ -199,7 +193,7 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo func showActionSheet() { // ActionSheet 생성 let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - + // 버튼 추가 let takePhotoAction = UIAlertAction(title: "촬영하기", style: .default) { [weak self] _ in self?.showCamera() @@ -212,32 +206,32 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo self?.reactor?.action.onNext(.changeDefaultImage) } let cancelAction = UIAlertAction(title: "취소", style: .cancel) - + // 버튼 스타일 변경 (기본 이미지는 빨간색으로 표시) changeToDefaultImageAction.setValue(UIColor.red, forKey: "titleTextColor") - + // 버튼 추가 actionSheet.addAction(takePhotoAction) actionSheet.addAction(selectFromAlbumAction) actionSheet.addAction(changeToDefaultImageAction) actionSheet.addAction(cancelAction) - + present(actionSheet, animated: true) } - + func showCamera() { guard UIImagePickerController.isSourceTypeAvailable(.camera) else { - Logger.log(message: "카메라를 사용할 수 없습니다.", category: .error) + Logger.log("카메라를 사용할 수 없습니다.", category: .error) return } - + let imagePicker = UIImagePickerController() imagePicker.sourceType = .camera - + imagePicker.delegate = self present(imagePicker, animated: true) } - + // MARK: - PHPicker 실행 func showPHPicker() { var configuration = PHPickerConfiguration() @@ -247,26 +241,26 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo picker.delegate = self present(picker, animated: true) } - + // MARK: - UIImagePickerControllerDelegate - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let selectedImage = info[.originalImage] as? UIImage { handleSelectedImage(selectedImage) } picker.dismiss(animated: true) } - + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } - + // MARK: - PHPickerViewControllerDelegate func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) - + for result in results { if result.itemProvider.canLoadObject(ofClass: UIImage.self) { - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, _) in if let selectedImage = image as? UIImage { DispatchQueue.main.async { self?.handleSelectedImage(selectedImage) @@ -276,7 +270,7 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo } } } - + // MARK: - 이미지 처리 func handleSelectedImage(_ image: UIImage) { // 선택한 이미지 처리 diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift similarity index 85% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift index 80f0740e..5dbfcba9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift @@ -1,19 +1,16 @@ -// -// ProfileEditReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/4/25. -// - -import UIKit import PhotosUI +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class ProfileEditReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -31,7 +28,7 @@ final class ProfileEditReactor: Reactor { case saveButtonTapped case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToInfoEditScene(controller: BaseViewController) @@ -40,39 +37,46 @@ final class ProfileEditReactor: Reactor { case moveToRecentScene(controller: BaseViewController) case changeNickNameState } - + struct State { var originProfileData: GetMyProfileResponse? var saveButtonIsEnable: Bool = false var nickNameState: NickNameState = .myNickName var introState: IntroState = .validate } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var originProfileData: GetMyProfileResponse? - + var currentImage: UIImage? var isChangeImage: Bool = false var currentImagePath: String? - + var currentNickName: String? var nickNameIsActive: Bool = false - + var currentIntro: String? var introIsActive: Bool = false - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - private let imageService = PreSignedService() - + + private let userAPIUseCase: UserAPIUseCase + private let signUpAPIUseCase: SignUpAPIUseCase + private let preSignedUseCase: PreSignedUseCase + // MARK: - init - init() { + init( + userAPIUseCase: UserAPIUseCase, + signUpAPIUseCase: SignUpAPIUseCase, + preSignedUseCase: PreSignedUseCase + ) { + self.userAPIUseCase = userAPIUseCase + self.signUpAPIUseCase = signUpAPIUseCase + self.preSignedUseCase = preSignedUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -124,7 +128,7 @@ final class ProfileEditReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -133,11 +137,19 @@ final class ProfileEditReactor: Reactor { newState.introState = checkIntroState(text: currentIntro, isActive: introIsActive) case .moveToCategoryEditScene(let controller): let nextController = CategoryEditModalController() - nextController.reactor = CategoryEditModalReactor(selectedID: newState.originProfileData?.interestCategoryList.map { $0.categoryId } ?? []) + nextController.reactor = CategoryEditModalReactor( + selectedID: newState.originProfileData?.interestCategoryList.map { $0.categoryId } ?? [], + userAPIUseCase: userAPIUseCase, + signUpAPIUseCase: signUpAPIUseCase + ) controller.presentPanModal(nextController) case .moveToInfoEditScene(let controller): let nextController = InfoEditModalController() - nextController.reactor = InfoEditModalReactor(age: originProfileData?.age ?? 0, gender: originProfileData?.gender) + nextController.reactor = InfoEditModalReactor( + age: originProfileData?.age ?? 0, + gender: originProfileData?.gender, + userAPIUseCase: userAPIUseCase + ) controller.presentPanModal(nextController) case .isValidateNickName(let isValidate): if isValidate { @@ -150,12 +162,12 @@ final class ProfileEditReactor: Reactor { case .changeNickNameState: newState.nickNameState = checkNickNameState(text: currentNickName, isActive: nickNameIsActive) } - + let originNickName = originProfileData?.nickname ?? "" let currentNickName = currentNickName ?? "" let originIntro = originProfileData?.intro ?? "" let currentIntro = currentIntro ?? "" - + if isChangeImage || originNickName != currentNickName || originIntro != currentIntro { if newState.nickNameState == .validate || newState.nickNameState == .validateActive || newState.nickNameState == .myNickName || newState.nickNameState == .myNickNameActive { if newState.introState == .validate || newState.introState == .validateActive || newState.introState == .empty || newState.introState == .emptyActive { @@ -169,23 +181,23 @@ final class ProfileEditReactor: Reactor { } else { newState.saveButtonIsEnable = false } - + return newState } - + func uploadS3() -> Observable { if let changeImage = currentImage { let newPath = "ProfileImage/\(UUID().uuidString).jpg" currentImagePath = newPath if originProfileData?.profileImageUrl == nil { - return imageService.tryUpload(datas: [.init(filePath: newPath, image: changeImage)]) + return preSignedUseCase.tryUpload(presignedURLRequest: [(filePath: newPath, image: changeImage)]) .asObservable() .map { .loadView } } else { let deletePath = originProfileData?.profileImageUrl ?? "" - return imageService.tryDelete(targetPaths: .init(objectKeyList: [deletePath])) + return preSignedUseCase.tryDelete(objectKeyList: [deletePath]) .andThen( - imageService.tryUpload(datas: [.init(filePath: newPath, image: changeImage)]) + preSignedUseCase.tryUpload(presignedURLRequest: [(filePath: newPath, image: changeImage)]) .asObservable() .map { .loadView } ) @@ -198,7 +210,7 @@ final class ProfileEditReactor: Reactor { } else { currentImagePath = nil let deletePath = originProfileData?.profileImageUrl ?? "" - return imageService.tryDelete(targetPaths: .init(objectKeyList: [deletePath])) + return preSignedUseCase.tryDelete(objectKeyList: [deletePath]) .andThen(Observable.just(.loadView)) } } else { @@ -206,7 +218,7 @@ final class ProfileEditReactor: Reactor { } } } - + func editProfile() -> Observable { isChangeImage = false ToastMaker.createToast(message: "내용을 저장했어요") @@ -219,7 +231,7 @@ final class ProfileEditReactor: Reactor { ) .andThen(Observable.just(.loadView)) } - + func loadProfileData() -> Observable { return userAPIUseCase.getMyProfile() .withUnretained(self) @@ -231,27 +243,27 @@ final class ProfileEditReactor: Reactor { return .loadView } } - + func checkNickNameState(text: String?, isActive: Bool) -> NickNameState { guard let text = text, let originNickName = originProfileData?.nickname else { return isActive ? .emptyActive : .empty } if originNickName == text { return isActive ? .myNickNameActive : .myNickName } - + // textEmpty Check if text.isEmpty { return isActive ? .emptyActive : .empty } - + // kor and end Check let pattern = "^[가-힣a-zA-Z\\s]+$" // 허용하는 문자만 검사 - let regex = try! NSRegularExpression(pattern: pattern) + guard let regex = try? NSRegularExpression(pattern: pattern) else { return .empty } let range = NSRange(location: 0, length: text.utf16.count) if regex.firstMatch(in: text, options: [], range: range) == nil { return isActive ? .korAndEngActive : .korAndEng } - + // textLength Check - + if text.count < 2 { return isActive ? .shortLengthActive : .shortLength } if text.count > 10 { return isActive ? .longLengthActive : .longLength } return isActive ? .checkActive : .check } - + func checkIntroState(text: String?, isActive: Bool) -> IntroState { guard let text = text else { return isActive ? .emptyActive : .empty } if text.isEmpty { return isActive ? .emptyActive : .empty } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift index aa83db1a..844b3d7c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift @@ -10,31 +10,30 @@ import UIKit import SnapKit final class ProfileEditListButton: UIButton { - + // MARK: - Components let mainTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let subTitleLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + let iconImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -42,19 +41,19 @@ final class ProfileEditListButton: UIButton { // MARK: - SetUp private extension ProfileEditListButton { - + func setUpConstraints() { self.addSubview(mainTitleLabel) mainTitleLabel.snp.makeConstraints { make in make.leading.centerY.equalToSuperview() } - + self.addSubview(iconImageView) iconImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.bottom.trailing.equalToSuperview() } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift index c1faa431..320ed56f 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift @@ -1,30 +1,24 @@ -// -// ProfileEditView.swift -// Poppool -// -// Created by SeoJunYoung on 1/4/25. -// - import UIKit +import DesignSystem + import SnapKit final class ProfileEditView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "프로필 설정", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "프로필 설정", font: .korFont(style: .regular, size: 15)) return view }() let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + private let scrollView: UIScrollView = UIScrollView() private let contentView: UIView = UIView() - + let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 48 @@ -51,10 +45,9 @@ final class ProfileEditView: UIView { view.contentMode = .scaleAspectFit return view }() - + private let nickNameTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "별명") - return label + return PPLabel(style: .regular, fontSize: 13, text: "별명") }() let nickNameTextFieldTrailingView: UIStackView = { let view = UIStackView() @@ -71,12 +64,12 @@ final class ProfileEditView: UIView { let nickNameTextField: UITextField = { let textField = UITextField() textField.placeholder = "별명을 입력해주세요" - textField.font = .KorFont(style: .medium, size: 14) + textField.font = .korFont(style: .medium, size: 14) return textField }() let nickNameClearButton: UIButton = { let button = UIButton() - button.setImage(UIImage(named: "icon_clearButton"), for: .normal) + button.setImage(UIImage(named: "icon_clear_button"), for: .normal) button.isHidden = true return button }() @@ -98,7 +91,7 @@ final class ProfileEditView: UIView { let attributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13), // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g1000 // 텍스트 색상 ] @@ -106,7 +99,7 @@ final class ProfileEditView: UIView { let disabledAttributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13), // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g300 // 텍스트 색상 ] @@ -116,10 +109,9 @@ final class ProfileEditView: UIView { button.setAttributedTitle(disabledAttributedTitle, for: .disabled) return button }() - + private let introTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "자기소개") - return label + return PPLabel(style: .regular, fontSize: 13, text: "자기소개") }() let introTextTrailingView: UIView = { let view = UIView() @@ -133,7 +125,7 @@ final class ProfileEditView: UIView { let view = UITextView() view.textContainerInset = .zero view.contentInset = .zero - view.font = .KorFont(style: .medium, size: 14) + view.font = .korFont(style: .medium, size: 14) return view }() let introTextCountLabel: PPLabel = { @@ -143,38 +135,36 @@ final class ProfileEditView: UIView { return label }() let introDescriptionLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 12) - return label + return PPLabel(style: .medium, fontSize: 12) }() let introPlaceHolderLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "자기소개를 입력해주세요") label.textColor = .g200 return label }() - + private let customInfoTitlelabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16, text: "맞춤정보") - return label + return PPLabel(style: .bold, fontSize: 16, text: "맞춤정보") }() - + let categoryButton: ProfileEditListButton = { let button = ProfileEditListButton() - button.mainTitleLabel.setLineHeightText(text: "관심 카테고리", font: .KorFont(style: .regular, size: 15)) + button.mainTitleLabel.setLineHeightText(text: "관심 카테고리", font: .korFont(style: .regular, size: 15)) return button }() - + let infoButton: ProfileEditListButton = { let button = ProfileEditListButton() - button.mainTitleLabel.setLineHeightText(text: "사용자 정보", font: .KorFont(style: .regular, size: 15)) + button.mainTitleLabel.setLineHeightText(text: "사용자 정보", font: .korFont(style: .regular, size: 15)) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -182,7 +172,7 @@ final class ProfileEditView: UIView { // MARK: - SetUp private extension ProfileEditView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in @@ -209,7 +199,7 @@ private extension ProfileEditView { setUpIntroView() setUpCustomInfoView() } - + func setUpProfileImageView() { contentView.addSubview(profileImageView) profileImageView.snp.makeConstraints { make in @@ -228,7 +218,7 @@ private extension ProfileEditView { make.center.equalToSuperview() } } - + func setUpNickNameView() { contentView.addSubview(nickNameTitleLabel) nickNameTitleLabel.snp.makeConstraints { make in @@ -261,7 +251,7 @@ private extension ProfileEditView { make.trailing.equalToSuperview().inset(24) } } - + func setUpIntroView() { contentView.addSubview(introTitleLabel) introTitleLabel.snp.makeConstraints { make in @@ -294,21 +284,21 @@ private extension ProfileEditView { make.leading.equalToSuperview().inset(20) } } - + func setUpCustomInfoView() { contentView.addSubview(customInfoTitlelabel) customInfoTitlelabel.snp.makeConstraints { make in make.top.equalTo(introTextTrailingView.snp.bottom).offset(27) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(categoryButton) categoryButton.snp.makeConstraints { make in make.top.equalTo(customInfoTitlelabel.snp.bottom).offset(32) make.leading.equalToSuperview().inset(22) make.trailing.equalToSuperview().inset(20) } - + contentView.addSubview(infoButton) infoButton.snp.makeConstraints { make in make.top.equalTo(categoryButton.snp.bottom).offset(32) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift index a21e7549..561ece27 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift @@ -1,28 +1,23 @@ -// -// MyPageRecentController.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageRecentController: BaseViewController, View { - + typealias Reactor = MyPageRecentReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageRecentView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -32,7 +27,7 @@ extension MyPageRecentController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,11 +45,11 @@ private extension MyPageRecentController { mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( DetailSimilarSectionCell.self, forCellWithReuseIdentifier: DetailSimilarSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers @@ -74,7 +69,7 @@ extension MyPageRecentController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -82,7 +77,7 @@ extension MyPageRecentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -90,13 +85,13 @@ extension MyPageRecentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.sections = state.sections if state.isReloadView { owner.mainView.contentCollectionView.reloadData() } - + } .disposed(by: disposeBag) } @@ -107,11 +102,11 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -120,7 +115,7 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData guard let reactor = reactor else { return cell } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -129,7 +124,7 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData reactor?.action.onNext(.changePage) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift index d7d85365..021747b7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift @@ -1,18 +1,15 @@ -// -// MyPageRecentReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageRecentReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,30 +17,30 @@ final class MyPageRecentReactor: Reactor { case backButtonTapped(controller: BaseViewController) case cellTapped(controller: BaseViewController, row: Int) } - + enum Mutation { case loadView case skip case moveToRecentScene(controller: BaseViewController) case moveToDetailScene(controller: BaseViewController, row: Int) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var isLoading: Bool = false private var totalPage: Int32 = 0 private var currentPage: Int32 = 0 private var size: Int32 = 100 - - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + + private let userAPIUseCase: UserAPIUseCase + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -57,16 +54,17 @@ final class MyPageRecentReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var countSection = CommentListTitleSection(inputDataList: []) private var listSection = RecentPopUpSection(inputDataList: []) private var spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -111,7 +109,7 @@ final class MyPageRecentReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -125,12 +123,18 @@ final class MyPageRecentReactor: Reactor { controller.navigationController?.popViewController(animated: true) case .moveToDetailScene(let controller, let row): let nextController = DetailController() - nextController.reactor = DetailReactor(popUpID: listSection.inputDataList[row].id) + nextController.reactor = DetailReactor( + popUpID: listSection.inputDataList[row].id, + userAPIUseCase: userAPIUseCase, + popUpAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + commentAPIUseCase: DIContainer.resolve(CommentAPIUseCase.self), + preSignedUseCase: DIContainer.resolve(PreSignedUseCase.self) + ) controller.navigationController?.pushViewController(nextController, animated: true) } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift index 0f585389..52d15d11 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift @@ -1,23 +1,18 @@ -// -// MyPageRecentView.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem + import SnapKit final class MyPageRecentView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "최근 본 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "최근 본 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -28,7 +23,7 @@ final class MyPageRecentView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -36,13 +31,13 @@ final class MyPageRecentView: UIView { // MARK: - SetUp private extension MyPageRecentView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift index 70afc695..23f0db9e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift @@ -1,26 +1,21 @@ -// -// RecentPopUpSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/14/25. -// - import UIKit +import DesignSystem + import RxSwift struct RecentPopUpSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailSimilarSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 8) / 2), @@ -38,8 +33,7 @@ struct RecentPopUpSection: Sectionable { let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 12 - + return section } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift index fbea50cf..4667c827 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift @@ -1,30 +1,25 @@ -// -// MyPageTermsController.swift -// Poppool -// -// Created by SeoJunYoung on 2/4/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageTermsController: BaseViewController, View { - + typealias Reactor = MyPageTermsReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "약관", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "약관", font: .korFont(style: .regular, size: 15)) return view }() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -38,15 +33,15 @@ final class MyPageTermsController: BaseViewController, View { return sections[section].getSection(section: section, env: env) } }() - + private lazy var contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: self.compositionalLayout) view.backgroundColor = .g50 return view }() - + private var sections: [any Sectionable] = [] - + private let cellTapped: PublishSubject = .init() } @@ -56,7 +51,7 @@ extension MyPageTermsController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -76,7 +71,7 @@ private extension MyPageTermsController { make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) } - + contentCollectionView.delegate = self contentCollectionView.dataSource = self contentCollectionView.register(CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers) @@ -92,7 +87,7 @@ extension MyPageTermsController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -100,7 +95,7 @@ extension MyPageTermsController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, indexPath) in @@ -108,7 +103,7 @@ extension MyPageTermsController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -124,19 +119,18 @@ extension MyPageTermsController: UICollectionViewDelegate, UICollectionViewDataS func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { cellTapped.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift index bc4fffc3..a0230242 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift @@ -1,53 +1,49 @@ -// -// MyPageTermsReactor.swift -// Poppool -// -// Created by SeoJunYoung on 2/4/25. -// - import Foundation + +import DesignSystem + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageTermsReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear - case cellTapped(indexPath: IndexPath, controller : BaseViewController) + case cellTapped(indexPath: IndexPath, controller: BaseViewController) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private lazy var countSection = CommentListTitleSection(inputDataList: [.init(count: termsSection.dataCount)]) private var termsSection = MyPageListSection(inputDataList: [ .init(title: "서비스이용약관"), .init(title: "개인정보처리방침"), .init(title: "위치정보 이용약관") ]) - + private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,10 +53,10 @@ final class MyPageTermsReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) case .cellTapped(let indexPath, let controller): return Observable.just(.moveToDetailScene(controller: controller, indexPath: indexPath)) - + } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -75,7 +71,7 @@ final class MyPageTermsReactor: Reactor { } return newState } - + func getSections() -> [any Sectionable] { return [ spacing16Section, @@ -84,7 +80,7 @@ final class MyPageTermsReactor: Reactor { termsSection ] } - + func getContent(index: Int) -> String { if let path = Bundle.main.path(forResource: "Terms", ofType: "plist"), let dict = NSDictionary(contentsOfFile: path) as? [String: String], diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift index cf7f2d28..b08b5514 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift @@ -1,33 +1,29 @@ -// -// WithdrawlCheckModalController.swift -// Poppool -// -// Created by SeoJunYoung on 1/6/25. -// import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class WithdrawlCheckModalController: BaseViewController, View { - + typealias Reactor = WithdrawlCheckModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = WithdrawlCheckModalView() - + init(nickName: String?) { super.init() let title = "\(nickName ?? "")님, 팝풀 서비스를\n정말 탈퇴하시겠어요?" - mainView.titleLabel.setLineHeightText(text: title, font: .KorFont(style: .bold, size: 18), lineHeight: 1.312) + mainView.titleLabel.setLineHeightText(text: title, font: .korFont(style: .bold, size: 18), lineHeight: 1.312) mainView.titleLabel.numberOfLines = 2 } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -58,7 +54,7 @@ extension WithdrawlCheckModalController { .map { Reactor.Action.cancelButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.agreeButton.rx.tap .map { Reactor.Action.appleyButtonTapped } .bind(to: reactor.action) @@ -71,7 +67,7 @@ extension WithdrawlCheckModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(370) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift similarity index 96% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift index 81094df7..51b0c6ad 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift @@ -6,41 +6,41 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class WithdrawlCheckModalReactor: Reactor { - + enum ModalState { case none case cancel case apply } - + // MARK: - Reactor enum Action { case cancelButtonTapped case appleyButtonTapped } - + enum Mutation { case setModalState(state: ModalState) } - + struct State { var state: ModalState = .none } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +50,7 @@ final class WithdrawlCheckModalReactor: Reactor { return Observable.just(.setModalState(state: .cancel)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift similarity index 84% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift index 860c1739..a073fca3 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift @@ -1,57 +1,49 @@ -// -// WithdrawlCheckModalView.swift -// Poppool -// -// Created by SeoJunYoung on 1/6/25. -// - import UIKit +import DesignSystem + import SnapKit final class WithdrawlCheckModalView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18) - return label + return PPLabel(style: .bold, fontSize: 18) }() - + private let trailingView: UIView = { let view = UIView() view.backgroundColor = .g50 view.layer.cornerRadius = 4 return view }() - + let cancelButton: PPButton = { - let button = PPButton(style: .secondary, text: "취소") - return button + return PPButton(style: .secondary, text: "취소") }() - + let agreeButton: PPButton = { - let button = PPButton(style: .primary, text: "동의 후 탈퇴하기") - return button + return PPButton(style: .primary, text: "동의 후 탈퇴하기") }() - + let firstLabel: UILabel = { let text = "서비스 탈퇴 시 회원 전용 서비스 이용이 불가하며 회원 데이터는 일괄 삭제 처리돼요." let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 13), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 13), lineHeight: 1.4) label.numberOfLines = 2 label.textColor = .g600 return label }() - + let secondLabel: UILabel = { let text = "탈퇴 후에는 계정을 다시 살리거나 복구할 수 없어요." let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 13), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 13), lineHeight: 1.4) label.numberOfLines = 2 label.textColor = .g600 return label }() - + let thirdLabel: PPLabel = { let text = "작성하신 코멘트나 댓글 등의 일부 정보는 계속 남아있을 수 있어요. " let label = PPLabel(style: .regular, fontSize: 13, text: text) @@ -59,20 +51,20 @@ final class WithdrawlCheckModalView: UIView { label.textColor = .g600 return label }() - + let textStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -80,20 +72,20 @@ final class WithdrawlCheckModalView: UIView { // MARK: - SetUp private extension WithdrawlCheckModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) @@ -101,7 +93,7 @@ private extension WithdrawlCheckModalView { make.height.equalTo(50) make.width.equalTo(108) } - + self.addSubview(agreeButton) agreeButton.snp.makeConstraints { make in make.bottom.equalToSuperview() @@ -109,13 +101,13 @@ private extension WithdrawlCheckModalView { make.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + trailingView.addSubview(textStackView) textStackView.snp.makeConstraints { make in // make.top.leading.trailing.equalToSuperview().inset(20) make.edges.equalToSuperview().inset(20) } - + textStackView.addArrangedSubview(firstLabel) textStackView.addArrangedSubview(secondLabel) textStackView.addArrangedSubview(thirdLabel) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift index 75be91ad..8fc0cb03 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift @@ -1,22 +1,17 @@ -// -// WithdrawlCompleteController.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class WithdrawlCompleteController: BaseViewController { - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = WithdrawlCompleteView() } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift index 3538fc0a..87ec7dbe 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift @@ -1,23 +1,18 @@ -// -// WithdrawlCompleteView.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit +import DesignSystem + import SnapKit final class WithdrawlCompleteView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_check_fill_blue") return view }() - + private let titleLabel: PPLabel = { let text = "탈퇴 완료\n다음에 또 만나요" let label = PPLabel(style: .bold, fontSize: 20, text: text) @@ -25,28 +20,27 @@ final class WithdrawlCompleteView: UIView { label.textAlignment = .center return label }() - + private let descriptionLabel: PPLabel = { let text = "고객님이 만족하실 수 있는\n팝풀이 되도록 앞으로도 노력할게요 :)" let label = PPLabel(style: .regular, fontSize: 15, text: text) - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 15), lineHeight: 1.5) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 15), lineHeight: 1.5) label.numberOfLines = 2 label.textAlignment = .center label.textColor = .g600 return label }() - + let checkButton: PPButton = { - let button = PPButton(style: .primary, text: "확인") - return button + return PPButton(style: .primary, text: "확인") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -54,7 +48,7 @@ final class WithdrawlCompleteView: UIView { // MARK: - SetUp private extension WithdrawlCompleteView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -62,19 +56,19 @@ private extension WithdrawlCompleteView { make.top.equalToSuperview().inset(84) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(64) make.centerX.equalToSuperview() } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.centerX.equalToSuperview() } - + self.addSubview(checkButton) checkButton.snp.makeConstraints { make in make.leading.bottom.trailing.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift index 862397ec..7b1f5672 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift @@ -1,26 +1,21 @@ -// -// WithdrawlCheckSection.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit +import DesignSystem + import RxSwift struct WithdrawlCheckSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = WithdrawlCheckSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift index 9d308a9d..6438f709 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift @@ -1,45 +1,37 @@ -// -// WithdrawlCheckSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class WithdrawlCheckSectionCell: UICollectionViewCell { - + // MARK: - Components private let checkImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + private let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let textView: UITextView = { let view = UITextView() view.textContainerInset = .zero view.contentInset = .zero - view.font = .KorFont(style: .medium, size: 14) + view.font = .korFont(style: .medium, size: 14) view.backgroundColor = .clear return view }() - + let cellButton: UIButton = UIButton() - + private let trailingView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let textTrailingView: UIView = { let view = UIView() view.layer.cornerRadius = 4 @@ -47,36 +39,36 @@ final class WithdrawlCheckSectionCell: UICollectionViewCell { view.layer.borderColor = UIColor.g100.cgColor view.backgroundColor = .w100 view.clipsToBounds = true - + return view }() - + private let contentStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 10 return view }() - + private let placeHolderLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "탈퇴 이유를 입력해주세요") label.textColor = .g200 return label }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() bind() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -92,7 +84,7 @@ private extension WithdrawlCheckSectionCell { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + cellButton.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalTo(checkImageView.snp.trailing).offset(8) @@ -100,12 +92,12 @@ private extension WithdrawlCheckSectionCell { make.top.bottom.equalToSuperview() make.trailing.equalToSuperview() } - + contentView.addSubview(cellButton) cellButton.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + contentView.addSubview(contentStackView) contentStackView.addArrangedSubview(cellButton) contentStackView.addArrangedSubview(trailingView) @@ -113,31 +105,30 @@ private extension WithdrawlCheckSectionCell { make.edges.equalToSuperview() } - trailingView.snp.makeConstraints { make in make.height.equalTo(120) } - + trailingView.addSubview(textTrailingView) textTrailingView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.trailing.equalToSuperview() make.top.bottom.equalToSuperview() } - + textTrailingView.addSubview(textView) textView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(70) } - + textTrailingView.addSubview(placeHolderLabel) placeHolderLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(16) make.leading.equalToSuperview().inset(20) } } - + func bind() { textView.rx.text .withUnretained(self) @@ -156,12 +147,12 @@ extension WithdrawlCheckSectionCell: Inputable { var id: Int64 var text: String? } - + func injection(with input: Input) { let image = input.isSelected ? UIImage(named: "icon_check_fill") : UIImage(named: "icon_check") checkImageView.image = image let title = input.title ?? "" - titleLabel.setLineHeightText(text: title, font: .KorFont(style: .regular, size: 14)) + titleLabel.setLineHeightText(text: title, font: .korFont(style: .regular, size: 14)) bind() if input.isSelected { if title == "기타" { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift index 7692c604..1f830fb4 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift @@ -1,58 +1,52 @@ -// -// WithdrawlReasonView.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit +import DesignSystem + import SnapKit final class WithdrawlReasonView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "회원 탈퇴", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "회원 탈퇴", font: .korFont(style: .regular, size: 15)) return view }() - + private let titleLabel: UILabel = { let text = "탈퇴하려는 이유가\n무엇인가요?" let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .bold, size: 20), lineHeight: 1.312) + label.setLineHeightText(text: text, font: .korFont(style: .bold, size: 20), lineHeight: 1.312) label.numberOfLines = 2 return label }() - + private let descriptionLabel: PPLabel = { let text = "알려주시는 내용을 참고해 더 나은 팝풀을\n만들어볼게요." let label = PPLabel(style: .regular, fontSize: 15, text: text) label.textColor = .g600 label.numberOfLines = 2 - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 15), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 15), lineHeight: 1.4) return label }() - + let skipButton: PPButton = { let button = PPButton(style: .secondary, text: "건너뛰기") button.setBackgroundColor(.w100, for: .normal) return button }() - + let checkButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -63,7 +57,7 @@ final class WithdrawlReasonView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -71,34 +65,34 @@ final class WithdrawlReasonView: UIView { // MARK: - SetUp private extension WithdrawlReasonView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(64) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + buttonStackView.addArrangedSubview(skipButton) buttonStackView.addArrangedSubview(checkButton) - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(descriptionLabel.snp.bottom).offset(48) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift index b584ab6b..e469604a 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift @@ -1,28 +1,23 @@ -// -// WithdrawlReasonController.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit -import SnapKit -import RxCocoa -import RxSwift +import DesignSystem + import ReactorKit -import RxKeyboard +import RxCocoa import RxGesture +import RxKeyboard +import RxSwift +import SnapKit final class WithdrawlReasonController: BaseViewController, View { - + typealias Reactor = WithdrawlReasonReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = WithdrawlReasonView() - + private var sections: [any Sectionable] = [] } @@ -32,7 +27,7 @@ extension WithdrawlReasonController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,12 +45,12 @@ private extension WithdrawlReasonController { mainView.contentCollectionView.register( WithdrawlCheckSectionCell.self, forCellWithReuseIdentifier: WithdrawlCheckSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers ) - + view.backgroundColor = .g50 view.addSubview(mainView) mainView.snp.makeConstraints { make in @@ -73,7 +68,7 @@ extension WithdrawlReasonController { owner.view.endEditing(true) } .disposed(by: disposeBag) - + RxKeyboard.instance.visibleHeight .drive { [weak self] height in if height > 0 { @@ -93,7 +88,7 @@ extension WithdrawlReasonController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -101,7 +96,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.checkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -109,7 +104,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.skipButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -117,7 +112,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -136,24 +131,24 @@ extension WithdrawlReasonController: UICollectionViewDelegate, UICollectionViewD func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? WithdrawlCheckSectionCell { cell.cellButton.rx.tap .map { Reactor.Action.cellTapped(row: indexPath.row) } .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.textView.rx.text .map { Reactor.Action.etcTextInput(text: $0)} .bind(to: reactor.action) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift index 29943ce2..a27dc32c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift @@ -1,18 +1,15 @@ -// -// WithdrawlReasonReactor.swift -// Poppool -// -// Created by SeoJunYoung on 1/7/25. -// - import UIKit +import DesignSystem +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class WithdrawlReasonReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -22,25 +19,25 @@ final class WithdrawlReasonReactor: Reactor { case skipButtonTapped(controller: BaseViewController) case checkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) case none case moveToCompleteScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var buttonIsEnabled: Bool = false var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -54,17 +51,18 @@ final class WithdrawlReasonReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var reasonSection = WithdrawlCheckSection(inputDataList: []) private var spacing156Section = SpacingSection(inputDataList: [.init(spacing: 156)]) - private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - private let keyChainService = KeyChainService() + private let userAPIUseCase: UserAPIUseCase + @Dependency private var keyChainService: KeyChainService private let userDefaultService = UserDefaultService() // MARK: - init - init() { + init(userAPIUseCase: UserAPIUseCase) { + self.userAPIUseCase = userAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -96,7 +94,7 @@ final class WithdrawlReasonReactor: Reactor { .andThen(Observable.just(.moveToCompleteScene(controller: controller))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -125,9 +123,9 @@ final class WithdrawlReasonReactor: Reactor { .disposed(by: disposeBag) controller.navigationController?.pushViewController(nextController, animated: true) } - + let isEmpty = reasonSection.inputDataList.filter { $0.isSelected == true }.isEmpty - + if let etc = reasonSection.inputDataList.filter({ $0.title == "기타" }).first { if etc.isSelected { if etc.text?.isEmpty ?? true { @@ -143,7 +141,7 @@ final class WithdrawlReasonReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ reasonSection, diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainController.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainController.swift index 6580a573..3086975f 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainController.swift @@ -1,52 +1,53 @@ -// -// SignUpMainController.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit +import DesignSystem +import DomainInterface +import Infrastructure + +import Pageboy +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import Pageboy +import SnapKit import Tabman final class SignUpMainController: BaseTabmanController, View { - + typealias Reactor = SignUpMainReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SignUpMainView() - + var step1Controller: SignUpStep1Controller = { let controller = SignUpStep1Controller() controller.reactor = SignUpStep1Reactor() return controller }() - + var step2Controller: SignUpStep2Controller = { let controller = SignUpStep2Controller() - controller.reactor = SignUpStep2Reactor() + controller.reactor = SignUpStep2Reactor( + signUpAPIUseCase: DIContainer.resolve(SignUpAPIUseCase.self) + ) return controller }() - + var step3Controller: SignUpStep3Controller = { let controller = SignUpStep3Controller() - controller.reactor = SignUpStep3Reactor() + controller.reactor = SignUpStep3Reactor( + signUpAPIUseCase: DIContainer.resolve(SignUpAPIUseCase.self) + ) return controller }() - + var step4Controller: SignUpStep4Controller = { let controller = SignUpStep4Controller() controller.reactor = SignUpStep4Reactor() return controller }() - + lazy var controllers = [ step1Controller, step2Controller, @@ -78,7 +79,7 @@ private extension SignUpMainController { // MARK: - Methods extension SignUpMainController { func bind(reactor: Reactor) { - + // 취소버튼 이벤트 mainView.headerView.cancelButton.rx.tap .withUnretained(self) @@ -87,7 +88,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // 뒤로가기 버튼 mainView.headerView.backButton.rx.tap .withUnretained(self) @@ -97,7 +98,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step1 button tap 이벤트 step1Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -107,7 +108,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step2 button tap 이벤트 step2Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -117,7 +118,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 button tap 이벤트 step3Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -127,7 +128,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 Skip button tap 이벤트 step3Controller.mainView.skipButton.rx.tap .withUnretained(self) @@ -137,7 +138,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 button tap 이벤트 step4Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -148,7 +149,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 Skip button tap 이벤트 step4Controller.mainView.skipButton.rx.tap .withUnretained(self) @@ -160,7 +161,6 @@ extension SignUpMainController { .bind(to: reactor.action) .disposed(by: disposeBag) - // step1 Terms 이벤트 step1Controller.reactor?.state .map({ (state) in @@ -169,7 +169,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step2 nickName 이벤트 step2Controller.reactor?.state .map({ state in @@ -177,7 +177,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 category 이벤트 step3Controller.reactor?.state .map({ state in @@ -185,7 +185,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 gender 이벤트 step4Controller.reactor?.state .map({ state in @@ -205,7 +205,6 @@ extension SignUpMainController { .bind(to: reactor.action) .disposed(by: disposeBag) - reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -225,18 +224,18 @@ extension SignUpMainController: PageboyViewControllerDataSource, TMBarDataSource func barItem(for bar: any Tabman.TMBar, at index: Int) -> any Tabman.TMBarItemable { return TMBarItem(title: "") } - + func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { return controllers.count } - + func viewController( for pageboyViewController: Pageboy.PageboyViewController, at index: Pageboy.PageboyViewController.PageIndex ) -> UIViewController? { return controllers[index] } - + func defaultPage( for pageboyViewController: Pageboy.PageboyViewController ) -> Pageboy.PageboyViewController.Page? { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift index 238105b7..3c5bafe2 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift @@ -1,16 +1,13 @@ -// -// SignUpMainReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// +import DesignSystem +import DomainInterface +import Infrastructure import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpMainReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped(controller: BaseTabmanController) @@ -23,11 +20,11 @@ final class SignUpMainReactor: Reactor { case step4SkipButtonTapped(controller: BaseTabmanController) case changeTerms(isMarketingAgree: Bool) case changeNickName(nickName: String?) - case changeCategory(categorys: [Int64], categoryTitles: [String], categoryIDList: [Int64]) + case changeCategory(categorys: [Int], categoryTitles: [String], categoryIDList: [Int]) case changeGender(gender: String?) case changeAge(age: Int?) } - + enum Mutation { case moveToLoginScene(controller: BaseTabmanController) case increasePageIndex(controller: BaseTabmanController, currentIndex: Int) @@ -37,40 +34,45 @@ final class SignUpMainReactor: Reactor { case moveToCompleteScene(controller: BaseTabmanController) case setTerms(isMarketingAgree: Bool) case setNickName(nickName: String?) - case setCategory(categorys: [Int64], categoryTitles: [String], categoryIDList: [Int64]) + case setCategory(categorys: [Int], categoryTitles: [String], categoryIDList: [Int]) case setGender(gender: String?) case setAge(age: Int?) } - + struct State { var currentIndex: Int = 0 var isMarketingAgree: Bool = false var nickName: String? - var categorys: [Int64] = [] + var categorys: [Int] = [] var categoryTitles: [String] = [] var gender: String? = "선택안함" - var categoryIDList: [Int64] = [] + var categoryIDList: [Int] = [] var age: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - - private var signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) + + private let signUpAPIUseCase: SignUpAPIUseCase private let userDefaultService = UserDefaultService() var isFirstResponderCase: Bool - + // MARK: - init - init(isFirstResponderCase: Bool, authrizationCode: String?) { + init( + isFirstResponderCase: Bool, + authrizationCode: String?, + signUpAPIUseCase: SignUpAPIUseCase + ) { self.initialState = State() self.authrizationCode = authrizationCode self.isFirstResponderCase = isFirstResponderCase + self.signUpAPIUseCase = signUpAPIUseCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -105,7 +107,7 @@ final class SignUpMainReactor: Reactor { ]) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -121,9 +123,7 @@ final class SignUpMainReactor: Reactor { guard let socialType = userDefaultService.fetch(key: "socialType"), let nickName = newState.nickName, let gender = newState.gender else { return newState } - - signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - + signUpAPIUseCase.trySignUp( nickName: nickName, gender: gender, @@ -146,7 +146,7 @@ final class SignUpMainReactor: Reactor { ToastMaker.createToast(message: "회원가입 실패:\(error.localizedDescription)") } .disposed(by: disposeBag) - + case .skipStep3(let controller, let currentIndex): if newState.categoryIDList.count >= 5 { newState.categorys = Array(newState.categoryIDList.shuffled().prefix(5)) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift index e3b9d5c4..ffe88df1 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift @@ -1,27 +1,22 @@ -// -// SignUpMainView.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpMainView: UIView { - + // MARK: - Components let headerView: PPCancelHeaderView = PPCancelHeaderView() - + let progressIndicator: PPProgressIndicator = PPProgressIndicator(totalStep: 4, startPoint: 1) - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -29,7 +24,7 @@ final class SignUpMainView: UIView { // MARK: - SetUp private extension SignUpMainView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift index f0ab226f..c76da8a8 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift @@ -1,24 +1,19 @@ -// -// SignUpCompleteController.swift -// Poppool -// -// Created by SeoJunYoung on 11/27/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpCompleteController: BaseViewController, View { - + typealias Reactor = SignUpCompleteReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SignUpCompleteView() } @@ -43,7 +38,7 @@ private extension SignUpCompleteController { // MARK: - Methods extension SignUpCompleteController { func bind(reactor: Reactor) { - + mainView.bottomButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -51,7 +46,7 @@ extension SignUpCompleteController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -65,17 +60,17 @@ extension SignUpCompleteController { let categoryString = state.categoryTitles.enumerated() .map { "#\($0.element)" } .joined(separator: ", ") - + let attributedText = NSMutableAttributedString( string: categoryString, attributes: [ - .font: UIFont.KorFont(style: .bold, size: 15)!, + .font: UIFont.korFont(style: .bold, size: 15), .foregroundColor: UIColor.g600, NSAttributedString.Key.paragraphStyle: paragraphStyle ] ) attributedText.append(NSAttributedString(string: "와 연관된 팝업스토어 정보를 안내해드릴게요.", attributes: [ - .font: UIFont.KorFont(style: .regular, size: 15)!, + .font: UIFont.korFont(style: .regular, size: 15), .foregroundColor: UIColor.g600, NSAttributedString.Key.paragraphStyle: paragraphStyle ])) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift index 7c5e14a1..d26997ba 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift @@ -1,32 +1,27 @@ -// -// SignUpCompleteReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/27/24. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpCompleteReactor: Reactor { - + // MARK: - Reactor enum Action { case completeButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToHomeScene(controller: BaseViewController) } - + struct State { var nickName: String var categoryTitles: [String] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var isFirstResponderCase: Bool @@ -35,7 +30,7 @@ final class SignUpCompleteReactor: Reactor { self.initialState = State(nickName: nickName, categoryTitles: categoryTitles) self.isFirstResponderCase = isFirstResponderCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -43,7 +38,7 @@ final class SignUpCompleteReactor: Reactor { return Observable.just(.moveToHomeScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToHomeScene(let controller): diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift similarity index 79% rename from Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift index 73b5739c..b89f2ffa 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift @@ -1,56 +1,47 @@ -// -// SignUpCompleteView.swift -// Poppool -// -// Created by SeoJunYoung on 11/27/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpCompleteView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_signUp_complete") return view }() - + private let titleTopLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "가입완료") - return label + return PPLabel(style: .bold, fontSize: 20, text: "가입완료") }() - + private let titleMiddleStackView: UIStackView = { - let view = UIStackView() - return view + return UIStackView() }() - + let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.textColor = .blu500 return label }() - + private let nickNameSubLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "님의") - return label + return PPLabel(style: .bold, fontSize: 20, text: "님의") }() - + private let titleBottomLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "피드를 확인해보세요") - return label + return PPLabel(style: .bold, fontSize: 20, text: "피드를 확인해보세요") }() - + private let titleStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.alignment = .center return view }() - + let descriptionLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 15) label.textColor = .g600 @@ -59,18 +50,17 @@ final class SignUpCompleteView: UIView { label.textAlignment = .center return label }() - + let bottomButton: PPButton = { - let button = PPButton(style: .primary, text: "바로가기") - return button + return PPButton(style: .primary, text: "바로가기") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -78,7 +68,7 @@ final class SignUpCompleteView: UIView { // MARK: - SetUp private extension SignUpCompleteView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -86,27 +76,27 @@ private extension SignUpCompleteView { make.size.equalTo(80) make.top.equalToSuperview().inset(124) } - + titleMiddleStackView.addArrangedSubview(nickNameLabel) titleMiddleStackView.addArrangedSubview(nickNameSubLabel) - + titleStackView.addArrangedSubview(titleTopLabel) titleStackView.addArrangedSubview(titleMiddleStackView) titleStackView.addArrangedSubview(titleBottomLabel) - + self.addSubview(titleStackView) titleStackView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(imageView.snp.bottom).offset(32) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleStackView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) - + } - + self.addSubview(bottomButton) bottomButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift index 80cc81d2..ad6ab703 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift @@ -1,24 +1,19 @@ -// -// SignUpStep1Controller.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpStep1Controller: BaseViewController, View { - + typealias Reactor = SignUpStep1Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep1View() } @@ -44,13 +39,13 @@ private extension SignUpStep1Controller { // MARK: - Methods extension SignUpStep1Controller { func bind(reactor: Reactor) { - + // totalButton Tap 이벤트 mainView.totalButton.button.rx.tap .map { Reactor.Action.totalButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + // terms Tap 이벤트 mainView.terms1Button.button.rx.tap .map { Reactor.Action.termsButtonTapped(index: 1)} @@ -68,7 +63,7 @@ extension SignUpStep1Controller { .map { Reactor.Action.termsButtonTapped(index: 4)} .bind(to: reactor.action) .disposed(by: disposeBag) - + // terms Detail Button 이벤트 mainView.terms1Button.righticonButton.rx.tap .withUnretained(self) @@ -98,18 +93,18 @@ extension SignUpStep1Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - + // selectedIndex가 4일 경우 전체 선택 버튼 활성화 및 비활성화 if state.selectedIndex.count == 4 { owner.mainView.totalButton.isSelected.accept(true) } else { owner.mainView.totalButton.isSelected.accept(false) } - + // 현 selectedIndex에 따라 view 변경 let termsViews = [ owner.mainView.terms1Button, @@ -121,7 +116,7 @@ extension SignUpStep1Controller { let isSelected = state.selectedIndex.contains(index + 1) view.isSelected.accept(isSelected) } - + // selectedIndex가 1,2,3을 포함할 시 completeButton 활성화 if state.selectedIndex.contains(1) && state.selectedIndex.contains(2) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift similarity index 95% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift index 467e416f..788b5e0b 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift @@ -1,44 +1,40 @@ -// -// SignUpStep1Reactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import Foundation + +import DesignSystem + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep1Reactor: Reactor { - + // MARK: - Reactor enum Action { case totalButtonTapped case termsButtonTapped(index: Int) case termsRightButtonTapped(index: Int, controller: BaseViewController) } - + enum Mutation { case setTotalSelected case setSelectedIndex(index: Int) case moveToTermsDetailScene(index: Int, controller: BaseViewController) } - + struct State { var selectedIndex: [Int] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +46,7 @@ final class SignUpStep1Reactor: Reactor { return Observable.just(.moveToTermsDetailScene(index: index, controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -75,7 +71,7 @@ final class SignUpStep1Reactor: Reactor { } return newState } - + func getTitle(index: Int) -> String { if index == 1 { return "[필수] 이용약관" @@ -85,7 +81,7 @@ final class SignUpStep1Reactor: Reactor { return "[선택] 위치정보 이용약관" } } - + func getContent(index: Int) -> String { if let path = Bundle.main.path(forResource: "Terms", ofType: "plist"), let dict = NSDictionary(contentsOfFile: path) as? [String: String], diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift similarity index 86% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift index 0fead2c6..064b423f 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift @@ -1,39 +1,32 @@ -// -// SignUpCheckBoxButton.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit -import RxSwift +import DesignSystem + import RxCocoa +import RxSwift +import SnapKit final class SignUpCheckBoxButton: UIView { - + // MARK: - Components private let checkImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_checkBox") return view }() - + private let buttonLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 15, text: "약관에 모두 동의할게요") - return label + return PPLabel(style: .bold, fontSize: 15, text: "약관에 모두 동의할게요") }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let disposeBag = DisposeBag() - + let isSelected: BehaviorRelay = .init(value: false) - + // MARK: - init init() { super.init(frame: .zero) @@ -43,7 +36,7 @@ final class SignUpCheckBoxButton: UIView { self.layer.cornerRadius = 4 self.clipsToBounds = true } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -51,7 +44,7 @@ final class SignUpCheckBoxButton: UIView { // MARK: - SetUp private extension SignUpCheckBoxButton { - + func setUpConstraints() { self.addSubview(checkImageView) checkImageView.snp.makeConstraints { make in @@ -59,20 +52,20 @@ private extension SignUpCheckBoxButton { make.centerY.equalToSuperview() make.leading.equalTo(20) } - + self.addSubview(buttonLabel) buttonLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(checkImageView.snp.trailing).offset(8) make.trailing.equalToSuperview().inset(20) } - + self.addSubview(button) button.snp.makeConstraints { make in make.edges.equalToSuperview() } } - + func bind() { button.rx.tap .withUnretained(self) @@ -80,7 +73,7 @@ private extension SignUpCheckBoxButton { owner.isSelected.accept(!owner.isSelected.value) } .disposed(by: disposeBag) - + isSelected .withUnretained(self) .subscribe { (owner, isSelected) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift index abd7ef42..c7b46b7e 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift @@ -1,51 +1,44 @@ -// -// SignUpStep1View.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpStep1View: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20, text: "서비스 이용을 위한\n약관을 확인해주세요") label.numberOfLines = 0 return label }() - + let totalButton: SignUpCheckBoxButton = { - let button = SignUpCheckBoxButton() - return button + return SignUpCheckBoxButton() }() - + let terms1Button: SignUpTermsView = SignUpTermsView(title: "[필수] 이용약관") let terms2Button: SignUpTermsView = SignUpTermsView(title: "[필수] 개인정보 수집 및 이용") let terms3Button: SignUpTermsView = SignUpTermsView(title: "[필수] 만 14세 이상") let terms4Button: SignUpTermsView = SignUpTermsView(title: "[선택] 위치정보 이용약관") - + let termsStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 16 return view }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,7 +46,7 @@ final class SignUpStep1View: UIView { // MARK: - SetUp private extension SignUpStep1View { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -61,14 +54,14 @@ private extension SignUpStep1View { make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(56) } - + self.addSubview(totalButton) totalButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(49) } - + self.addSubview(termsStackView) termsStackView.snp.makeConstraints { make in make.top.equalTo(totalButton.snp.bottom).offset(36) @@ -78,14 +71,14 @@ private extension SignUpStep1View { termsStackView.addArrangedSubview(terms2Button) termsStackView.addArrangedSubview(terms3Button) termsStackView.addArrangedSubview(terms4Button) - + self.addSubview(completeButton) completeButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) make.bottom.equalToSuperview() make.height.equalTo(52) } - + terms3Button.righticonButton.isHidden = true } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift index 8230c466..03a16047 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift @@ -1,46 +1,40 @@ -// -// SignUpTermsView.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit -import RxSwift +import DesignSystem + import RxCocoa +import RxSwift +import SnapKit final class SignUpTermsView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_check") return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14) label.text = "some" return label }() - + let righticonButton: UIButton = { let view = UIButton() view.setImage(UIImage(named: "icon_right_gray"), for: .normal) return view }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let isSelected: BehaviorRelay = .init(value: false) - + let disposeBag = DisposeBag() - + // MARK: - init init(title: String?) { super.init(frame: .zero) @@ -48,7 +42,7 @@ final class SignUpTermsView: UIView { bind() titleLabel.text = title } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -56,7 +50,7 @@ final class SignUpTermsView: UIView { // MARK: - SetUp private extension SignUpTermsView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -64,27 +58,27 @@ private extension SignUpTermsView { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + self.addSubview(righticonButton) righticonButton.snp.makeConstraints { make in make.size.equalTo(22) make.top.bottom.trailing.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(imageView.snp.trailing).offset(8) make.trailing.equalTo(righticonButton.snp.leading) } - + self.addSubview(button) button.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview() make.trailing.equalTo(righticonButton.snp.leading) } } - + func bind() { button.rx.tap .withUnretained(self) @@ -92,7 +86,7 @@ private extension SignUpTermsView { owner.isSelected.accept(!owner.isSelected.value) } .disposed(by: disposeBag) - + isSelected .withUnretained(self) .subscribe { (owner, isSelected) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/IntroState.swift similarity index 98% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/IntroState.swift index 1828b8a6..ce9e6152 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/IntroState.swift @@ -14,7 +14,7 @@ enum IntroState { case validateActive case longLength case longLengthActive - + var borderColor: UIColor? { switch self { case .empty, .validate: @@ -25,7 +25,7 @@ enum IntroState { return .re500 } } - + var description: String? { switch self { case .empty, .emptyActive, .validate, .validateActive: @@ -34,7 +34,7 @@ enum IntroState { return "최대 30글자까지 입력해주세요" } } - + var textColor: UIColor? { switch self { case .empty, .emptyActive, .validate, .validateActive: @@ -43,7 +43,7 @@ enum IntroState { return .re500 } } - + var textFieldTextColor: UIColor? { switch self { case .longLength, .longLengthActive: @@ -52,7 +52,7 @@ enum IntroState { return .g1000 } } - + var placeHolderIsHidden: Bool { switch self { case .empty, .emptyActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/NickNameState.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/NickNameState.swift index 964d5894..7693833f 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/NickNameState.swift @@ -26,10 +26,10 @@ enum NickNameState { case shortLengthActive case longLength case longLengthActive - + var borderColor: UIColor? { switch self { - case .empty, .duplicated, .validate, .check , .myNickName: + case .empty, .duplicated, .validate, .check, .myNickName: return .g200 case .emptyActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive: return .g1000 @@ -37,7 +37,7 @@ enum NickNameState { return .re500 } } - + var description: String? { switch self { case .empty, .emptyActive: @@ -62,18 +62,18 @@ enum NickNameState { return nil } } - + var textColor: UIColor? { switch self { case .empty, .emptyActive, .check, .checkActive: return .g500 case .length, .lengthActive, .korAndEng, .korAndEngActive, .duplicated, .duplicatedActive, .shortLength, .shortLengthActive, .longLength, .longLengthActive: return .re500 - case .validate, .validateActive , .myNickName ,.myNickNameActive: + case .validate, .validateActive, .myNickName, .myNickNameActive: return .blu500 } } - + var textFieldTextColor: UIColor? { switch self { case .length, .lengthActive, .korAndEng, .korAndEngActive, .duplicated, .duplicatedActive, .shortLength, .shortLengthActive, .longLength, .longLengthActive: @@ -82,25 +82,25 @@ enum NickNameState { return .g1000 } } - + var isHiddenClearButton: Bool { switch self { - case .lengthActive , .korAndEngActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive, .shortLengthActive, .longLengthActive: + case .lengthActive, .korAndEngActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive, .shortLengthActive, .longLengthActive: return false default: return true } } - + var isHiddenCheckButton: Bool { switch self { - case .length , .korAndEng, .duplicated, .validate, .check, .shortLength, .longLength ,.myNickName: + case .length, .korAndEng, .duplicated, .validate, .check, .shortLength, .longLength, .myNickName: return false default: return true } } - + var isShakeAnimation: Bool { switch self { case .lengthActive, .korAndEngActive, .duplicatedActive: @@ -109,7 +109,7 @@ enum NickNameState { return false } } - + var duplicatedCheckButtonIsEnabled: Bool { switch self { case .check, .checkActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift index 3b588360..8aefa684 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift @@ -1,25 +1,20 @@ -// -// SignUpStep2Controller.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit -import RxCocoa -import RxSwift +import DesignSystem + import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class SignUpStep2Controller: BaseViewController, View { - + typealias Reactor = SignUpStep2Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep2View() } @@ -45,29 +40,29 @@ private extension SignUpStep2Controller { // MARK: - Methods extension SignUpStep2Controller { func bind(reactor: Reactor) { - + mainView.rx.tapGesture() .withUnretained(self) .subscribe { (owner, _) in owner.view.endEditing(true) } .disposed(by: disposeBag) - + mainView.textField.rx.text .map { Reactor.Action.inputNickName(text: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.textField.rx.controlEvent(.editingDidBegin) .map { Reactor.Action.beginNickNameInput } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.textField.rx.controlEvent(.editingDidEnd) .map { Reactor.Action.endNickNameInput } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.clearButton.rx.tap .withUnretained(self) .map({ (owner, _) in @@ -76,16 +71,16 @@ extension SignUpStep2Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.duplicatedCheckButton.rx.tap .map { Reactor.Action.duplicatedButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - + // duplicatedButton Active set switch state.nickNameState { case .check, .checkActive: @@ -98,11 +93,11 @@ extension SignUpStep2Controller { owner.mainView.textFieldTrailingView.layer.borderColor = state.nickNameState.borderColor?.cgColor owner.mainView.textDescriptionLabel.text = state.nickNameState.description owner.mainView.textDescriptionLabel.textColor = state.nickNameState.textColor - + // clearButton, Duplicated Button set owner.mainView.duplicatedCheckButton.isHidden = state.nickNameState.isHiddenCheckButton owner.mainView.clearButton.isHidden = state.nickNameState.isHiddenClearButton - + // count Label set if let nickName = state.nickName { owner.mainView.textCountLabel.text = "\(nickName.count) / 10자" @@ -114,8 +109,7 @@ extension SignUpStep2Controller { default: owner.mainView.textCountLabel.textColor = .g500 } - - + // completeButton isActive set switch state.nickNameState { case .validate, .validateActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift index c46fd041..3c700b01 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift @@ -1,18 +1,14 @@ -// -// SignUpStep2Reactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DomainInterface +import Infrastructure + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep2Reactor: Reactor { - + // MARK: - Reactor enum Action { case inputNickName(text: String?) @@ -21,32 +17,35 @@ final class SignUpStep2Reactor: Reactor { case clearButtonTapped case duplicatedButtonTapped } - + enum Mutation { case setNickNameState(text: String?) case setActiveState(isActive: Bool) case setDuplicatedSet(isDuplicated: Bool) case resetNickName } - + struct State { var nickNameState: NickNameState = .empty var isActiveInput: Bool = false var nickName: String? = nil } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) + private let signUpAPIUseCase: SignUpAPIUseCase private var nickName: String? - + // MARK: - init - init() { + init( + signUpAPIUseCase: SignUpAPIUseCase + ) { + self.signUpAPIUseCase = signUpAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -66,7 +65,7 @@ final class SignUpStep2Reactor: Reactor { return Observable.just(.resetNickName) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -78,7 +77,7 @@ final class SignUpStep2Reactor: Reactor { newState.isActiveInput = isActive newState.nickNameState = checkNickNameState(text: newState.nickName, isActive: newState.isActiveInput) case .setDuplicatedSet(let isDuplicated): - newState.nickNameState = isDuplicated + newState.nickNameState = isDuplicated ? newState.isActiveInput ? .duplicatedActive : .duplicated : newState.isActiveInput ? .validateActive : .validate case .resetNickName: @@ -87,24 +86,22 @@ final class SignUpStep2Reactor: Reactor { } return newState } - + func checkNickNameState(text: String?, isActive: Bool) -> NickNameState { guard let text = text else { return isActive ? .emptyActive : .empty } // textEmpty Check if text.isEmpty { return isActive ? .emptyActive : .empty } - - + // textLength Check if text.count < 2 { return isActive ? .shortLengthActive : .shortLength } if text.count > 10 { return isActive ? .longLengthActive : .longLength } - + // kor and end Check let pattern = "^[가-힣a-zA-Z\\s]+$" // 허용하는 문자만 검사 - let regex = try! NSRegularExpression(pattern: pattern) + guard let regex = try? NSRegularExpression(pattern: pattern) else { return .empty } let range = NSRange(location: 0, length: text.utf16.count) if regex.firstMatch(in: text, options: [], range: range) == nil { return isActive ? .korAndEngActive : .korAndEng } - return isActive ? .checkActive : .check } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift index 1cb8caed..9626eac2 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift @@ -1,35 +1,30 @@ -// -// SignUpStep2View.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpStep2View: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20, text: "팝풀에서 사용할\n별명을 설정해볼까요?") label.numberOfLines = 0 return label }() - + private let descriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 15, text: "이후 이 별명으로 팝풀에서 활동할 예정이에요.") label.textColor = .g600 return label }() - + let completeButton: PPButton = { let button = PPButton(style: .primary, text: "확인", disabledText: "다음") button.isEnabled = false return button }() - + let textFieldTrailingView: UIStackView = { let view = UIStackView() view.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) @@ -41,33 +36,33 @@ final class SignUpStep2View: UIView { view.layer.borderWidth = 1 return view }() - + let textField: UITextField = { let textField = UITextField() textField.placeholder = "별명을 입력해주세요" - textField.font = .KorFont(style: .medium, size: 14) + textField.font = .korFont(style: .medium, size: 14) return textField }() - + let clearButton: UIButton = { let button = UIButton() - button.setImage(UIImage(named: "icon_clearButton"), for: .normal) + button.setImage(UIImage(named: "icon_clear_button"), for: .normal) return button }() - + let textDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "temptemp" return label }() - + let textCountLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "0/10자" label.textColor = .g500 return label }() - + let duplicatedCheckButton: UIButton = { let button = UIButton() let title = "중복체크" @@ -75,7 +70,7 @@ final class SignUpStep2View: UIView { let attributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13), // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g1000 // 텍스트 색상 ] @@ -83,7 +78,7 @@ final class SignUpStep2View: UIView { let disabledAttributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13), // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g300 // 텍스트 색상 ] @@ -93,13 +88,13 @@ final class SignUpStep2View: UIView { button.setAttributedTitle(disabledAttributedTitle, for: .disabled) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -107,7 +102,7 @@ final class SignUpStep2View: UIView { // MARK: - SetUp private extension SignUpStep2View { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -115,13 +110,13 @@ private extension SignUpStep2View { make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(56) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(textFieldTrailingView) textFieldTrailingView.snp.makeConstraints { make in make.top.equalTo(descriptionLabel.snp.bottom).offset(48) @@ -134,26 +129,26 @@ private extension SignUpStep2View { clearButton.snp.makeConstraints { make in make.size.equalTo(16) } - + self.addSubview(textDescriptionLabel) textDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(textFieldTrailingView.snp.bottom).offset(6) make.leading.equalToSuperview().inset(24) } - + self.addSubview(textCountLabel) textCountLabel.snp.makeConstraints { make in make.centerY.equalTo(textDescriptionLabel) make.trailing.equalToSuperview().inset(24) } - + self.addSubview(completeButton) completeButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) make.bottom.equalToSuperview() make.height.equalTo(52) } - + textField.snp.makeConstraints { make in make.height.equalTo(52) } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift index 0260f4d3..246c49f3 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift @@ -1,29 +1,24 @@ -// -// SignUpStep3Controller.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit -import RxCocoa -import RxSwift +import DesignSystem + import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class SignUpStep3Controller: BaseViewController, View { - + typealias Reactor = SignUpStep3Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep3View() - + private var sections: [any Sectionable] = [] - + private let selectedTag: PublishSubject = .init() } @@ -63,12 +58,12 @@ extension SignUpStep3Controller { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + selectedTag .map { Reactor.Action.selectedTag(indexPath: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -85,19 +80,18 @@ extension SignUpStep3Controller: UICollectionViewDelegate, UICollectionViewDataS func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedTag.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift index 50aff05f..43ddd997 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift @@ -1,42 +1,38 @@ -// -// SignUpStep3Reactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem +import DomainInterface + import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep3Reactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case selectedTag(indexPath: IndexPath) } - + enum Mutation { case loadView } - + struct State { var sections: [any Sectionable] = [] - var selectedCategory: [Int64] = [] + var selectedCategory: [Int] = [] var selectedCategoryTitle: [String] = [] - var categoryIDList: [Int64] = [] + var categoryIDList: [Int] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - private var cetegoryIDList: [Int64] = [] - + private let signUpAPIUseCase: SignUpAPIUseCase + private var cetegoryIDList: [Int] = [] + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -50,14 +46,17 @@ final class SignUpStep3Reactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var categorySection: TagSection = TagSection(inputDataList: []) - + // MARK: - init - init() { + init( + signUpAPIUseCase: SignUpAPIUseCase + ) { + self.signUpAPIUseCase = signUpAPIUseCase self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -79,14 +78,14 @@ final class SignUpStep3Reactor: Reactor { } else { ToastMaker.createToast(message: "최대 5개까지 선택할 수 있어요") } - + } else { categorySection.inputDataList[indexPath.row].isSelected.toggle() } return Observable.just(.loadView) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -98,7 +97,7 @@ final class SignUpStep3Reactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ categorySection diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift index 5de5d4b5..5388a368 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift @@ -1,16 +1,11 @@ -// -// SignUpStep3View.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpStep3View: UIView { - + // MARK: - Components let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) @@ -18,64 +13,62 @@ final class SignUpStep3View: UIView { label.text = "하이" return label }() - + private let titleTopLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "님에 대해" return label }() - + private let titleBottomLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "조금 더 알려주시겠어요?" return label }() - + private let subTitleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.text = "관심이 있는 카테고리를 선택해주세요" return label }() - + private let subTitleDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "최대 5개까지 선택할 수 있어요." return label }() - + let categoryCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.isScrollEnabled = false return view }() - + let skipButton: PPButton = { - let button = PPButton(style: .secondary, text: "건너뛰기") - return button + return PPButton(style: .secondary, text: "건너뛰기") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "다음", disabledText: "다음") - return button + return PPButton(style: .primary, text: "다음", disabledText: "다음") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func setNickName(nickName: String?) { nickNameLabel.text = nickName } @@ -83,7 +76,7 @@ final class SignUpStep3View: UIView { // MARK: - SetUp private extension SignUpStep3View { - + func setUpConstraints() { self.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in @@ -91,35 +84,35 @@ private extension SignUpStep3View { make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(titleTopLabel) titleTopLabel.snp.makeConstraints { make in make.top.equalTo(nickNameLabel) make.leading.equalTo(nickNameLabel.snp.trailing) make.height.equalTo(28) } - + self.addSubview(titleBottomLabel) titleBottomLabel.snp.makeConstraints { make in make.top.equalTo(titleTopLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleBottomLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(22) } - + self.addSubview(subTitleDescriptionLabel) subTitleDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(subTitleLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(18) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) @@ -127,7 +120,7 @@ private extension SignUpStep3View { } buttonStackView.addArrangedSubview(skipButton) buttonStackView.addArrangedSubview(completeButton) - + self.addSubview(categoryCollectionView) categoryCollectionView.snp.makeConstraints { make in make.top.equalTo(subTitleDescriptionLabel.snp.bottom).offset(36) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift similarity index 90% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift index 58f00723..59afe696 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift @@ -1,26 +1,21 @@ -// -// TagSection.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import RxSwift struct TagSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = TagSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(26), @@ -38,7 +33,7 @@ struct TagSection: Sectionable { let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 16 - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift similarity index 79% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift index 6712e0d5..e4c1f754 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift @@ -1,31 +1,25 @@ -// -// TagSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxSwift +import SnapKit final class TagSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 13) - return label + return PPLabel(style: .medium, fontSize: 13) }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -37,7 +31,7 @@ private extension TagSectionCell { contentView.clipsToBounds = true contentView.layer.cornerRadius = 18 contentView.layer.borderColor = UIColor.g200.cgColor - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(14).priority(.high) @@ -50,21 +44,21 @@ extension TagSectionCell: Inputable { struct Input { var title: String? var isSelected: Bool - var id: Int64? + var id: Int? } - + func injection(with input: Input) { titleLabel.text = input.title if input.isSelected { contentView.backgroundColor = .blu500 contentView.layer.borderWidth = 0 titleLabel.textColor = .w100 - titleLabel.font = . KorFont(style: .medium, size: 13) + titleLabel.font = . korFont(style: .medium, size: 13) } else { contentView.backgroundColor = .clear contentView.layer.borderWidth = 1 titleLabel.textColor = .g400 - titleLabel.font = . KorFont(style: .medium, size: 13) + titleLabel.font = . korFont(style: .medium, size: 13) } } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift similarity index 94% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift index b3bbf5b0..4024ecea 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift @@ -1,25 +1,20 @@ -// -// AgeSelectedController.swift -// Poppool -// -// Created by SeoJunYoung on 11/26/24. -// - import UIKit -import SnapKit +import DesignSystem + +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class AgeSelectedController: BaseViewController, View { - + typealias Reactor = AgeSelectedReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = AgeSelectedView() } @@ -51,7 +46,7 @@ extension AgeSelectedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.completeButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -60,7 +55,7 @@ extension AgeSelectedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift index 8e1e2aef..907e51d1 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift @@ -1,41 +1,36 @@ -// -// AgeSelectedReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/26/24. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class AgeSelectedReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped(controller: BaseViewController) case completeButtonTapped(selectedAge: Int, controller: BaseViewController) } - + enum Mutation { case setSelectedAge(selectedAge: Int, controller: BaseViewController) case moveToRecentScene(controller: BaseViewController) } - + struct State { var selectedAge: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(age: Int?) { self.initialState = State(selectedAge: age) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -45,7 +40,7 @@ final class AgeSelectedReactor: Reactor { return Observable.just(.setSelectedAge(selectedAge: selectedAge, controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift similarity index 75% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift index 6732f881..fbb5cabe 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift @@ -1,38 +1,29 @@ -// -// AgeSelectedView.swift -// Poppool -// -// Created by SeoJunYoung on 11/26/24. -// - import UIKit +import DesignSystem + import SnapKit final class AgeSelectedView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "나이를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "나이를 선택해주세요") }() - + let picker: PPPicker = { let ageRange = (0...100).map { "\($0)세"} - let picker = PPPicker(components: ageRange) - return picker + return PPPicker(components: ageRange) }() - + let cancelButton: PPButton = { - let button = PPButton(style: .secondary, text: "취소") - return button + return PPButton(style: .secondary, text: "취소") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually @@ -44,7 +35,7 @@ final class AgeSelectedView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -52,21 +43,21 @@ final class AgeSelectedView: UIView { // MARK: - SetUp private extension AgeSelectedView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(picker) picker.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(208) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.top.equalTo(picker.snp.bottom).offset(24) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift index 17d64901..9294c172 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift @@ -1,24 +1,19 @@ -// -// SignUpStep4Controller.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit +import DesignSystem + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpStep4Controller: BaseViewController, View { - + typealias Reactor = SignUpStep4Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep4View() } @@ -50,7 +45,7 @@ extension SignUpStep4Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.ageSelectedButton.button.rx.tap .withUnretained(self) .map { (owner, _) in @@ -58,7 +53,7 @@ extension SignUpStep4Controller { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift similarity index 93% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift index 50d87d2f..8f848425 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift @@ -1,44 +1,39 @@ -// -// SignUpStep4Reactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// +import DesignSystem import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep4Reactor: Reactor { - + // MARK: - Reactor enum Action { case selectedGender(index: Int) case ageSelectedButtonTapped(controller: BaseViewController) case ageSelected(age: Int?) } - + enum Mutation { case setGender(index: Int) case setAge(age: Int?) case moveToAgeSelectedScene(controller: BaseViewController) } - + struct State { var selectedGenderIndex: Int = 2 var age: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +45,7 @@ final class SignUpStep4Reactor: Reactor { return Observable.just(.moveToAgeSelectedScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift similarity index 92% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift index 55ec1d8a..ac5bf151 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift @@ -1,47 +1,42 @@ -// -// AgeSelectedButton.swift -// Poppool -// -// Created by SeoJunYoung on 11/26/24. -// - import UIKit +import DesignSystem + import SnapKit final class AgeSelectedButton: UIView { - + // MARK: - Components private let contentStackView: UIStackView = { let view = UIStackView() view.alignment = .center return view }() - + private let defaultLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "나이를 선택해주세요") label.textColor = .g400 return label }() - + private let rightImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + private let ageTitleLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11, text: "나이") label.textColor = .g400 return label }() - + private let ageLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "") label.textColor = .g1000 return label }() - + private let verticalStackView: UIStackView = { let view = UIStackView() view.axis = .vertical @@ -50,18 +45,17 @@ final class AgeSelectedButton: UIView { view.spacing = 4 return view }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -69,12 +63,12 @@ final class AgeSelectedButton: UIView { // MARK: - SetUp private extension AgeSelectedButton { - + func setUpConstraints() { self.layer.borderWidth = 1 self.layer.cornerRadius = 4 self.layer.borderColor = UIColor.g200.cgColor - + self.addSubview(contentStackView) contentStackView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) @@ -85,11 +79,11 @@ private extension AgeSelectedButton { } verticalStackView.addArrangedSubview(ageTitleLabel) verticalStackView.addArrangedSubview(ageLabel) - + contentStackView.addArrangedSubview(defaultLabel) contentStackView.addArrangedSubview(verticalStackView) contentStackView.addArrangedSubview(rightImageView) - + self.addSubview(button) button.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -101,7 +95,7 @@ extension AgeSelectedButton: Inputable { struct Input { var age: Int? } - + func injection(with input: Input) { if let age = input.age { verticalStackView.isHidden = false diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift similarity index 89% rename from Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift index c9e19f9c..a7d8df1d 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift @@ -1,16 +1,11 @@ -// -// SignUpStep4View.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class SignUpStep4View: UIView { - + // MARK: - Components let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) @@ -18,84 +13,80 @@ final class SignUpStep4View: UIView { label.text = "하이" return label }() - + private let titleTopLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "님에 대해" return label }() - + private let titleBottomLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "조금 더 알려주시겠어요?" return label }() - + private let subTitleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.text = "해당되시는 성별 / 나이대를 알려주세요" return label }() - + private let subTitleDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "가장 잘 맞는 팝업스토어를 소개해드릴게요." return label }() - + private let genderTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.text = "성별" return label }() - + let genderSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl( + return PPSegmentedControl( type: .base, segments: ["남성", "여성", "선택안함"], selectedSegmentIndex: 2 ) - return control }() - + private let ageTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.text = "나이" return label }() - + let ageSelectedButton: AgeSelectedButton = { - let button = AgeSelectedButton() - return button + return AgeSelectedButton() }() - + let skipButton: PPButton = { - let button = PPButton(style: .secondary, text: "건너뛰기") - return button + return PPButton(style: .secondary, text: "건너뛰기") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func setNickName(nickName: String?) { nickNameLabel.text = nickName } @@ -103,7 +94,7 @@ final class SignUpStep4View: UIView { // MARK: - SetUp private extension SignUpStep4View { - + func setUpConstraints() { self.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in @@ -111,62 +102,62 @@ private extension SignUpStep4View { make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(titleTopLabel) titleTopLabel.snp.makeConstraints { make in make.top.equalTo(nickNameLabel) make.leading.equalTo(nickNameLabel.snp.trailing) make.height.equalTo(28) } - + self.addSubview(titleBottomLabel) titleBottomLabel.snp.makeConstraints { make in make.top.equalTo(titleTopLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleBottomLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(22) } - + self.addSubview(subTitleDescriptionLabel) subTitleDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(subTitleLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(18) } - + self.addSubview(genderTitleLabel) genderTitleLabel.snp.makeConstraints { make in make.top.equalTo(subTitleDescriptionLabel.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) make.height.equalTo(20) } - + self.addSubview(genderSegmentControl) genderSegmentControl.snp.makeConstraints { make in make.top.equalTo(genderTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(ageTitleLabel) ageTitleLabel.snp.makeConstraints { make in make.top.equalTo(genderSegmentControl.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) make.height.equalTo(20) } - + self.addSubview(ageSelectedButton) ageSelectedButton.snp.makeConstraints { make in make.top.equalTo(ageTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(72) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift similarity index 88% rename from Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift index 987ddee1..818b7eea 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift @@ -1,38 +1,33 @@ -// -// TermsDetailController.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit +import DesignSystem + import RxCocoa import RxSwift +import SnapKit final class TermsDetailController: BaseViewController { - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = TermsDetailView() - + init(title: String?, content: String?) { super.init() let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.2 let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.KorFont(style: .regular, size: 14), + .font: UIFont.korFont(style: .regular, size: 14), .paragraphStyle: paragraphStyle ] mainView.contentTextView.attributedText = NSAttributedString(string: content ?? "", attributes: attributes) mainView.titleLabel.text = title - + } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -55,7 +50,7 @@ private extension TermsDetailController { make.edges.equalTo(view.safeAreaLayoutGuide) } } - + func bind() { mainView.xmarkButton.rx.tap .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift similarity index 87% rename from Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift index 9a12dc81..75772762 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift @@ -1,22 +1,16 @@ -// -// TermsDetailView.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit +import DesignSystem + import SnapKit final class TermsDetailView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 15) - return label + return PPLabel(style: .bold, fontSize: 15) }() - + let contentTextView: UITextView = { let view = UITextView() view.isSelectable = false @@ -25,19 +19,18 @@ final class TermsDetailView: UIView { return view }() - let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -45,7 +38,7 @@ final class TermsDetailView: UIView { // MARK: - SetUp private extension TermsDetailView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -53,14 +46,14 @@ private extension TermsDetailView { make.top.equalToSuperview().inset(25) make.height.equalTo(21) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(16) make.centerY.equalTo(titleLabel) } - + self.addSubview(contentTextView) contentTextView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(43) diff --git a/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/SplashController.swift similarity index 68% rename from Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/SplashController.swift index f0eb9ac8..006b1446 100644 --- a/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/SplashController.swift @@ -1,32 +1,30 @@ -// -// SplashController.swift -// Poppool -// -// Created by Porori on 11/26/24. -// - import UIKit -import SnapKit +import DesignSystem +import DomainInterface +import Infrastructure + +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit + +public final class SplashController: BaseViewController { -final class SplashController: BaseViewController { - // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SplashView() - private let authAPIUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) - private let keyChainService = KeyChainService() - + // //FIXME: Reactor 태워서 UseCase 처리하도록 수정 + @Dependency private var authAPIUseCase: AuthAPIUseCase + @Dependency private var keyChainService: KeyChainService + private var rootViewController: UIViewController? } // MARK: - Life Cycle extension SplashController { - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setUp() setRootview() @@ -43,7 +41,7 @@ private extension SplashController { make.edges.equalTo(view.safeAreaLayoutGuide) } } - + func playAnimation() { mainView.animationView.play { [weak self] _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -51,27 +49,31 @@ private extension SplashController { } } } - + func setRootview() { authAPIUseCase.postTokenReissue() .withUnretained(self) .subscribe(onNext: { (owner, response) in let newAccessToken = response.accessToken ?? "" let newRefreshToken = response.refreshToken ?? "" - let _ = owner.keyChainService.saveToken(type: .accessToken, value: newAccessToken) - let _ = owner.keyChainService.saveToken(type: .refreshToken, value: newRefreshToken) + _ = owner.keyChainService.saveToken(type: .accessToken, value: newAccessToken) + _ = owner.keyChainService.saveToken(type: .refreshToken, value: newRefreshToken) let navigationController = WaveTabBarController() owner.rootViewController = navigationController }, onError: { [weak self] _ in guard let self = self else { return } let loginViewController = LoginController() - loginViewController.reactor = LoginReactor() + loginViewController.reactor = LoginReactor( + authAPIUseCase: authAPIUseCase, + kakaoLoginUseCase: DIContainer.resolve(KakaoLoginUseCase.self), + appleLoginUseCase: DIContainer.resolve(AppleLoginUseCase.self) + ) let loginNavigationController = UINavigationController(rootViewController: loginViewController) rootViewController = loginNavigationController }) .disposed(by: disposeBag) } - + func changeRootView() { view.window?.rootViewController = rootViewController view.window?.makeKeyAndVisible() diff --git a/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/View/SplashView.swift similarity index 97% rename from Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/View/SplashView.swift index 08b21aee..db657eb4 100644 --- a/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/Splash/View/SplashView.swift @@ -7,25 +7,25 @@ import UIKit -import SnapKit import Lottie +import SnapKit final class SplashView: UIView { - + // MARK: - Components - + let animationView: LottieAnimationView = { let view = LottieAnimationView(name: "PP_splash") view.contentMode = .scaleAspectFit return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -33,7 +33,7 @@ final class SplashView: UIView { // MARK: - SetUp private extension SplashView { - + func setUpConstraints() { addSubview(animationView) animationView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift b/Poppool/PresentationLayer/Presentation/Presentation/Scene/TabbarController/TabbarController.swift similarity index 91% rename from Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Scene/TabbarController/TabbarController.swift index 35d94f0c..69d4639b 100644 --- a/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Scene/TabbarController/TabbarController.swift @@ -1,16 +1,12 @@ -// -// TabbarController.swift -// Poppool -// -// Created by SeoJunYoung on 12/1/24. -// - import UIKit +import DomainInterface +import Infrastructure + class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { - + private let waveLayer = CAShapeLayer() - + private let dotView: UIView = { let view = UIView() view.layer.borderWidth = 0.6 @@ -18,7 +14,7 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { view.backgroundColor = .blu500 return view }() - + override func viewDidLoad() { super.viewDidLoad() addSomeTabItems() @@ -26,36 +22,36 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { setupWaveTabBar() delegate = self } - + private func setupWaveTabBar() { // TabBar의 배경 투명 설정 tabBar.backgroundImage = UIImage() tabBar.shadowImage = UIImage() tabBar.isTranslucent = true - + // Wave Layer 설정 waveLayer.fillColor = UIColor.white.cgColor tabBar.layer.insertSublayer(waveLayer, at: 0) - + // Dot 설정 dotView.frame.size = CGSize(width: 12, height: 12) dotView.layer.cornerRadius = 6 tabBar.addSubview(dotView) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() updateWavePath() updateDotPosition(animated: false) updateItem() } - + private func updateItem() { let tabBarItemViews = tabBar.subviews.filter { $0.isUserInteractionEnabled } - + if selectedIndex < tabBarItemViews.count { let selectedView = tabBarItemViews[selectedIndex] - + // 애니메이션 적용 UIView.animate( withDuration: 0.3, @@ -71,7 +67,7 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { ) } } - + private func updateWavePath(animated: Bool = false) { guard let items = tabBar.items else { return } let tabWidth = tabBar.bounds.width / CGFloat(items.count) @@ -139,12 +135,12 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { guard let items = tabBar.items else { return } let tabWidth = tabBar.bounds.width / CGFloat(items.count) let selectedTabX = CGFloat(selectedIndex) * tabWidth - + let targetCenter = CGPoint( x: selectedTabX + tabWidth / 2, y: -12 ) - + if animated { UIView.animate(withDuration: 1, delay: 0, @@ -158,20 +154,20 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { dotView.center = targetCenter } } - + // 탭 선택 시 애니메이션 적용 func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { updateWavePath(animated: true) updateDotPosition(animated: true) updateItem() } - + func setUp() { self.selectedIndex = 1 self.tabBar.barTintColor = .g200 self.tabBar.tintColor = .blu500 } - + func resizeImage(image: UIImage?, targetSize: CGSize) -> UIImage? { guard let image = image else { return nil } let size = image.size @@ -180,32 +176,35 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { let heightRatio = targetSize.height / size.height let scaleFactor = min(widthRatio, heightRatio) - + let scaledImageSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor) UIGraphicsBeginImageContextWithOptions(scaledImageSize, false, 0.0) image.draw(in: CGRect(origin: .zero, size: scaledImageSize)) - + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resizedImage } - - func addSomeTabItems() { - let provider = ProviderImpl() + func addSomeTabItems() { let mapController = MapViewController() - let mapUseCase = DefaultMapUseCase(repository: DefaultMapRepository(provider: provider)) - let directionRepository = DefaultMapDirectionRepository(provider: provider) - mapController.reactor = MapReactor(useCase: mapUseCase, directionRepository: directionRepository) + + mapController.reactor = MapReactor( + mapUseCase: DIContainer.resolve(MapUseCase.self), + mapDirectionRepository: DIContainer.resolve(MapDirectionRepository.self) + ) let homeController = HomeController() - homeController.reactor = HomeReactor() - + homeController.reactor = HomeReactor( + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + homeAPIUseCase: DIContainer.resolve(HomeAPIUseCase.self) + ) + let myPageController = MyPageController() - myPageController.reactor = MyPageReactor() - + myPageController.reactor = MyPageReactor(userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self)) + let iconSize = CGSize(width: 32, height: 32) // 탭바 아이템 생성 mapController.tabBarItem = UITabBarItem( @@ -223,33 +222,33 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { image: resizeImage(image: UIImage(named: "icon_tabbar_menu"), targetSize: iconSize), selectedImage: resizeImage(image: UIImage(named: "icon_tabbar_menu"), targetSize: iconSize) ) - + // 네비게이션 컨트롤러 설정 let map = UINavigationController(rootViewController: mapController) let home = UINavigationController(rootViewController: homeController) let myPage = UINavigationController(rootViewController: myPageController) - + viewControllers = [map, home, myPage] - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.2 // 기본 값보다 높은 라인 간격을 설정 - + // 폰트 설정 let appearance = UITabBarAppearance() appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ - .font: UIFont.KorFont(style: .medium, size: 11)!, + .font: UIFont.korFont(style: .medium, size: 11), .paragraphStyle: paragraphStyle ] appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ - .font: UIFont.KorFont(style: .bold, size: 11)!, + .font: UIFont.korFont(style: .bold, size: 11), .paragraphStyle: paragraphStyle ] - + let verticalOffset: CGFloat = 4 // 원하는 간격 (양수: 아래로 이동, 음수: 위로 이동) appearance.stackedLayoutAppearance.normal.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: verticalOffset) appearance.stackedLayoutAppearance.selected.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: verticalOffset) - + tabBar.standardAppearance = appearance } - + } diff --git a/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringManager.swift similarity index 54% rename from Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringManager.swift index 306392bb..a4bd93be 100644 --- a/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringManager.swift @@ -1,23 +1,25 @@ -import CoreLocation -import UIKit +import DomainInterface +import Infrastructure + +import NMapsMap class ClusteringManager { - private let regions = RegionDefinitions.allClusters + private let regions = RegionType.RegionDefinitions.allClusters private class MutableCluster { let base: RegionCluster var stores: [MapPopUpStore] var storeCount: Int - var fixedCenter: CLLocationCoordinate2D? + var fixedCenter: NMGLatLng? - init(base: RegionCluster, fixedCenter: CLLocationCoordinate2D? = nil) { + init(base: RegionCluster, fixedCenter: NMGLatLng? = nil) { self.base = base self.stores = [] self.storeCount = 0 self.fixedCenter = fixedCenter } - func centerCoordinate() -> CLLocationCoordinate2D { + func centerCoordinate() -> NMGLatLng { return fixedCenter ?? base.coordinate } @@ -35,106 +37,40 @@ class ClusteringManager { } } - // 수정: 항상 구 단위 클러스터링만 사용하도록 변경 func clusterStores(_ stores: [MapPopUpStore], at zoomLevel: Float) -> [ClusterMarkerData] { - // 줌 레벨 무시하고 항상 구 단위 클러스터링 실행 - return clusterByDistrictOnly(stores) - } - - // 새로운 함수: 모든 스토어를 구 단위로만 클러스터링 - private func clusterByDistrictOnly(_ stores: [MapPopUpStore]) -> [ClusterMarkerData] { - var clusters: [String: MutableCluster] = [:] + let level = MapZoomLevel.getLevel(from: zoomLevel) - // 서울 구 클러스터 초기화 - for cluster in RegionDefinitions.seoulClusters { - clusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) - } - - // 경기 시/군 클러스터 초기화 - for cluster in RegionDefinitions.gyeonggiClusters { - clusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) - } - - // 기타 광역시/도 초기화 - for cluster in RegionDefinitions.metropolitanClusters { - clusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) - } - - // 도 단위 초기화 - for cluster in RegionDefinitions.provinceClusters { - clusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) - } - - // 각 스토어를 적절한 클러스터에 할당 - for store in stores { + let partitionedStores = stores.partition { store in let city = extractCity(from: store.address) - - switch city { - case "서울": - if let clusterName = findMatchingSeoulDistrictName(in: store.address), - let cluster = clusters[clusterName] { - cluster.stores.append(store) - cluster.storeCount += 1 - } - case "경기": - if let clusterName = findMatchingGyeonggiCityName(in: store.address), - let cluster = clusters[clusterName] { - cluster.stores.append(store) - cluster.storeCount += 1 - } - default: - // 서울/경기 외 지역은 광역시/도 단위로 클러스터링 - if let cluster = clusters[city] { - cluster.stores.append(store) - cluster.storeCount += 1 - } - } - } - - // 스토어가 있는 클러스터만 필터링 - let validClusters = clusters.values.filter { $0.storeCount > 0 } - return validClusters.map { $0.toMarkerData() } - } - - // 기존 함수들 유지 (다만 더 이상 사용되지 않음) - private func clusterByCityName(_ stores: [MapPopUpStore]) -> [ClusterMarkerData] { - var clusters: [String: MutableCluster] = [:] - - for store in stores { - let city = extractCity(from: store.address) - - // 아직 해당 city 이름으로 된 MutableCluster가 없다면 생성 - if clusters[city] == nil { - let baseRegion = RegionCluster( - name: city, - subRegions: [city], - coordinate: getFixedCenterForCity(city) ?? CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), // 기본값(서울 좌표) - type: .metropolitan - ) - clusters[city] = MutableCluster(base: baseRegion, fixedCenter: baseRegion.coordinate) - } - - // 스토어 할당 - if let cluster = clusters[city] { - cluster.stores.append(store) - cluster.storeCount += 1 - } + return city == "서울" || city == "경기" + } + let seoulGyeonggiStores = partitionedStores.0 + let otherStores = partitionedStores.1 + + switch level { + case .country: + return clusterByProvince(stores) + case .city: + let seoulGyeonggiClusters = clusterByDistrict(seoulGyeonggiStores) + let otherClusters = clusterByMetropolitan(otherStores) + return seoulGyeonggiClusters + otherClusters + case .district: + let seoulGyeonggiClusters = clusterByDistrict(seoulGyeonggiStores) + let otherClusters = clusterByMetropolitan(otherStores) + return seoulGyeonggiClusters + otherClusters + case .detailed: + return [] } - - let validClusters = clusters.values.filter { $0.storeCount > 0 } - return validClusters.map { $0.toMarkerData() } } private func clusterByMetropolitan(_ stores: [MapPopUpStore]) -> [ClusterMarkerData] { var clusters: [String: MutableCluster] = [:] - // 광역시/도 클러스터 초기화 - let allClusters = RegionDefinitions.metropolitanClusters + RegionDefinitions.provinceClusters + let allClusters = RegionType.RegionDefinitions.metropolitanClusters + RegionType.RegionDefinitions.provinceClusters for cluster in allClusters { clusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) } - // 스토어 할당 for store in stores { let city = extractCity(from: store.address) if let cluster = clusters[city] { @@ -152,19 +88,16 @@ class ClusteringManager { var gyeonggiClusters: [String: MutableCluster] = [:] var otherClusters: [String: MutableCluster] = [:] - // 서울/경기 각 구/시 초기화 - for cluster in RegionDefinitions.seoulClusters { + for cluster in RegionType.RegionDefinitions.seoulClusters { seoulClusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) } - for cluster in RegionDefinitions.gyeonggiClusters { + for cluster in RegionType.RegionDefinitions.gyeonggiClusters { gyeonggiClusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) } - - // 그 외 지역 - for cluster in RegionDefinitions.metropolitanClusters { + for cluster in RegionType.RegionDefinitions.metropolitanClusters { otherClusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) } - for cluster in RegionDefinitions.provinceClusters { + for cluster in RegionType.RegionDefinitions.provinceClusters { otherClusters[cluster.name] = MutableCluster(base: cluster, fixedCenter: cluster.coordinate) } @@ -193,12 +126,16 @@ class ClusteringManager { let combined = Array(seoulClusters.values) + Array(gyeonggiClusters.values) + Array(otherClusters.values) let filtered = combined.filter { $0.storeCount > 0 } + for cluster in filtered { + Logger.log("- \(cluster.base.name): \(cluster.storeCount)개 매장", category: .debug) + } + return filtered.map { $0.toMarkerData() } } private func clusterByProvince(_ stores: [MapPopUpStore]) -> [ClusterMarkerData] { var clusters: [String: MutableCluster] = [:] - for cluster in RegionDefinitions.provinceClusters { + for cluster in RegionType.RegionDefinitions.provinceClusters { clusters[cluster.name] = MutableCluster(base: cluster) } for store in stores { @@ -213,7 +150,7 @@ class ClusteringManager { } private func findMatchingSeoulDistrictName(in address: String) -> String? { - return RegionDefinitions.seoulClusters.first { cluster in + return RegionType.RegionDefinitions.seoulClusters.first { cluster in cluster.subRegions.contains { district in address.contains(district) } @@ -221,7 +158,7 @@ class ClusteringManager { } private func findMatchingGyeonggiCityName(in address: String) -> String? { - return RegionDefinitions.gyeonggiClusters.first { cluster in + return RegionType.RegionDefinitions.gyeonggiClusters.first { cluster in cluster.subRegions.contains { cityName in address.contains(cityName) } @@ -229,14 +166,14 @@ class ClusteringManager { } private func findMatchingProvinceName(in address: String) -> String? { - return RegionDefinitions.provinceClusters.first { cluster in + return RegionType.RegionDefinitions.provinceClusters.first { cluster in cluster.subRegions.contains { province in address.contains(province) } }?.name } - private func getFixedCenterForCity(_ city: String) -> CLLocationCoordinate2D? { + private func getFixedCenterForCity(_ city: String) -> NMGLatLng? { switch city { case "대구": return RegionCoordinate.daegu case "부산": return RegionCoordinate.busan @@ -250,7 +187,6 @@ class ClusteringManager { } } -// partition() 확장: 서울·경기 vs 그 외 지역 분류 용 extension Array { func partition(by predicate: (Element) -> Bool) -> ([Element], [Element]) { var matching: [Element] = [] diff --git a/Poppool/Poppool/Presentation/Map/Common/ClusteringModels.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringModels.swift similarity index 87% rename from Poppool/Poppool/Presentation/Map/Common/ClusteringModels.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringModels.swift index 3d5a1c94..84da1db2 100644 --- a/Poppool/Poppool/Presentation/Map/Common/ClusteringModels.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ClusteringModels.swift @@ -1,4 +1,6 @@ -import CoreLocation +import DomainInterface + +import NMapsMap enum MapZoomLevel { case country @@ -12,7 +14,7 @@ enum MapZoomLevel { return .country case 7..<10: return .city - case 10..<12: + case 10..<11: return .district default: return .detailed @@ -20,11 +22,10 @@ enum MapZoomLevel { } } - struct RegionCluster { let name: String let subRegions: [String] - let coordinate: CLLocationCoordinate2D + let coordinate: NMGLatLng let type: RegionType var storeCount: Int = 0 diff --git a/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/DateTimePickerManager.swift similarity index 98% rename from Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/DateTimePickerManager.swift index 3e251d01..9fb27041 100644 --- a/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/DateTimePickerManager.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class DateTimePickerManager { @@ -77,6 +77,7 @@ final class DateTimePickerManager { let alert1 = UIAlertController(title: "시작시간 선택", message: nil, preferredStyle: .actionSheet) let dp1 = UIDatePicker() dp1.datePickerMode = .time + dp1.minuteInterval = 15 dp1.preferredDatePickerStyle = .wheels alert1.view.addSubview(dp1) @@ -107,6 +108,7 @@ final class DateTimePickerManager { let alert2 = UIAlertController(title: "종료시간 선택", message: nil, preferredStyle: .actionSheet) let dp2 = UIDatePicker() dp2.datePickerMode = .time + dp2.minuteInterval = 15 dp2.preferredDatePickerStyle = .wheels alert2.view.addSubview(dp2) diff --git a/Poppool/Poppool/Presentation/Admin/Common/ExtendedImage.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ExtendedImage.swift similarity index 100% rename from Poppool/Poppool/Presentation/Admin/Common/ExtendedImage.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/ExtendedImage.swift diff --git a/Poppool/Poppool/Presentation/Map/Common/FilterType.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/FilterType.swift similarity index 90% rename from Poppool/Poppool/Presentation/Map/Common/FilterType.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/FilterType.swift index 354d319a..c181a940 100644 --- a/Poppool/Poppool/Presentation/Map/Common/FilterType.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/FilterType.swift @@ -1,7 +1,4 @@ - - import Foundation -import UIKit /// 맵과 리스트에서 공통으로 사용하는 필터 타입 enum FilterType { diff --git a/Poppool/Poppool/Presentation/Map/Common/LocationPermissionBottomSheet.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/LocationPermissionBottomSheet.swift similarity index 93% rename from Poppool/Poppool/Presentation/Map/Common/LocationPermissionBottomSheet.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/LocationPermissionBottomSheet.swift index 88dcc961..1b5d50da 100644 --- a/Poppool/Poppool/Presentation/Map/Common/LocationPermissionBottomSheet.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/LocationPermissionBottomSheet.swift @@ -1,5 +1,5 @@ -import UIKit import SnapKit +import UIKit final class LocationPermissionBottomSheet: UIViewController { @@ -10,7 +10,7 @@ final class LocationPermissionBottomSheet: UIViewController { private let titleLabel: UILabel = { let label = UILabel() label.text = "내 위치를 중심으로\n보기 위한 준비가 필요해요" - label.font = UIFont.KorFont(style: .bold, size: 18) + label.font = UIFont.korFont(style: .bold, size: 18) label.textColor = .g1000 label.numberOfLines = 2 label.textAlignment = .center @@ -21,7 +21,7 @@ final class LocationPermissionBottomSheet: UIViewController { private let descriptionLabel: UILabel = { let label = UILabel() label.text = "설정 > 위치 권한을 허용하신 후에\n내 주변의 다양한 팝업스토어를 볼 수 있어요." - label.font = UIFont.KorFont(style: .regular, size: 14) + label.font = UIFont.korFont(style: .regular, size: 14) label.textColor = .g600 label.numberOfLines = 2 label.textAlignment = .center @@ -33,7 +33,7 @@ final class LocationPermissionBottomSheet: UIViewController { let button = UIButton() button.setTitle("취소", for: .normal) button.setTitleColor(.g600, for: .normal) - button.titleLabel?.font = UIFont.KorFont(style: .medium, size: 16) + button.titleLabel?.font = UIFont.korFont(style: .medium, size: 16) button.backgroundColor = .g50 button.layer.cornerRadius = 10 return button @@ -44,7 +44,7 @@ final class LocationPermissionBottomSheet: UIViewController { let button = UIButton() button.setTitle("권한설정", for: .normal) button.setTitleColor(.white, for: .normal) - button.titleLabel?.font = UIFont.KorFont(style: .medium, size: 16) + button.titleLabel?.font = UIFont.korFont(style: .medium, size: 16) button.backgroundColor = .blu500 button.layer.cornerRadius = 10 return button diff --git a/Poppool/Poppool/Presentation/Map/Common/MapFilterChips.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapFilterChips.swift similarity index 99% rename from Poppool/Poppool/Presentation/Map/Common/MapFilterChips.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapFilterChips.swift index 7745525c..8006ce31 100644 --- a/Poppool/Poppool/Presentation/Map/Common/MapFilterChips.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapFilterChips.swift @@ -1,6 +1,5 @@ -import UIKit import SnapKit - +import UIKit class MapFilterChips: UIView { // MARK: - Components diff --git a/Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapUtilities.swift similarity index 57% rename from Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapUtilities.swift index ff6f6557..d76dafe1 100644 --- a/Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/MapUtilities.swift @@ -1,6 +1,5 @@ - - -import CoreLocation +import NMapsMap +import UIKit public func extractCity(from address: String) -> String { let components = address.components(separatedBy: " ") @@ -29,26 +28,3 @@ public let gyeonggiSouthRegions: [String] = [ "용인시", "화성시", "수원시", "안산시", "부천시", "의왕시", "과천시", "여주시", "양평군", "광주시", "이천시" ] - -// RepresentativeScope 수정 -public struct RepresentativeScope { - public static let seoulNorth = ( - center: CLLocationCoordinate2D(latitude: 37.6020, longitude: 127.0350), - radius: 3000.0 - ) - public static let seoulSouth = ( - center: CLLocationCoordinate2D(latitude: 37.4959, longitude: 127.0664), // 강남/서초 중심 - radius: 3000.0 - ) - - // 경기 북부/남부 좌표 조정 - public static let gyeonggiNorth = ( - center: CLLocationCoordinate2D(latitude: 37.7358, longitude: 127.0346), // 의정부 중심 - radius: 4000.0 - ) - public static let gyeonggiSouth = ( - center: CLLocationCoordinate2D(latitude: 37.2911, longitude: 127.0876), // 용인/분당 중심 - radius: 4000.0 - ) -} - diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/NMFMapViewDelegateProxy.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/NMFMapViewDelegateProxy.swift new file mode 100644 index 00000000..71e6a303 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/NMFMapViewDelegateProxy.swift @@ -0,0 +1,92 @@ +import NMapsMap +import RxCocoa +import RxSwift + +/// NMFMapViewDelegateProxy는 NMFMapView의 delegate 이벤트를 RxSwift Observable로 변환하는 역할 +class NMFMapViewDelegateProxy: DelegateProxy, DelegateProxyType, NMFMapViewDelegate { + + // MARK: - Properties + + /// 연결된 NMFMapView 인스턴스 (약한 참조) + public weak private(set) var mapView: NMFMapView? + + /// 카메라 위치 변경 이벤트를 전달하기 위한 Rx Subject + let didChangePositionSubject = PublishSubject() + + /// 맵이 idle 상태가 되었을 때 이벤트를 전달하기 위한 Rx Subject + let idleAtPositionSubject = PublishSubject() + + // MARK: - Initializer + + /// NMFMapViewDelegateProxy 초기화 메서드 + /// - Parameter mapView: 이벤트를 받아올 NMFMapView 인스턴스 + init(mapView: NMFMapView) { + self.mapView = mapView + super.init(parentObject: mapView, delegateProxy: NMFMapViewDelegateProxy.self) + } + + // MARK: - DelegateProxyType Implementation + + /// Rx에서 사용하기 위한 구현 등록 + static func registerKnownImplementations() { + self.register { NMFMapViewDelegateProxy(mapView: $0) } + } + + /// 지정된 NMFMapView의 현재 delegate를 반환 + /// - Parameter object: NMFMapView 인스턴스 + /// - Returns: 해당 mapView의 delegate + static func currentDelegate(for object: NMFMapView) -> NMFMapViewDelegate? { + return object.delegate + } + + /// 지정된 NMFMapView에 delegate를 설정 + /// - Parameters: + /// - delegate: 설정할 delegate + /// - object: NMFMapView 인스턴스 + static func setCurrentDelegate(_ delegate: NMFMapViewDelegate?, to object: NMFMapView) { + object.delegate = delegate + } + + // MARK: - NMFMapViewDelegate Methods + + /// 카메라 위치가 변경될 때 호출되는 메서드. + /// - Parameters: + /// - mapView: 이벤트가 발생한 NMFMapView + /// - reason: 카메라 변경 사유 + /// - animated: 애니메이션 여부 + func mapView(_ mapView: NMFMapView, cameraDidChangeByReason reason: Int, animated: Bool) { + didChangePositionSubject.onNext(()) + // 기존 delegate로 이벤트 전달 (옵셔널 체이닝) + _forwardToDelegate?.mapView?(mapView, cameraDidChangeByReason: reason, animated: animated) + } + + /// 맵뷰가 idle 상태가 되었을 때 호출되는 메서드. + /// Rx Subject를 통해 idle 이벤트를 전달하고, 기존 delegate에게 까지 + /// - Parameter mapView: idle 상태가 된 NMFMapView + func mapViewIdle(_ mapView: NMFMapView) { + idleAtPositionSubject.onNext(()) + // 기존 delegate로 idle 이벤트 전달 + forwardToDelegate()?.mapViewIdle?(mapView) + } +} + +/// NMFMapView의 Reactive 확장 +extension Reactive where Base: NMFMapView { + + /// NMFMapViewDelegateProxy를 반환하여 delegate 이벤트를 처리할 수 있도록 + var delegate: DelegateProxy { + return NMFMapViewDelegateProxy.proxy(for: base) + } + + /// mapView의 카메라 위치 변경 이벤트를 Observable로 + var didChangePosition: Observable { + let proxy = NMFMapViewDelegateProxy.proxy(for: base) + return proxy.didChangePositionSubject.asObservable() + } + + /// mapView가 idle 상태가 되었을 때의 이벤트를 Observable로 + var idleAtPosition: Observable { + let proxy = NMFMapViewDelegateProxy.proxy(for: base) + return proxy.idleAtPositionSubject.asObservable() + } +} diff --git a/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/RegionDefinitions.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/RegionDefinitions.swift new file mode 100644 index 00000000..92e35ab1 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/Common/RegionDefinitions.swift @@ -0,0 +1,264 @@ +import NMapsMap + +struct RegionCoordinate { + static let seoul = NMGLatLng(lat: 37.5665, lng: 126.9780) + static let gyeonggi = NMGLatLng(lat: 37.4138, lng: 127.5183) + static let incheon = NMGLatLng(lat: 37.4563, lng: 126.7052) + static let daejeon = NMGLatLng(lat: 36.3504, lng: 127.3845) + static let gwangju = NMGLatLng(lat: 35.1595, lng: 126.8526) + static let daegu = NMGLatLng(lat: 35.8714, lng: 128.6014) + static let busan = NMGLatLng(lat: 35.1796, lng: 129.0756) + static let ulsan = NMGLatLng(lat: 35.5384, lng: 129.3114) + static let chungbuk = NMGLatLng(lat: 36.6357, lng: 127.4914) + static let chungnam = NMGLatLng(lat: 36.6588, lng: 126.6728) + static let sejong = NMGLatLng(lat: 36.4801, lng: 127.2892) + static let jeonbuk = NMGLatLng(lat: 35.7175, lng: 127.1530) + static let jeonnam = NMGLatLng(lat: 34.8679, lng: 126.9910) + static let gyeongbuk = NMGLatLng(lat: 36.4919, lng: 128.8889) + static let gyeongnam = NMGLatLng(lat: 35.4606, lng: 128.2132) + static let gangwon = NMGLatLng(lat: 37.8228, lng: 128.1555) + static let jeju = NMGLatLng(lat: 33.4890, lng: 126.4983) +} + +enum RegionType { + case seoul + case gyeonggi + case metropolitan + case province + + struct RegionDefinitions { + static let seoulClusters: [RegionCluster] = [ + RegionCluster( + name: "도봉/노원/강북/중랑", + subRegions: ["도봉구", "노원구", "강북구", "중랑구"], + coordinate: NMGLatLng(lat: 37.6494, lng: 127.0510), + type: .seoul + ), + RegionCluster( + name: "동대문/성북", + subRegions: ["동대문구", "성북구"], + coordinate: NMGLatLng(lat: 37.5894, lng: 127.0435), + type: .seoul + ), + RegionCluster( + name: "중구/종로", + subRegions: ["중구", "종로구"], + coordinate: NMGLatLng(lat: 37.5738, lng: 126.9861), + type: .seoul + ), + RegionCluster( + name: "성동/광진", + subRegions: ["성동구", "광진구"], + coordinate: NMGLatLng(lat: 37.5509, lng: 127.0403), + type: .seoul + ), + RegionCluster( + name: "송파/강동", + subRegions: ["송파구", "강동구"], + coordinate: NMGLatLng(lat: 37.5145, lng: 127.1058), + type: .seoul + ), + RegionCluster( + name: "동작/관악", + subRegions: ["동작구", "관악구"], + coordinate: NMGLatLng(lat: 37.4959, lng: 126.9410), + type: .seoul + ), + RegionCluster( + name: "서초/강남", + subRegions: ["서초구", "강남구"], + coordinate: NMGLatLng(lat: 37.4959, lng: 127.0664), + type: .seoul + ), + RegionCluster( + name: "은평/서대문/마포", + subRegions: ["은평구", "서대문구", "마포구"], + coordinate: NMGLatLng(lat: 37.5744, lng: 126.9185), + type: .seoul + ), + RegionCluster( + name: "영등포/구로", + subRegions: ["영등포구", "구로구"], + coordinate: NMGLatLng(lat: 37.5162, lng: 126.8968), + type: .seoul + ), + RegionCluster( + name: "용산", + subRegions: ["용산구"], + coordinate: NMGLatLng(lat: 37.5384, lng: 126.9654), + type: .seoul + ), + RegionCluster( + name: "양천/강서/금천", + subRegions: ["양천구", "강서구", "금천구"], + coordinate: NMGLatLng(lat: 37.5509, lng: 126.8553), + type: .seoul + ) + ] + + // 경기도 클러스터 + static let gyeonggiClusters: [RegionCluster] = [ + RegionCluster( + name: "포천/연천/동두천/양주", + subRegions: ["포천시", "연천군", "동두천시", "양주시"], + coordinate: NMGLatLng(lat: 37.8859, lng: 127.0543), + type: .gyeonggi + ), + RegionCluster( + name: "의정부/구리/남양주", + subRegions: ["의정부시", "구리시", "남양주시"], + coordinate: NMGLatLng(lat: 37.7358, lng: 127.1422), + type: .gyeonggi + ), + RegionCluster( + name: "파주/고양/가평", + subRegions: ["파주시", "고양시", "가평군"], + coordinate: NMGLatLng(lat: 37.7599, lng: 126.7762), + type: .gyeonggi + ), + RegionCluster( + name: "용인/화성/수원", + subRegions: ["용인시", "화성시", "수원시"], + coordinate: NMGLatLng(lat: 37.2911, lng: 127.0876), + type: .gyeonggi + ), + RegionCluster( + name: "군포/의왕/과천/안양", + subRegions: ["군포시", "의왕시", "과천시", "안양시"], + coordinate: NMGLatLng(lat: 37.3956, lng: 126.9477), + type: .gyeonggi + ), + RegionCluster( + name: "부천/광명/시흥/안산", + subRegions: ["부천시", "광명시", "시흥시", "안산시"], + coordinate: NMGLatLng(lat: 37.4563, lng: 126.8040), + type: .gyeonggi + ), + RegionCluster( + name: "안성/평택/오산", + subRegions: ["안성시", "평택시", "오산시"], + coordinate: NMGLatLng(lat: 37.0042, lng: 127.2003), + type: .gyeonggi + ), + RegionCluster( + name: "여주/양평/광주/이천", + subRegions: ["여주시", "양평군", "광주시", "이천시"], + coordinate: NMGLatLng(lat: 37.2958, lng: 127.5986), + type: .gyeonggi + ), + RegionCluster( + name: "김포", + subRegions: ["김포시"], + coordinate: NMGLatLng(lat: 37.6153, lng: 126.7164), + type: .gyeonggi + ), + RegionCluster( + name: "성남/하남", + subRegions: ["성남시", "하남시"], + coordinate: NMGLatLng(lat: 37.4517, lng: 127.1486), + type: .gyeonggi + ) + ] + + // 광역시 클러스터 + static let metropolitanClusters: [RegionCluster] = [ + RegionCluster( + name: "인천", + subRegions: ["인천광역시"], + coordinate: NMGLatLng(lat: 37.4563, lng: 126.7052), + type: .metropolitan + ), + RegionCluster( + name: "대전", + subRegions: ["대전광역시"], + coordinate: NMGLatLng(lat: 36.3504, lng: 127.3845), + type: .metropolitan + ), + RegionCluster( + name: "광주", + subRegions: ["광주광역시"], + coordinate: NMGLatLng(lat: 35.1595, lng: 126.8526), + type: .metropolitan + ), + RegionCluster( + name: "대구", + subRegions: ["대구광역시"], + coordinate: NMGLatLng(lat: 35.8714, lng: 128.6014), + type: .metropolitan + ), + RegionCluster( + name: "부산", + subRegions: ["부산광역시"], + coordinate: NMGLatLng(lat: 35.1796, lng: 129.0756), + type: .metropolitan + ), + RegionCluster( + name: "울산", + subRegions: ["울산광역시"], + coordinate: NMGLatLng(lat: 35.5384, lng: 129.3114), + type: .metropolitan + ) + ] + + static let provinceClusters: [RegionCluster] = [ + RegionCluster( + name: "충북", + subRegions: ["충청북도"], + coordinate: NMGLatLng(lat: 36.6357, lng: 127.4914), + type: .province + ), + RegionCluster( + name: "충남", + subRegions: ["충청남도"], + coordinate: NMGLatLng(lat: 36.6588, lng: 126.6728), + type: .province + ), + RegionCluster( + name: "세종", + subRegions: ["세종특별자치시"], + coordinate: NMGLatLng(lat: 36.4801, lng: 127.2892), + type: .province + ), + RegionCluster( + name: "전북", + subRegions: ["전라북도"], + coordinate: NMGLatLng(lat: 35.7175, lng: 127.1530), + type: .province + ), + RegionCluster( + name: "전남", + subRegions: ["전라남도"], + coordinate: NMGLatLng(lat: 34.8679, lng: 126.9910), + type: .province + ), + RegionCluster( + name: "경북", + subRegions: ["경상북도"], + coordinate: NMGLatLng(lat: 36.4919, lng: 128.8889), + type: .province + ), + RegionCluster( + name: "경남", + subRegions: ["경상남도"], + coordinate: NMGLatLng(lat: 35.4606, lng: 128.2132), + type: .province + ), + RegionCluster( + name: "강원", + subRegions: ["강원도"], + coordinate: NMGLatLng(lat: 37.8228, lng: 128.1555), + type: .province + ), + RegionCluster( + name: "제주", + subRegions: ["제주특별자치도"], + coordinate: NMGLatLng(lat: 33.4890, lng: 126.4983), + type: .province + ) + ] + + static var allClusters: [RegionCluster] { + seoulClusters + gyeonggiClusters + metropolitanClusters + provinceClusters + } + } +} diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/BookMarkToastView.swift similarity index 88% rename from Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/BookMarkToastView.swift index a23d52c3..114aa5a1 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/BookMarkToastView.swift @@ -1,18 +1,13 @@ -// -// BookMarkToastView.swift -// Poppool -// -// Created by SeoJunYoung on 1/21/25. -// - import UIKit +import DesignSystem + import SnapKit final class BookMarkToastView: UIView { - + // MARK: - Components - + private let bgView: UIView = { let view = UIView() view.backgroundColor = .pb70 @@ -20,17 +15,17 @@ final class BookMarkToastView: UIView { view.clipsToBounds = true return view }() - + private let bookMarkLabel: UILabel = { let label = UILabel() - label.setLineHeightText(text: "찜한 팝업에 저장했어요", font: .KorFont(style: .regular, size: 15), lineHeight: 1) + label.setLineHeightText(text: "찜한 팝업에 저장했어요", font: .korFont(style: .regular, size: 15), lineHeight: 1) label.textColor = .w100 return label }() - + private let unbookMarkLabel: UILabel = { let label = PPLabel(style: .regular, fontSize: 15, text: "찜한 팝업을 해제했어요") - label.setLineHeightText(text: "찜한 팝업을 해제했어요", font: .KorFont(style: .regular, size: 15), lineHeight: 1) + label.setLineHeightText(text: "찜한 팝업을 해제했어요", font: .korFont(style: .regular, size: 15), lineHeight: 1) label.textColor = .w100 return label }() @@ -42,10 +37,10 @@ final class BookMarkToastView: UIView { button.setTitleColor(.blu300, for: .normal) button.layer.cornerRadius = 4 button.clipsToBounds = true - button.titleLabel?.font = .KorFont(style: .medium, size: 12) + button.titleLabel?.font = .korFont(style: .medium, size: 12) return button }() - + // MARK: - init init(isBookMark: Bool) { super.init(frame: .zero) @@ -59,7 +54,7 @@ final class BookMarkToastView: UIView { setUpUnBookMarkConstraints() } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -67,7 +62,7 @@ final class BookMarkToastView: UIView { // MARK: - SetUp private extension BookMarkToastView { - + func setUpBookMarkConstraints() { bgView.addSubview(moveButton) moveButton.snp.makeConstraints { make in @@ -83,7 +78,7 @@ private extension BookMarkToastView { make.centerY.equalToSuperview() } } - + func setUpUnBookMarkConstraints() { bgView.addSubview(unbookMarkLabel) unbookMarkLabel.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastMaker.swift similarity index 94% rename from Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastMaker.swift index a52b772c..7c6f29be 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastMaker.swift @@ -1,20 +1,16 @@ -// -// ToastMaker.swift -// Poppool -// -// Created by SeoJunYoung on 11/25/24. -// - import UIKit -import SnapKit -import RxSwift +import DomainInterface +import Infrastructure + import RxCocoa +import RxSwift +import SnapKit final class ToastMaker { - + // MARK: - Properties - + /// 현재 디바이스 최상단 Window를 지정 static var window: UIWindow? { return UIApplication @@ -23,7 +19,7 @@ final class ToastMaker { .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } .first { $0.isKeyWindow } } - + /// 최상단의 ViewController를 가져오는 메서드 private static func topViewController( _ rootViewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController @@ -39,32 +35,32 @@ final class ToastMaker { } return rootViewController } - + private static var currentToast: ToastView? private static var currentBookMarkToast: BookMarkToastView? private static var disposeBag = DisposeBag() } extension ToastMaker { - + // MARK: - Method - + /// 토스트 메시지를 생성하는 메서드 /// - Parameter message: 토스트 메세지에 담길 String 타입 static func createToast(message: String) { - + currentToast?.removeFromSuperview() currentToast = nil let toastMSG = ToastView(message: message) guard let window = window else { return } window.addSubview(toastMSG) currentToast = toastMSG - + toastMSG.snp.makeConstraints { make in make.bottom.equalTo(window.snp.bottom).inset(120) make.centerX.equalTo(window.snp.centerX) } - + UIView.animate( withDuration: 0.3, delay: 4, @@ -76,11 +72,11 @@ extension ToastMaker { if currentToast == toastMSG { currentToast = nil } } } - + /// 토스트 메시지를 생성하는 메서드 /// - Parameter message: 토스트 메세지에 담길 String 타입 static func createBookMarkToast(isBookMark: Bool) { - + currentBookMarkToast?.removeFromSuperview() currentBookMarkToast = nil disposeBag = DisposeBag() @@ -92,11 +88,13 @@ extension ToastMaker { .withUnretained(currentVC) .subscribe(onNext: { (owner, _) in let nextController = MyPageBookmarkController() - nextController.reactor = MyPageBookmarkReactor() + nextController.reactor = MyPageBookmarkReactor( + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self) + ) owner.navigationController?.pushViewController(nextController, animated: true) }) .disposed(by: disposeBag) - + if isBookMark { toastMSG.snp.makeConstraints { make in make.bottom.equalTo(currentVC.view.snp.bottom).inset(120) @@ -108,8 +106,7 @@ extension ToastMaker { make.centerX.equalTo(currentVC.view.snp.centerX) } } - - + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { UIView.animate( withDuration: 0.3, delay: 0, @@ -122,6 +119,6 @@ extension ToastMaker { disposeBag = DisposeBag() } } - + } } diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastView.swift similarity index 92% rename from Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift rename to Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastView.swift index e20c3c72..79124cca 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift +++ b/Poppool/PresentationLayer/Presentation/Presentation/Utills/ToastMaker/ToastView.swift @@ -11,9 +11,9 @@ import SnapKit /// 토스트 메시지를 담는 view 객체입니다 final class ToastView: UIView { - + // MARK: - Properties - + private let bgView: UIView = { let view = UIView() view.backgroundColor = .pb70 @@ -22,42 +22,42 @@ final class ToastView: UIView { view.sizeToFit() return view }() - + private let messageLabel: UILabel = { let label = UILabel() label.textColor = .w100 - label.font = .KorFont(style: .regular, size: 15) + label.font = .korFont(style: .regular, size: 15) return label }() - + // MARK: - Initializer - + init(message: String) { super.init(frame: .zero) setup() messageLabel.text = message } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension ToastView { - + // MARK: - Method - + private func setup() { addSubview(bgView) bgView.addSubview(messageLabel) - + bgView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalTo(snp.bottom) make.top.equalTo(snp.top) make.height.equalTo(38) } - + messageLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) make.centerY.equalToSuperview() diff --git a/Poppool/PresentationLayer/Presentation/PresentationInterface/Factory/DetailFactory.swift b/Poppool/PresentationLayer/Presentation/PresentationInterface/Factory/DetailFactory.swift new file mode 100644 index 00000000..c21eea95 --- /dev/null +++ b/Poppool/PresentationLayer/Presentation/PresentationInterface/Factory/DetailFactory.swift @@ -0,0 +1,5 @@ +import DesignSystem + +public protocol DetailFactory { + func make(popupID: Int) -> BaseViewController +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/project.pbxproj b/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/project.pbxproj new file mode 100644 index 00000000..271b78b4 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/project.pbxproj @@ -0,0 +1,981 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0506BE882DD79A6C006CDEDE /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 0506BE872DD79A6C006CDEDE /* RxCocoa */; }; + 052413092DCF7DA100C42E2D /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 052413082DCF7DA100C42E2D /* DesignSystem.framework */; }; + 054A96202DCE38B500C0DD58 /* SearchFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */; }; + 054A96212DCE38B500C0DD58 /* SearchFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 054A96262DCE38E900C0DD58 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = 054A96252DCE38E900C0DD58 /* Tabman */; }; + 054A96282DCE390A00C0DD58 /* ReactorKit in Frameworks */ = {isa = PBXBuildFile; productRef = 054A96272DCE390A00C0DD58 /* ReactorKit */; }; + 054A962B2DCE3A0300C0DD58 /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 054A962A2DCE3A0300C0DD58 /* NMapsMap */; }; + 05734C082DCDA7D20093825D /* SearchFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */; }; + 05734C2B2DCDD6380093825D /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C2A2DCDD6380093825D /* Infrastructure.framework */; }; + 05734C442DCDF7240093825D /* PresentationInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C432DCDF7240093825D /* PresentationInterface.framework */; }; + 05734C502DCDF8B40093825D /* PresentationInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C4F2DCDF8B40093825D /* PresentationInterface.framework */; }; + 05734C512DCDF8B40093825D /* PresentationInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C4F2DCDF8B40093825D /* PresentationInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05734C532DCDF8B80093825D /* Presentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C522DCDF8B80093825D /* Presentation.framework */; }; + 05734C542DCDF8B80093825D /* Presentation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05734C522DCDF8B80093825D /* Presentation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 058AE08D2DCCC27F009119B2 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 058AE08C2DCCC27F009119B2 /* Then */; }; + 05CFFBE52DCB8F6C0051129F /* SearchFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0516336D2DC457A900A6C0D1 /* SearchFeature.framework */; }; + 05CFFBE62DCB8F6C0051129F /* SearchFeature.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0516336D2DC457A900A6C0D1 /* SearchFeature.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBE92DCB90070051129F /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 05CFFBE82DCB90070051129F /* RxSwift */; }; + 05CFFBEA2DCB90210051129F /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC286A2DC5FE5B00C761A5 /* Data.framework */; }; + 05CFFBEB2DCB90210051129F /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC286A2DC5FE5B00C761A5 /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBEC2DCB90440051129F /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC28642DC5FDF800C761A5 /* Domain.framework */; }; + 05CFFBED2DCB90440051129F /* Domain.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC28642DC5FDF800C761A5 /* Domain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBEE2DCB90460051129F /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC28652DC5FDF800C761A5 /* DomainInterface.framework */; }; + 05CFFBEF2DCB90460051129F /* DomainInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC28652DC5FDF800C761A5 /* DomainInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBF02DCB90580051129F /* Infrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC286D2DC5FE6000C761A5 /* Infrastructure.framework */; }; + 05CFFBF12DCB90580051129F /* Infrastructure.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC286D2DC5FE6000C761A5 /* Infrastructure.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBF32DCB906F0051129F /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 05CFFBF22DCB906F0051129F /* RxCocoa */; }; + 05CFFBF42DCB908B0051129F /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC23422DC49AA200C761A5 /* DesignSystem.framework */; }; + 05CFFBF52DCB908B0051129F /* DesignSystem.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC23422DC49AA200C761A5 /* DesignSystem.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 05CFFBF72DCB90A10051129F /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05CFFBF62DCB90A10051129F /* SnapKit */; }; + 05EC233E2DC49A7600C761A5 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC233D2DC49A7600C761A5 /* RxSwift */; }; + 05EC23412DC49A8B00C761A5 /* ReactorKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC23402DC49A8B00C761A5 /* ReactorKit */; }; + 05EC23472DC49AA800C761A5 /* DomainInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC23462DC49AA800C761A5 /* DomainInterface.framework */; }; + 05EC234B2DC49AB400C761A5 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC234A2DC49AB400C761A5 /* Then */; }; + 05EC234E2DC49AC100C761A5 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC234D2DC49AC100C761A5 /* SnapKit */; }; + 05EC285F2DC5C1CF00C761A5 /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05EC23422DC49AA200C761A5 /* DesignSystem.framework */; }; + 05EC2AE92DC7C07400C761A5 /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 05EC2AE82DC7C07400C761A5 /* RxRelay */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0516364D2DC45E6300A6C0D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 051633642DC457A900A6C0D1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0516336C2DC457A900A6C0D1; + remoteInfo = SearchFeature; + }; + 054A96222DCE38B500C0DD58 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 051633642DC457A900A6C0D1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05734BF42DCDA6B90093825D; + remoteInfo = SearchFeatureInterface; + }; + 05734C0A2DCDA7D20093825D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 051633642DC457A900A6C0D1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05734BF42DCDA6B90093825D; + remoteInfo = SearchFeatureInterface; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 05CFFBE72DCB8F6C0051129F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 05CFFBED2DCB90440051129F /* Domain.framework in Embed Frameworks */, + 05CFFBEB2DCB90210051129F /* Data.framework in Embed Frameworks */, + 05734C512DCDF8B40093825D /* PresentationInterface.framework in Embed Frameworks */, + 054A96212DCE38B500C0DD58 /* SearchFeatureInterface.framework in Embed Frameworks */, + 05CFFBF12DCB90580051129F /* Infrastructure.framework in Embed Frameworks */, + 05CFFBF52DCB908B0051129F /* DesignSystem.framework in Embed Frameworks */, + 05CFFBEF2DCB90460051129F /* DomainInterface.framework in Embed Frameworks */, + 05734C542DCDF8B80093825D /* Presentation.framework in Embed Frameworks */, + 05CFFBE62DCB8F6C0051129F /* SearchFeature.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0516336D2DC457A900A6C0D1 /* SearchFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SearchFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0516362F2DC457DE00A6C0D1 /* SearchFeatureDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SearchFeatureDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 052413082DCF7DA100C42E2D /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SearchFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C232DCDD4CF0093825D /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C2A2DCDD6380093825D /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C432DCDF7240093825D /* PresentationInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PresentationInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C4F2DCDF8B40093825D /* PresentationInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PresentationInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05734C522DCDF8B80093825D /* Presentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Presentation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC23422DC49AA200C761A5 /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC23462DC49AA800C761A5 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC28642DC5FDF800C761A5 /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC28652DC5FDF800C761A5 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC286A2DC5FE5B00C761A5 /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05EC286D2DC5FE6000C761A5 /* Infrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Infrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 051636402DC457DF00A6C0D1 /* Exceptions for "SearchFeatureDemo" folder in "SearchFeatureDemo" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resource/Info.plist, + ); + target = 0516362E2DC457DE00A6C0D1 /* SearchFeatureDemo */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 0516336F2DC457A900A6C0D1 /* SearchFeature */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SearchFeature; + sourceTree = ""; + }; + 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 051636402DC457DF00A6C0D1 /* Exceptions for "SearchFeatureDemo" folder in "SearchFeatureDemo" target */, + ); + path = SearchFeatureDemo; + sourceTree = ""; + }; + 05734BF62DCDA6B90093825D /* SearchFeatureInterface */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SearchFeatureInterface; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0516336A2DC457A900A6C0D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05734C442DCDF7240093825D /* PresentationInterface.framework in Frameworks */, + 05EC2AE92DC7C07400C761A5 /* RxRelay in Frameworks */, + 0506BE882DD79A6C006CDEDE /* RxCocoa in Frameworks */, + 05EC285F2DC5C1CF00C761A5 /* DesignSystem.framework in Frameworks */, + 05EC23472DC49AA800C761A5 /* DomainInterface.framework in Frameworks */, + 05EC234B2DC49AB400C761A5 /* Then in Frameworks */, + 05EC233E2DC49A7600C761A5 /* RxSwift in Frameworks */, + 05EC234E2DC49AC100C761A5 /* SnapKit in Frameworks */, + 05734C082DCDA7D20093825D /* SearchFeatureInterface.framework in Frameworks */, + 05EC23412DC49A8B00C761A5 /* ReactorKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0516362C2DC457DE00A6C0D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CFFBEE2DCB90460051129F /* DomainInterface.framework in Frameworks */, + 05CFFBF42DCB908B0051129F /* DesignSystem.framework in Frameworks */, + 05734C532DCDF8B80093825D /* Presentation.framework in Frameworks */, + 05CFFBEC2DCB90440051129F /* Domain.framework in Frameworks */, + 05CFFBEA2DCB90210051129F /* Data.framework in Frameworks */, + 054A962B2DCE3A0300C0DD58 /* NMapsMap in Frameworks */, + 05CFFBF32DCB906F0051129F /* RxCocoa in Frameworks */, + 058AE08D2DCCC27F009119B2 /* Then in Frameworks */, + 05CFFBF02DCB90580051129F /* Infrastructure.framework in Frameworks */, + 054A96202DCE38B500C0DD58 /* SearchFeatureInterface.framework in Frameworks */, + 054A96262DCE38E900C0DD58 /* Tabman in Frameworks */, + 05CFFBF72DCB90A10051129F /* SnapKit in Frameworks */, + 05CFFBE92DCB90070051129F /* RxSwift in Frameworks */, + 05734C502DCDF8B40093825D /* PresentationInterface.framework in Frameworks */, + 054A96282DCE390A00C0DD58 /* ReactorKit in Frameworks */, + 05CFFBE52DCB8F6C0051129F /* SearchFeature.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05734BF22DCDA6B90093825D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05734C2B2DCDD6380093825D /* Infrastructure.framework in Frameworks */, + 052413092DCF7DA100C42E2D /* DesignSystem.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 051633632DC457A900A6C0D1 = { + isa = PBXGroup; + children = ( + 0516336F2DC457A900A6C0D1 /* SearchFeature */, + 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */, + 05734BF62DCDA6B90093825D /* SearchFeatureInterface */, + 0516364A2DC45E6300A6C0D1 /* Frameworks */, + 0516336E2DC457A900A6C0D1 /* Products */, + ); + sourceTree = ""; + }; + 0516336E2DC457A900A6C0D1 /* Products */ = { + isa = PBXGroup; + children = ( + 0516336D2DC457A900A6C0D1 /* SearchFeature.framework */, + 0516362F2DC457DE00A6C0D1 /* SearchFeatureDemo.app */, + 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */, + ); + name = Products; + sourceTree = ""; + }; + 0516364A2DC45E6300A6C0D1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 052413082DCF7DA100C42E2D /* DesignSystem.framework */, + 05734C522DCDF8B80093825D /* Presentation.framework */, + 05734C4F2DCDF8B40093825D /* PresentationInterface.framework */, + 05734C432DCDF7240093825D /* PresentationInterface.framework */, + 05734C2A2DCDD6380093825D /* Infrastructure.framework */, + 05734C232DCDD4CF0093825D /* DesignSystem.framework */, + 05EC286D2DC5FE6000C761A5 /* Infrastructure.framework */, + 05EC286A2DC5FE5B00C761A5 /* Data.framework */, + 05EC28642DC5FDF800C761A5 /* Domain.framework */, + 05EC28652DC5FDF800C761A5 /* DomainInterface.framework */, + 05EC23462DC49AA800C761A5 /* DomainInterface.framework */, + 05EC23422DC49AA200C761A5 /* DesignSystem.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 051633682DC457A900A6C0D1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05734BF02DCDA6B90093825D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 0516336C2DC457A900A6C0D1 /* SearchFeature */ = { + isa = PBXNativeTarget; + buildConfigurationList = 051633742DC457A900A6C0D1 /* Build configuration list for PBXNativeTarget "SearchFeature" */; + buildPhases = ( + 051633682DC457A900A6C0D1 /* Headers */, + 051633692DC457A900A6C0D1 /* Sources */, + 0516336A2DC457A900A6C0D1 /* Frameworks */, + 0516336B2DC457A900A6C0D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 05734C0B2DCDA7D20093825D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 0516336F2DC457A900A6C0D1 /* SearchFeature */, + ); + name = SearchFeature; + packageProductDependencies = ( + 05EC233D2DC49A7600C761A5 /* RxSwift */, + 05EC23402DC49A8B00C761A5 /* ReactorKit */, + 05EC234A2DC49AB400C761A5 /* Then */, + 05EC234D2DC49AC100C761A5 /* SnapKit */, + 05EC2AE82DC7C07400C761A5 /* RxRelay */, + 0506BE872DD79A6C006CDEDE /* RxCocoa */, + ); + productName = SearchFeature; + productReference = 0516336D2DC457A900A6C0D1 /* SearchFeature.framework */; + productType = "com.apple.product-type.framework"; + }; + 0516362E2DC457DE00A6C0D1 /* SearchFeatureDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 051636412DC457DF00A6C0D1 /* Build configuration list for PBXNativeTarget "SearchFeatureDemo" */; + buildPhases = ( + 0516362B2DC457DE00A6C0D1 /* Sources */, + 0516362C2DC457DE00A6C0D1 /* Frameworks */, + 0516362D2DC457DE00A6C0D1 /* Resources */, + 05CFFBE72DCB8F6C0051129F /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 0516364E2DC45E6300A6C0D1 /* PBXTargetDependency */, + 054A96232DCE38B500C0DD58 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */, + ); + name = SearchFeatureDemo; + packageProductDependencies = ( + 05CFFBE82DCB90070051129F /* RxSwift */, + 05CFFBF22DCB906F0051129F /* RxCocoa */, + 05CFFBF62DCB90A10051129F /* SnapKit */, + 058AE08C2DCCC27F009119B2 /* Then */, + 054A96252DCE38E900C0DD58 /* Tabman */, + 054A96272DCE390A00C0DD58 /* ReactorKit */, + 054A962A2DCE3A0300C0DD58 /* NMapsMap */, + ); + productName = SearchFeatureDemo; + productReference = 0516362F2DC457DE00A6C0D1 /* SearchFeatureDemo.app */; + productType = "com.apple.product-type.application"; + }; + 05734BF42DCDA6B90093825D /* SearchFeatureInterface */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05734BF92DCDA6B90093825D /* Build configuration list for PBXNativeTarget "SearchFeatureInterface" */; + buildPhases = ( + 05734BF02DCDA6B90093825D /* Headers */, + 05734BF12DCDA6B90093825D /* Sources */, + 05734BF22DCDA6B90093825D /* Frameworks */, + 05734BF32DCDA6B90093825D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 05734BF62DCDA6B90093825D /* SearchFeatureInterface */, + ); + name = SearchFeatureInterface; + packageProductDependencies = ( + ); + productName = SearchFeatureInterface; + productReference = 05734BF52DCDA6B90093825D /* SearchFeatureInterface.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 051633642DC457A900A6C0D1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 0516336C2DC457A900A6C0D1 = { + CreatedOnToolsVersion = 16.3; + LastSwiftMigration = 1630; + }; + 0516362E2DC457DE00A6C0D1 = { + CreatedOnToolsVersion = 16.3; + }; + 05734BF42DCDA6B90093825D = { + CreatedOnToolsVersion = 16.3; + }; + }; + }; + buildConfigurationList = 051633672DC457A900A6C0D1 /* Build configuration list for PBXProject "SearchFeature" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 051633632DC457A900A6C0D1; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */, + 05EC233F2DC49A8B00C761A5 /* XCRemoteSwiftPackageReference "ReactorKit" */, + 05EC23492DC49AB400C761A5 /* XCRemoteSwiftPackageReference "Then" */, + 05EC234C2DC49AC100C761A5 /* XCRemoteSwiftPackageReference "SnapKit" */, + 054A96242DCE38E900C0DD58 /* XCRemoteSwiftPackageReference "Tabman" */, + 054A96292DCE3A0300C0DD58 /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 0516336E2DC457A900A6C0D1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0516336C2DC457A900A6C0D1 /* SearchFeature */, + 05734BF42DCDA6B90093825D /* SearchFeatureInterface */, + 0516362E2DC457DE00A6C0D1 /* SearchFeatureDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0516336B2DC457A900A6C0D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0516362D2DC457DE00A6C0D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05734BF32DCDA6B90093825D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 051633692DC457A900A6C0D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0516362B2DC457DE00A6C0D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05734BF12DCDA6B90093825D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0516364E2DC45E6300A6C0D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0516336C2DC457A900A6C0D1 /* SearchFeature */; + targetProxy = 0516364D2DC45E6300A6C0D1 /* PBXContainerItemProxy */; + }; + 054A96232DCE38B500C0DD58 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05734BF42DCDA6B90093825D /* SearchFeatureInterface */; + targetProxy = 054A96222DCE38B500C0DD58 /* PBXContainerItemProxy */; + }; + 05734C0B2DCDA7D20093825D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05734BF42DCDA6B90093825D /* SearchFeatureInterface */; + targetProxy = 05734C0A2DCDA7D20093825D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 051633722DC457A900A6C0D1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */; + baseConfigurationReferenceRelativePath = Resource/Debug.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 051633732DC457A900A6C0D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 051633752DC457A900A6C0D1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */; + baseConfigurationReferenceRelativePath = Resource/Debug.xcconfig; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeature; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 051633762DC457A900A6C0D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeature; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 051636422DC457DF00A6C0D1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 051636302DC457DE00A6C0D1 /* SearchFeatureDemo */; + baseConfigurationReferenceRelativePath = Resource/Debug.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT = NO; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; + GENERATE_INFOPLIST_FILE = YES; + IBSC_COMPILER_AUTO_ACTIVATE_CUSTOM_FONTS = YES; + INFOPLIST_FILE = SearchFeatureDemo/Resource/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeatureDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = poppoolSearchFeatureDemoProvisioning; + SKIP_INSTALL = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_SKIP_AUTOLINKING_ALL_FRAMEWORKS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 051636432DC457DF00A6C0D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IBSC_COMPILER_AUTO_ACTIVATE_CUSTOM_FONTS = YES; + INFOPLIST_FILE = SearchFeatureDemo/Resource/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeatureDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_SKIP_AUTOLINKING_ALL_FRAMEWORKS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 05734BFA2DCDA6B90093825D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeatureInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 05734BFB2DCDA6B90093825D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.poppoolIOS.poppool.SearchFeatureInterface; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 051633672DC457A900A6C0D1 /* Build configuration list for PBXProject "SearchFeature" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 051633722DC457A900A6C0D1 /* Debug */, + 051633732DC457A900A6C0D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 051633742DC457A900A6C0D1 /* Build configuration list for PBXNativeTarget "SearchFeature" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 051633752DC457A900A6C0D1 /* Debug */, + 051633762DC457A900A6C0D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 051636412DC457DF00A6C0D1 /* Build configuration list for PBXNativeTarget "SearchFeatureDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 051636422DC457DF00A6C0D1 /* Debug */, + 051636432DC457DF00A6C0D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05734BF92DCDA6B90093825D /* Build configuration list for PBXNativeTarget "SearchFeatureInterface" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05734BFA2DCDA6B90093825D /* Debug */, + 05734BFB2DCDA6B90093825D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 054A96242DCE38E900C0DD58 /* XCRemoteSwiftPackageReference "Tabman" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uias/Tabman"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.0; + }; + }; + 054A96292DCE3A0300C0DD58 /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/navermaps/SPM-NMapsMap"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.21.0; + }; + }; + 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.0; + }; + }; + 05EC233F2DC49A8B00C761A5 /* XCRemoteSwiftPackageReference "ReactorKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactorKit/ReactorKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.0; + }; + }; + 05EC23492DC49AB400C761A5 /* XCRemoteSwiftPackageReference "Then" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devxoul/Then"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; + 05EC234C2DC49AC100C761A5 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.7.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0506BE872DD79A6C006CDEDE /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 054A96252DCE38E900C0DD58 /* Tabman */ = { + isa = XCSwiftPackageProductDependency; + package = 054A96242DCE38E900C0DD58 /* XCRemoteSwiftPackageReference "Tabman" */; + productName = Tabman; + }; + 054A96272DCE390A00C0DD58 /* ReactorKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233F2DC49A8B00C761A5 /* XCRemoteSwiftPackageReference "ReactorKit" */; + productName = ReactorKit; + }; + 054A962A2DCE3A0300C0DD58 /* NMapsMap */ = { + isa = XCSwiftPackageProductDependency; + package = 054A96292DCE3A0300C0DD58 /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */; + productName = NMapsMap; + }; + 058AE08C2DCCC27F009119B2 /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC23492DC49AB400C761A5 /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; + 05CFFBE82DCB90070051129F /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 05CFFBF22DCB906F0051129F /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 05CFFBF62DCB90A10051129F /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC234C2DC49AC100C761A5 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 05EC233D2DC49A7600C761A5 /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 05EC23402DC49A8B00C761A5 /* ReactorKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233F2DC49A8B00C761A5 /* XCRemoteSwiftPackageReference "ReactorKit" */; + productName = ReactorKit; + }; + 05EC234A2DC49AB400C761A5 /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC23492DC49AB400C761A5 /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; + 05EC234D2DC49AC100C761A5 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC234C2DC49AC100C761A5 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 05EC2AE82DC7C07400C761A5 /* RxRelay */ = { + isa = XCSwiftPackageProductDependency; + package = 05EC233A2DC49A7600C761A5 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxRelay; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 051633642DC457A900A6C0D1 /* Project object */; +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/xcshareddata/xcschemes/SearchFeatureDemo.xcscheme b/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/xcshareddata/xcschemes/SearchFeatureDemo.xcscheme new file mode 100644 index 00000000..5b3c548c --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature.xcodeproj/xcshareddata/xcschemes/SearchFeatureDemo.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Factory/CategorySelectorFactoryImpl.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Factory/CategorySelectorFactoryImpl.swift new file mode 100644 index 00000000..a1645730 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Factory/CategorySelectorFactoryImpl.swift @@ -0,0 +1,19 @@ +import DesignSystem +import DomainInterface +import Infrastructure +import SearchFeatureInterface + +public final class CategorySelectorFactoryImpl: CategorySelectorFactory { + public init() { } + + public func make() -> BaseViewController & PPModalPresentable { + let reactor = CategorySelectReactor( + fetchCategoryListUseCase: DIContainer.resolve(FetchCategoryListUseCase.self) + ) + let viewController = CategorySelectViewController() + + viewController.reactor = reactor + + return viewController + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Reactor/CategorySelectReactor.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Reactor/CategorySelectReactor.swift new file mode 100644 index 00000000..1682dbdf --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/Reactor/CategorySelectReactor.swift @@ -0,0 +1,129 @@ +import Foundation + +import DomainInterface + +import ReactorKit +import RxCocoa +import RxSwift + +final class CategorySelectReactor: Reactor { + + // MARK: - Reactor + enum Action { + case viewWillAppear + case closeButtonTapped + case resetButtonTapped + case saveButtonTapped + case categoryTagButtonTapped(indexPath: IndexPath) + } + + enum Mutation { + case setupCategotyTag(items: [TagModel]) + case dismiss + case resetCategory + case saveCategory + case updateCategoryTagSelection(categoryID: Int) + case updateSaveButtonEnable + case updateSelectedCategory + } + + struct State { + var categoryItems: [TagModel] = [] + var saveButtonIsEnable: Bool = false + var selectedCategoryChanged: Bool? + + @Pulse var categoryChanged: Void? + @Pulse var dismiss: Void? + } + + // MARK: - properties + var initialState: State + var disposeBag = DisposeBag() + + private var originCategoryItems: [TagModel] = [] + private let fetchCategoryListUseCase: FetchCategoryListUseCase + + // MARK: - init + init( + fetchCategoryListUseCase: FetchCategoryListUseCase + ) { + self.initialState = State() + self.fetchCategoryListUseCase = fetchCategoryListUseCase + } + + // MARK: - Reactor Methods + func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppear: + return fetchCategoryListUseCase.execute() + .withUnretained(self) + .map { (_, response) in + let items = response.map { + return TagModel(title: $0.category, id: $0.categoryId, isCancelable: false) + } + return .setupCategotyTag(items: items) + } + + case .closeButtonTapped: + return .just(.dismiss) + + case .resetButtonTapped: + return Observable.concat([ + .just(.resetCategory), + .just(.dismiss), + .just(.updateSelectedCategory) + ]) + + case .saveButtonTapped: + return Observable.concat([ + .just(.saveCategory), + .just(.dismiss), + .just(.updateSelectedCategory) + ]) + + case .categoryTagButtonTapped(let indexPath): + guard let categoryID = currentState.categoryItems[indexPath.row].id else { return .empty() } + return Observable.concat([ + .just(.updateCategoryTagSelection(categoryID: categoryID)), + .just(.updateSaveButtonEnable) + ]) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .setupCategotyTag(let items): + let fetchedItems = items.map { + if let id = $0.id, Category.shared.contains(id: id) { return $0.selectionToggledItem() } else { return $0 } + } + originCategoryItems = fetchedItems + newState.categoryItems = fetchedItems + + case .dismiss: + newState.dismiss = () + + case .resetCategory: + Category.shared.resetItems() + newState.categoryChanged = () + + case .saveCategory: + Category.shared.items = newState.categoryItems.filter { $0.isSelected == true } + newState.categoryChanged = () + + case .updateCategoryTagSelection(let categoryID): + newState.categoryItems = state.categoryItems.map { + if $0.id == categoryID { return $0.selectionToggledItem() } else { return $0 } + } + + case .updateSaveButtonEnable: + newState.saveButtonIsEnable = (originCategoryItems != newState.categoryItems) + + case .updateSelectedCategory: + newState.selectedCategoryChanged = (originCategoryItems != newState.categoryItems) + } + + return newState + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectView.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectView.swift new file mode 100644 index 00000000..0dcdd526 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectView.swift @@ -0,0 +1,114 @@ +import UIKit + +import DesignSystem + +import SnapKit +import Then + +final class CategorySelectView: UIView { + + // MARK: - Components + private let titleLabel: PPLabel = { + return PPLabel(style: .bold, fontSize: 18, text: "카테고리를 선택해주세요") + }() + + let closeButton = UIButton().then { + $0.setImage(UIImage(named: "icon_xmark"), for: .normal) + } + + lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()).then { + $0.setCollectionViewLayout(makeLayout(), animated: false) + $0.isScrollEnabled = false + + $0.register( + PPTagCollectionViewCell.self, + forCellWithReuseIdentifier: PPTagCollectionViewCell.identifiers + ) + } + + let buttonStackView = UIStackView().then { + $0.distribution = .fillEqually + $0.spacing = 12 + } + + let resetButton = PPButton(style: .secondary, text: "초기화") + + let saveButton = PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장") + + // MARK: - init + init() { + super.init(frame: .zero) + + self.addViews() + self.setupConstraints() + } + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension CategorySelectView { + + func addViews() { + [titleLabel, closeButton, collectionView, buttonStackView].forEach { + self.addSubview($0) + } + + [resetButton, saveButton].forEach { + buttonStackView.addArrangedSubview($0) + } + } + + // FIXME: 레이아웃 에러로 인한 Modal이 살짝 내려가지는 문제 발생중 + func setupConstraints() { + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(32) + make.leading.equalTo(safeAreaLayoutGuide).inset(20) + } + + closeButton.snp.makeConstraints { make in + make.size.equalTo(24) + make.trailing.equalTo(safeAreaLayoutGuide).inset(20) + make.centerY.equalTo(titleLabel) + } + + collectionView.snp.makeConstraints { make in + make.horizontalEdges.equalTo(safeAreaLayoutGuide) + make.top.equalTo(titleLabel.snp.bottom).offset(24) + make.bottom.equalTo(buttonStackView.snp.top).offset(24) + } + + buttonStackView.snp.makeConstraints { make in + make.leading.trailing.equalTo(safeAreaLayoutGuide).inset(20) + make.height.equalTo(52) + make.bottom.equalTo(safeAreaLayoutGuide) + } + } +} + +private extension CategorySelectView { + + func makeLayout() -> UICollectionViewLayout { + return UICollectionViewCompositionalLayout { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(26), + heightDimension: .absolute(36) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(200) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(12) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) + section.interGroupSpacing = 16 + + return section + } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectViewController.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectViewController.swift new file mode 100644 index 00000000..4d6a233a --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/CatagorySelect/View/CategorySelectViewController.swift @@ -0,0 +1,96 @@ +import UIKit + +import DesignSystem +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit + +final class CategorySelectViewController: BaseViewController, View { + + typealias Reactor = CategorySelectReactor + + // MARK: - Properties + var disposeBag = DisposeBag() + + private var mainView = CategorySelectView() +} + +// MARK: - Life Cycle +extension CategorySelectViewController { + override func loadView() { + self.view = mainView + } +} + +// MARK: - Methods +extension CategorySelectViewController { + func bind(reactor: Reactor) { + self.bindAction(reactor: reactor) + self.bindState(reactor: reactor) + } + + private func bindAction(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.closeButton.rx.tap + .map { Reactor.Action.closeButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.resetButton.rx.tap + .map { Reactor.Action.resetButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.saveButton.rx.tap + .map { Reactor.Action.saveButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.collectionView.rx.itemSelected + .map(Reactor.Action.categoryTagButtonTapped) + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindState(reactor: Reactor) { + reactor.pulse(\.$dismiss) + .withUnretained(self) + .subscribe { (owner, _) in owner.dismissModal() } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.categoryItems) + .map(\.categoryItems) + .bind(to: mainView.collectionView.rx.items( + cellIdentifier: PPTagCollectionViewCell.identifiers, + cellType: PPTagCollectionViewCell.self + )) { _, item, cell in + cell.configureCell(title: item.title, id: item.id, isSelected: item.isSelected, isCancelable: item.isCancelable, fontSize: 13, cornerRadius: 18) + } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.saveButtonIsEnable) + .withUnretained(self) + .subscribe { (owner, state) in owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable } + .disposed(by: disposeBag) + + reactor.pulse(\.$categoryChanged) + .skip(1) + .subscribe { _ in Category.valueChanged.onNext(()) } + .disposed(by: disposeBag) + } +} + +extension CategorySelectViewController: PPModalPresentable { + var modalHeight: CGFloat? { return 384 } + + var backgroundColor: UIColor { return .pb60 } + + var cornerRadius: CGFloat { return 20 } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Category.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Category.swift new file mode 100644 index 00000000..ff2fe9c4 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Category.swift @@ -0,0 +1,60 @@ +import Foundation + +import RxSwift + +final class Category: NSCopying, Equatable { + func copy(with zone: NSZone? = nil) -> Any { + return Category(items: self.items) + } + + static func == (lhs: Category, rhs: Category) -> Bool { return lhs === rhs } + + static let shared = Category() + static let valueChanged = PublishSubject() + + /// 선택된 아이템들만 들어가는 인스턴스 + private var _items: [TagModel] + var items: [TagModel] { + get { _items } + set { _items = newValue.isEmpty ? [Category.defaultItem] : newValue } + } + + private static let defaultItem = TagModel(title: "카테고리", isSelected: false, isCancelable: false) + + private init(items: [TagModel] = [Category.defaultItem]) { + self._items = items.isEmpty ? [Category.defaultItem] : items + } +} + +// MARK: - Functions +extension Category { + + func contains(id: Int) -> Bool { + items.contains { $0.id == id } + } + + func toggleItemSelection(by categoryID: Int) { + guard let index = items.firstIndex(where: { $0.id == categoryID }) else { return } + items[index].isSelected.toggle() + } + + func turnOffAllItemSelection() { + for index in items.indices { items[index].isSelected = false } + } + + func resetItems() { + items = [Category.defaultItem] + } + + func getSelectedCategoryIDs() -> [Int] { + return items.filter { $0.isSelected == true }.compactMap { $0.id } + } + + func getCancelableCategoryItems() -> [TagModel] { + if items == [Category.defaultItem] { return items } else { return items.filter { $0.isSelected == true }.map { $0.cancelableItem() } } + } + + func removeItem(by categoryID: Int) { + items.removeAll { $0.id == categoryID } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Filter.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Filter.swift new file mode 100644 index 00000000..a24591c1 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/Filter.swift @@ -0,0 +1,82 @@ +import Foundation + +import RxSwift + +/// 필터 옵션 상태를 공유하기 위한 싱글톤 객체 +final class Filter: NSCopying, Equatable { + func copy(with zone: NSZone? = nil) -> Any { + return Filter( + status: self.status, + sort: self.sort + ) + } + + static func == (lhs: Filter, rhs: Filter) -> Bool { return (lhs.status == rhs.status) && (lhs.sort == rhs.sort) } + + static let shared = Filter(status: .open, sort: .newest) + static let valueChanged = PublishSubject() + + var status: PopupStatus = .open + var sort: PopupSort = .newest + + private init(status: PopupStatus, sort: PopupSort) { + self.status = status + self.sort = sort + } + + var title: String { [status.title, sort.title].joined(separator: "・") } +} + +/// 팝업 상점이 현재 열려 있는지 또는 닫혀 있는지 여부를 나타냅니다 +enum PopupStatus: CaseIterable { + case open + case closed + + /// UI 용 문자열 표시 (예 : 세그먼트 제목) + var title: String { + switch self { + case .open: return "오픈" + case .closed: return "종료" + } + } + + /// API 요청에 포함 할 값 + var requestValue: Bool { + switch self { + case .open: return true + case .closed: return false + } + } + + /// UISegmentedControl과 같은 UI 구성 요소의 색인 + var index: Int { + return Self.allCases.firstIndex(of: self)! + } +} + +/// 팝업 검색 결과를위한 정렬 옵션을 나타냅니다 +enum PopupSort: CaseIterable { + case newest + case popularity + + /// UI 용 문자열 표시 (예 : 세그먼트 제목) + var title: String { + switch self { + case .newest: return "신규순" + case .popularity: return "인기순" + } + } + + /// API 요청에 포함 할 값 + var requestValue: String { + switch self { + case .newest: return "NEWEST" + case .popularity: return "MOST_VIEWED,MOST_COMMENTED,MOST_BOOKMARKED" + } + } + + /// UISegmentedControl과 같은 UI 구성 요소의 색인 + var index: Int { + return Self.allCases.firstIndex(of: self)! + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultHeaderModel.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultHeaderModel.swift new file mode 100644 index 00000000..1a68a0ba --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultHeaderModel.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct SearchResultHeaderModel: Hashable { + public init(title: String? = nil, count: Int? = 0, filterText: String?) { + self.title = title + self.count = count + self.filterText = filterText + } + + var title: String? = nil + var count: Int? = 0 + var filterText: String? = Filter.shared.title +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultModel.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultModel.swift new file mode 100644 index 00000000..c52314a0 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/SearchResultModel.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct SearchResultModel: Hashable { + var imagePath: String? + var id: Int64 + var category: String? + var title: String? + var address: String? + var startDate: String? + var endDate: String? + var isBookmark: Bool + var isLogin: Bool + var isPopular: Bool = false + var row: Int? + + enum EmptyCase { + case option + case keyword + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/TagModel.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/TagModel.swift new file mode 100644 index 00000000..98e30f69 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/Model/TagModel.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct TagModel: Hashable { + var title: String? + var id: Int? = nil + var isSelected: Bool = false + var isCancelable: Bool = true + + func selectionToggledItem() -> TagModel { + let toggledSelection = !isSelected + return TagModel(title: self.title, id: self.id, isSelected: toggledSelection, isCancelable: self.isCancelable) + } + + func cancelableItem() -> TagModel { + return TagModel(title: self.title, id: self.id, isSelected: self.isSelected, isCancelable: true) + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/View/Component/TagCollectionHeaderView.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/View/Component/TagCollectionHeaderView.swift new file mode 100644 index 00000000..99425b24 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/Common/View/Component/TagCollectionHeaderView.swift @@ -0,0 +1,76 @@ +import UIKit + +import DesignSystem + +import RxSwift +import SnapKit + +final class TagCollectionHeaderView: UICollectionReusableView { + + // MARK: - Components + var disposeBag = DisposeBag() + + private let sectionTitleLabel = UILabel().then { + $0.font = .korFont(style: .bold, size: 16) + } + + let removeAllButton = UIButton().then { + $0.isHidden = true + + } + // MARK: - init + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addViews() + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } +} + +// MARK: - SetUp +private extension TagCollectionHeaderView { + func addViews() { + [sectionTitleLabel, removeAllButton].forEach { + self.addSubview($0) + } + } + + func setupConstraints() { + sectionTitleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(22) + } + + removeAllButton.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.centerY.equalTo(sectionTitleLabel) + make.height.equalTo(20) + } + } +} + +extension TagCollectionHeaderView { + func configureHeader(title: String, buttonTitle: String? = nil) { + sectionTitleLabel.text = title + if let buttonTitle = buttonTitle { + removeAllButton.isHidden = false + let attributes: [NSAttributedString.Key: Any] = [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .font: UIFont.korFont(style: .regular, size: 13) + ] + let attributedTitle = NSAttributedString(string: buttonTitle, attributes: attributes) + removeAllButton.setAttributedTitle(attributedTitle, for: .normal) + } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Factory/FilterSelectorFactoryImpl.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Factory/FilterSelectorFactoryImpl.swift new file mode 100644 index 00000000..95e8d2f2 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Factory/FilterSelectorFactoryImpl.swift @@ -0,0 +1,15 @@ +import DesignSystem +import Infrastructure +import SearchFeatureInterface + +public final class FilterSelectorFactoryImpl: FilterSelectorFactory { + public init() { } + + public func make() -> BaseViewController & PPModalPresentable { + let reactor = FilterSelectReactor() + let viewController = FilterSelectViewController() + viewController.reactor = reactor + + return viewController + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Reactor/FilterSelectReactor.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Reactor/FilterSelectReactor.swift new file mode 100644 index 00000000..b0deb372 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/Reactor/FilterSelectReactor.swift @@ -0,0 +1,109 @@ +import Foundation + +import ReactorKit +import RxCocoa +import RxSwift + +final class FilterSelectReactor: Reactor { + + // MARK: - Reactor + enum Action { + case closeButtonTapped + case statusSegmentChanged(index: Int) + case sortSegmentChanged(index: Int) + case saveButtonTapped + } + + enum Mutation { + case dismiss + case changeStatus(status: PopupStatus) + case changeSort(sort: PopupSort) + case updateSaveButtonEnable + case saveCurrentFilter + } + + struct State { + var selectedFilter: Filter + var saveButtonIsEnable: Bool = false + + @Pulse var filterChanged: Void? + @Pulse var dismiss: Void? + } + + // MARK: - properties + + var initialState: State + var disposeBag = DisposeBag() + + // MARK: - init + init() { + self.initialState = State(selectedFilter: Filter.shared.copy() as! Filter) + } + + // MARK: - Reactor Methods + func mutate(action: Action) -> Observable { + switch action { + case .closeButtonTapped: + return .just(.dismiss) + + case .statusSegmentChanged(let index): + switch index == 0 { + case true: + return .concat([ + .just(.changeStatus(status: .open)), + .just(.updateSaveButtonEnable) + ]) + case false: + return .concat([ + .just(.changeStatus(status: .closed)), + .just(.updateSaveButtonEnable) + ]) + } + + case .sortSegmentChanged(let index): + switch index == 0 { + case true: + return .concat([ + .just(.changeSort(sort: .newest)), + .just(.updateSaveButtonEnable) + ]) + case false: + return .concat([ + .just(.changeSort(sort: .popularity)), + .just(.updateSaveButtonEnable) + ]) + } + + case .saveButtonTapped: + return .concat([ + .just(.saveCurrentFilter), + .just(.dismiss) + ]) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .dismiss: + newState.dismiss = () + + case .changeStatus(let status): + newState.selectedFilter.status = status + + case .changeSort(let sort): + newState.selectedFilter.sort = sort + + case .updateSaveButtonEnable: + newState.saveButtonIsEnable = (newState.selectedFilter != Filter.shared) + + case .saveCurrentFilter: + Filter.shared.status = newState.selectedFilter.status + Filter.shared.sort = newState.selectedFilter.sort + newState.filterChanged = () + } + + return newState + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectView.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectView.swift new file mode 100644 index 00000000..efbbb69a --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectView.swift @@ -0,0 +1,89 @@ +import UIKit + +import DesignSystem + +import SnapKit + +final class FilterSelectView: UIView { + + // MARK: - Components + private let titleLabel = PPLabel(style: .bold, fontSize: 18, text: "노출 순서를 선택해주세요") + + let closeButton = UIButton().then { + $0.setImage(UIImage(named: "icon_xmark"), for: .normal) + } + + private let statusLabel = PPLabel(style: .regular, fontSize: 13, text: "노출 조건") + + let statusSegmentControl = PPSegmentedControl(type: .base, segments: ["오픈", "종료"], selectedSegmentIndex: 0) + + private let sortLabel = PPLabel(style: .regular, fontSize: 13, text: "팝업순서") + + let sortSegmentControl = PPSegmentedControl(type: .base, segments: ["신규순", "인기순"], selectedSegmentIndex: 0) + + let saveButton = PPButton(style: .primary, text: "저장", disabledText: "저장") + + // MARK: - init + init() { + super.init(frame: .zero) + + self.addViews() + self.setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension FilterSelectView { + func addViews() { + [titleLabel, closeButton, statusLabel, statusSegmentControl, + sortLabel, sortSegmentControl, saveButton].forEach { + self.addSubview($0) + } + } + + // FIXME: 레이아웃 에러로 인한 Modal이 살짝 내려가지는 문제 발생중 + func setupConstraints() { + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(32) + make.leading.equalTo(safeAreaLayoutGuide).inset(20) + } + + closeButton.snp.makeConstraints { make in + make.size.equalTo(24) + make.trailing.equalTo(safeAreaLayoutGuide).inset(20) + make.centerY.equalTo(titleLabel) + } + + statusLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(36) + make.leading.equalTo(safeAreaLayoutGuide).inset(20) + } + + statusSegmentControl.snp.makeConstraints { make in + make.top.equalTo(statusLabel.snp.bottom).offset(8) + make.leading.trailing.equalTo(safeAreaLayoutGuide).inset(20) + } + + sortLabel.snp.makeConstraints { make in + make.top.equalTo(statusSegmentControl.snp.bottom).offset(20) + make.leading.equalTo(safeAreaLayoutGuide).inset(20) + } + + sortSegmentControl.snp.makeConstraints { make in + make.top.equalTo(sortLabel.snp.bottom).offset(8) + make.horizontalEdges.equalTo(safeAreaLayoutGuide).inset(20) + } + + saveButton.snp.makeConstraints { make in + make.top.equalTo(sortSegmentControl.snp.bottom).offset(32) + make.height.equalTo(50) + make.horizontalEdges.equalTo(safeAreaLayoutGuide.snp.horizontalEdges).inset(20) + make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom) + } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectViewController.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectViewController.swift new file mode 100644 index 00000000..5a50254b --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/FilterSelector/View/FilterSelectViewController.swift @@ -0,0 +1,92 @@ +import UIKit + +import DesignSystem +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit + +final class FilterSelectViewController: BaseViewController, View { + + typealias Reactor = FilterSelectReactor + + // MARK: - Properties + var disposeBag = DisposeBag() + + private var mainView = FilterSelectView() +} + +// MARK: - Life Cycle +extension FilterSelectViewController { + override func loadView() { + self.view = mainView + } +} + +// MARK: - Methods +extension FilterSelectViewController { + func bind(reactor: Reactor) { + self.bindAction(reactor: reactor) + self.bindState(reactor: reactor) + } + + private func bindAction(reactor: Reactor) { + mainView.closeButton.rx.tap + .map { _ in Reactor.Action.closeButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.statusSegmentControl.rx.controlEvent(.valueChanged) + .withUnretained(self) + .map { (owner, _) in Reactor.Action.statusSegmentChanged(index: owner.mainView.statusSegmentControl.selectedSegmentIndex) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.sortSegmentControl.rx.controlEvent(.valueChanged) + .withUnretained(self) + .map { (owner, _) in Reactor.Action.sortSegmentChanged(index: owner.mainView.sortSegmentControl.selectedSegmentIndex) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.saveButton.rx.tap + .withUnretained(self) + .map { (_, _) in Reactor.Action.saveButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindState(reactor: Reactor) { + reactor.state.distinctUntilChanged(\.selectedFilter) + .withUnretained(self) + .subscribe { (owner, state) in + owner.mainView.statusSegmentControl.selectedSegmentIndex = state.selectedFilter.status.index + owner.mainView.sortSegmentControl.selectedSegmentIndex = state.selectedFilter.sort.index + } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.saveButtonIsEnable) + .withUnretained(self) + .subscribe { (owner, state) in owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable } + .disposed(by: disposeBag) + + reactor.pulse(\.$dismiss) + .withUnretained(self) + .subscribe { (owner, _) in owner.dismissModal() } + .disposed(by: disposeBag) + + reactor.pulse(\.$filterChanged) + .skip(1) + .subscribe { _ in Filter.valueChanged.onNext(()) } + .disposed(by: disposeBag) + } +} + +extension FilterSelectViewController: PPModalPresentable { + var modalHeight: CGFloat? { return 379 } + + var backgroundColor: UIColor { return .pb60 } + + var cornerRadius: CGFloat { return 20 } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchFactoryImpl.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchFactoryImpl.swift new file mode 100644 index 00000000..a352ad86 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchFactoryImpl.swift @@ -0,0 +1,20 @@ +import DesignSystem +import DomainInterface +import Infrastructure +import SearchFeatureInterface + +public final class PopupSearchFactoryImpl: PopupSearchFactory { + public init() { } + + public func make() -> BaseViewController { + let viewController = PopupSearchViewController() + + viewController.reactor = PopupSearchReactor( + popupAPIUseCase: DIContainer.resolve(PopUpAPIUseCase.self), + userAPIUseCase: DIContainer.resolve(UserAPIUseCase.self), + fetchKeywordBasePopupListUseCase: DIContainer.resolve(FetchKeywordBasePopupListUseCase.self) + ) + + return viewController + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchLayoutFactory.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchLayoutFactory.swift new file mode 100644 index 00000000..e134a780 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Factory/PopupSearchLayoutFactory.swift @@ -0,0 +1,78 @@ +import UIKit + +import DesignSystem + +// MARK: - Layout +struct PopupSearchLayoutFactory { + private let tagLayoutProvider = TagCollectionLayoutProvider() + private let gridLayoutProvider = GridCollectionLayoutProvider() + + private var sectionProvider: ((Int) -> PopupSearchSection?)? + + mutating func setSectionProvider(_ provider: @escaping (Int) -> PopupSearchSection?) { + self.sectionProvider = provider + } + + func makeCollectionViewLayout() -> UICollectionViewLayout { + return UICollectionViewCompositionalLayout { (sectionIndex, _) -> NSCollectionLayoutSection? in + + guard let sectionType = sectionProvider?(sectionIndex) else { return nil } + + switch sectionType { + case .recentSearch: + return makeRecentSearchSectionLayout() + + case .category: + return makeCategorySectionLayout() + + case .searchResultHeader: + return makeSearchResultHeaderSectionLayout() + + case .searchResult: + return self.gridLayoutProvider.makeLayout() + + case .searchResultEmpty: + return makeSearchResultEmptySectionLayout() + } + } + } + + private func makeRecentSearchSectionLayout() -> NSCollectionLayoutSection { + + return CollectionLayoutBuilder(section: tagLayoutProvider.makeLayout()) + .withContentInsets(top: 16, leading: 20, bottom: 48, trailing: 20) + .header([self.tagLayoutProvider.makeHeaderLayout( + PopupSearchView.SectionHeaderKind.recentSearch.rawValue + )]) + .build() + } + + private func makeCategorySectionLayout() -> NSCollectionLayoutSection { + + return CollectionLayoutBuilder(section: tagLayoutProvider.makeLayout()) + .withContentInsets(top: 16, leading: 20, bottom: 16, trailing: 20) + .header([ + tagLayoutProvider.makeHeaderLayout(PopupSearchView.SectionHeaderKind.category.rawValue) + ]) + .build() + } + + private func makeSearchResultHeaderSectionLayout() -> NSCollectionLayoutSection { + + return CollectionLayoutBuilder() + .item(width: .fractionalWidth(1.0), height: .estimated(22)) + .group(width: .fractionalWidth(1.0), height: .estimated(22)) + .composeSection(.horizontal) + .withContentInsets(top: 0, leading: 20, bottom: 0, trailing: 20) + .build() + } + + private func makeSearchResultEmptySectionLayout() -> NSCollectionLayoutSection { + + return CollectionLayoutBuilder() + .item(width: .fractionalWidth(1.0), height: .fractionalHeight(1.0)) + .group(width: .fractionalWidth(1.0), height: .fractionalHeight(1.0)) + .composeSection(.vertical) + .build() + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Reactor/PopupSearchReactor.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Reactor/PopupSearchReactor.swift new file mode 100644 index 00000000..2c931308 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/Reactor/PopupSearchReactor.swift @@ -0,0 +1,487 @@ +import Foundation + +import DomainInterface +import Infrastructure + +import ReactorKit +import RxCocoa +import RxSwift + +public final class PopupSearchReactor: Reactor { + + // MARK: - Reactor + public enum Action { + case viewDidLoad + + case searchBarEditing(text: String) + case searchBarExitEditing(text: String) + case searchBarEndEditing + case searchBarClearButtonTapped + case searchBarCancelButtonTapped + + case recentSearchTagButtonTapped(indexPath: IndexPath) + case recentSearchTagRemoveButtonTapped(text: String) + case recentSearchTagRemoveAllButtonTapped + + case categoryTagRemoveButtonTapped(categoryID: Int) + case categoryTagButtonTapped + case categoryChangedBySelector + + case searchResultFilterButtonTapped + case searchResultFilterChangedBySelector + case searchResultItemTapped(indexPath: IndexPath) + case searchResultBookmarkButtonTapped(indexPath: IndexPath) + case searchResultPrefetchItems(indexPathList: [IndexPath]) + } + + public enum Mutation { + case setupRecentSearch(items: [TagModel]) + case setupCategory(items: [TagModel]) + case setupSearchResult(items: [SearchResultModel]) + case setupSearchResultHeader(item: SearchResultHeaderModel) + case setupSearchResultTotalPageCount(count: Int32) + + case appendSearchResult(items: [SearchResultModel]) + + case updateEditingState + case updateSearchBar(to: String?) + case updateClearButtonIsHidden(to: Bool) + case updateCurrentPage(to: Int32) + case updateSearchingState(to: Bool) + case updateSearchResultBookmark(indexPath: IndexPath) + case updateSearchResultSection + + case present(target: PresentTarget) + } + + @frozen + public enum PresentTarget { + case categorySelector + case filterSelector + case popupDetail(popupID: Int) + case before + } + + public struct State { + var recentSearchItems: [TagModel] = [] + var categoryItems: [TagModel] = [] + var searchResultItems: [SearchResultModel] = [] + var searchResultHeader: SearchResultHeaderModel = SearchResultHeaderModel(filterText: Filter.shared.title) + + @Pulse var searchBarText: String? = nil + @Pulse var present: PresentTarget? + @Pulse var clearButtonIsHidden: Bool? + @Pulse var endEditing: Void? + @Pulse var updateSearchResultSection: String? + @Pulse var dismiss: Void? + + fileprivate var isSearching: Bool = false + fileprivate var currentPage: Int32 = 0 + fileprivate let paginationSize: Int32 = 10 + fileprivate var totalPagesCount: Int32 = 0 + } + + // MARK: - properties + public var initialState: State + + var disposeBag = DisposeBag() + + private let userDefaultService = UserDefaultService() + private let popupAPIUseCase: PopUpAPIUseCase + private let userAPIUseCase: UserAPIUseCase + private let fetchKeywordBasePopupListUseCase: FetchKeywordBasePopupListUseCase + + // MARK: - init + public init( + popupAPIUseCase: PopUpAPIUseCase, + userAPIUseCase: UserAPIUseCase, + fetchKeywordBasePopupListUseCase: FetchKeywordBasePopupListUseCase + ) { + self.popupAPIUseCase = popupAPIUseCase + self.userAPIUseCase = userAPIUseCase + self.fetchKeywordBasePopupListUseCase = fetchKeywordBasePopupListUseCase + self.initialState = State() + } + + // MARK: - Reactor Methods + public func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + return handleViewDidLoad() + + case .searchBarEditing(let text): + return handleSearchBarEditing(text) + + case .searchBarExitEditing(let text): + return handleSearchBarExitEditing(text) + + case .searchBarEndEditing: + return handleSearchBarEndEditing() + + case .searchBarClearButtonTapped: + return handleSearchBarClear() + + case .searchBarCancelButtonTapped: + return handleSearchBarCancel() + + case .recentSearchTagButtonTapped(let indexPath): + return handleRecentSearchTagTap(at: indexPath) + + case .recentSearchTagRemoveButtonTapped(let text): + return handleRecentSearchTagRemove(text) + + case .recentSearchTagRemoveAllButtonTapped: + return handleRecentSearchTagRemoveAll() + + case .categoryTagRemoveButtonTapped(let categoryID): + return handleCategoryTagRemove(categoryID) + + case .categoryTagButtonTapped: + return .just(.present(target: .categorySelector)) + + case .categoryChangedBySelector: + return handleCategoryChanged() + + case .searchResultFilterButtonTapped: + return .just(.present(target: .filterSelector)) + + case .searchResultFilterChangedBySelector: + return handleFilterChanged() + + case .searchResultItemTapped(let indexPath): + return handleSearchResultItemTap(at: indexPath) + + case .searchResultBookmarkButtonTapped(let indexPath): + return handleSearchResultBookmark(at: indexPath) + + case .searchResultPrefetchItems(let indexPathList): + return handleSearchResultPrefetch(at: indexPathList) + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setupRecentSearch(let items): + newState.recentSearchItems = items + + case .setupCategory(let items): + newState.categoryItems = items + + case .setupSearchResult(let items): + newState.searchResultItems = items + + case .setupSearchResultHeader(let input): + newState.searchResultHeader = input + + case .setupSearchResultTotalPageCount(let count): + newState.totalPagesCount = count + + case .appendSearchResult(let items): + newState.searchResultItems += items + + case .updateEditingState: + newState.endEditing = () + + case .updateSearchBar(let text): + newState.searchBarText = text + + case .updateClearButtonIsHidden(let state): + newState.clearButtonIsHidden = state + + case .updateCurrentPage(let currentPage): + newState.currentPage = currentPage + + case .updateSearchingState(let isSearching): + newState.isSearching = isSearching + + case .updateSearchResultBookmark(let indexPath): + newState.searchResultItems[indexPath.item].isBookmark.toggle() + + case .updateSearchResultSection: + newState.updateSearchResultSection = makeSearchResultEmpty(state: newState) + + case .present(let target): + newState.present = target + } + + return newState + } +} + +// MARK: Captulation Mutate +private extension PopupSearchReactor { + + func fetchSearchResult( + isOpen: Bool = Filter.shared.status.requestValue, + categories: [Int] = Category.shared.getSelectedCategoryIDs(), + page: Int32 = 0, + size: Int32 = 10, + sort: String = Filter.shared.sort.requestValue + ) -> Observable { + return popupAPIUseCase.getSearchBottomPopUpList( + isOpen: isOpen, + categories: categories, + page: page, + size: size, + sort: sort + ) + } + + func fetchSearchResult(keyword: String?) -> Observable { + guard let keyword else { return .empty() } + return fetchKeywordBasePopupListUseCase.execute(keyword: keyword) + } + + func fetchSearchResultBookmark(at indexPath: IndexPath) -> Completable { + let popupID = currentState.searchResultItems[indexPath.item].id + if currentState.searchResultItems[indexPath.item].isBookmark { + return userAPIUseCase.deleteBookmarkPopUp(popUpID: popupID) + } else { + return userAPIUseCase.postBookmarkPopUp(popUpID: popupID) + } + } +} + +// MARK: - Make Functions +private extension PopupSearchReactor { + func findRecentSearchKeyword(at indexPath: IndexPath) -> String? { + guard currentState.recentSearchItems.indices.contains(indexPath.item) + else { return nil } + + return currentState.recentSearchItems[indexPath.item].title + } + + func makeRecentSearchItems() -> [TagModel] { + let searchKeywords = userDefaultService.fetchArray(keyType: .searchKeyword) ?? [] + return searchKeywords.prefix(10).map { TagModel(title: $0) } + } + + func makeCategoryItems() -> [TagModel] { + return Category.shared.getCancelableCategoryItems() + } + + func makeSearchResultItems(_ popupStoreList: [PopUpStoreResponse], _ loginYn: Bool) -> [SearchResultModel] { + return popupStoreList.map { + SearchResultModel( + imagePath: $0.mainImageUrl, + id: $0.id, + category: $0.category, + title: $0.name, + address: $0.address, + startDate: $0.startDate, + endDate: $0.endDate, + isBookmark: $0.bookmarkYn, + isLogin: loginYn + ) + } + } + + // 빈 화면에서 탭할때 문제 + func findPopupStoreID(at indexPath: IndexPath) -> Int? { + guard currentState.searchResultItems.indices.contains(indexPath.item) else { return nil } + return Int(currentState.searchResultItems[indexPath.item].id) + } + + func makeSearchResultHeaderInput( + keyword afterTitle: String? = nil, + count: Int64, + filter filterTitle: String? = Filter.shared.title) -> SearchResultHeaderModel { + return SearchResultHeaderModel( + title: afterTitle, + count: Int(count), + filterText: filterTitle + ) + } + + func makeSearchResultEmpty(state: State) -> String? { + if !currentState.searchResultItems.isEmpty { return nil } else if currentState.isSearching { return "검색 결과가 없어요 :(\n다른 키워드로 검색해주세요" } else { return "검색 결과가 없어요 :(\n다른 옵션을 선택해주세요" } + } + + /// 받침에 따라 이/가 를 판단해서 붙여준다. + func makePostPositionedText(_ text: String?) -> String { + + guard let text, let lastCharacter = text.last else { return "" } + + let unicodeValue = Int(lastCharacter.unicodeScalars.first!.value) + + // 한글 유니코드 범위 체크 + let base = 0xAC00 + let last = 0xD7A3 + guard base...last ~= unicodeValue else { return text + "가" } + + // 종성 인덱스 계산 (받침이 있으면 1 이상) + let finalConsonantIndex = (unicodeValue - base) % 28 + return (finalConsonantIndex != 0) ? text + "이" : text + "가" + } +} + +// MARK: - Remove Funtions +private extension PopupSearchReactor { + func removeRecentSearchItem(text: String) { + guard let searchKeywords = userDefaultService.fetchArray(keyType: .searchKeyword) else { return } + userDefaultService.save(keyType: .searchKeyword, value: searchKeywords.filter { $0 != text }) + } + + func removeAllRecentSearchItems() { + userDefaultService.delete(keyType: .searchKeyword) + } + + func removeCategoryItem(by categoryID: Int) { + Category.shared.removeItem(by: categoryID) + } +} + +// MARK: - Checking Method +private extension PopupSearchReactor { + func isPrefetchable(prefetchCount: Int = 4, indexPathList: [IndexPath]) -> Bool { + guard let lastItemIndex = indexPathList.last?.last else { return false } + + let isScrollToEnd = lastItemIndex > Int(currentState.paginationSize) * Int(currentState.currentPage + 1) - prefetchCount + let hasNextPage = currentState.currentPage < (currentState.totalPagesCount - 1) + + return isScrollToEnd && hasNextPage + } +} + +// MARK: - Mutate Handlers +private extension PopupSearchReactor { + func handleViewDidLoad() -> Observable { + return loadDefaultSearchResults() + } + + func handleSearchBarEditing(_ text: String) -> Observable { + return .just(.updateClearButtonIsHidden(to: text.isEmpty)) + } + + func handleSearchBarExitEditing(_ text: String) -> Observable { + return loadKeywordSearchResults(text) + } + + func handleSearchBarEndEditing() -> Observable { + return Observable.concat([ + .just(.updateClearButtonIsHidden(to: true)), + .just(.updateEditingState) + ]) + } + + func handleSearchBarClear() -> Observable { + return Observable.concat([ + .just(.updateClearButtonIsHidden(to: true)), + .just(.updateSearchBar(to: nil)) + ]) + } + + func handleSearchBarCancel() -> Observable { + if currentState.isSearching { + return loadDefaultSearchResults() + } else { + return .just(.present(target: .before)) + } + } + + func handleRecentSearchTagTap(at indexPath: IndexPath) -> Observable { + let keyword = findRecentSearchKeyword(at: indexPath) + return loadKeywordSearchResults(keyword) + } + + func handleRecentSearchTagRemove(_ text: String) -> Observable { + removeRecentSearchItem(text: text) + return Observable.concat([ + .just(.setupRecentSearch(items: makeRecentSearchItems())), + .just(.updateSearchResultSection) + ]) + } + + func handleRecentSearchTagRemoveAll() -> Observable { + removeAllRecentSearchItems() + return Observable.concat([ + .just(.setupRecentSearch(items: makeRecentSearchItems())), + .just(.updateSearchResultSection) + ]) + } + + func handleCategoryTagRemove(_ categoryID: Int) -> Observable { + removeCategoryItem(by: categoryID) + return loadDefaultSearchResults() + } + + func handleCategoryChanged() -> Observable { + return loadDefaultSearchResults() + } + + func handleFilterChanged() -> Observable { + return loadDefaultSearchResults() + } + + func handleSearchResultItemTap(at indexPath: IndexPath) -> Observable { + guard let popupID = findPopupStoreID(at: indexPath) else { return .empty() } + return .just(.present(target: .popupDetail(popupID: popupID))) + } + + func handleSearchResultBookmark(at indexPath: IndexPath) -> Observable { + return fetchSearchResultBookmark(at: indexPath) + .andThen(.concat([ + .just(.updateSearchResultBookmark(indexPath: indexPath)), + .just(.updateSearchResultSection) + ])) + } + + func handleSearchResultPrefetch(at indexPathList: [IndexPath]) -> Observable { + guard isPrefetchable(prefetchCount: 4, indexPathList: indexPathList) else { return .empty() } + return fetchSearchResult(page: currentState.currentPage + 1) + .withUnretained(self) + .flatMap { owner, response in + Observable.concat([ + .just(.appendSearchResult(items: owner.makeSearchResultItems(response.popUpStoreList, response.loginYn))), + .just(.updateCurrentPage(to: owner.currentState.currentPage + 1)), + .just(.updateSearchResultSection) + ]) + } + } +} + +// MARK: - Load Search Results +private extension PopupSearchReactor { + func loadDefaultSearchResults(page: Int32 = 0) -> Observable { + return fetchSearchResult(page: page) + .withUnretained(self) + .flatMap { owner, response in + Observable.concat([ + .just(.setupRecentSearch(items: owner.makeRecentSearchItems())), + .just(.setupCategory(items: owner.makeCategoryItems())), + .just(.setupSearchResultHeader(item: owner.makeSearchResultHeaderInput(count: response.totalElements))), + .just(.setupSearchResult(items: owner.makeSearchResultItems(response.popUpStoreList, response.loginYn))), + .just(.setupSearchResultTotalPageCount(count: response.totalPages)), + .just(.updateCurrentPage(to: 0)), + .just(.updateSearchingState(to: false)), + .just(.updateSearchBar(to: nil)), + .just(.updateEditingState), + .just(.updateSearchResultSection) + ]) + } + } + + func loadKeywordSearchResults(_ keyword: String?) -> Observable { + guard let keyword = keyword else { return .empty() } + return fetchSearchResult(keyword: keyword) + .withUnretained(self) + .flatMap { owner, response in + Observable.concat([ + .just(.setupRecentSearch(items: [])), + .just(.setupCategory(items: [])), + .just(.setupSearchResult(items: owner.makeSearchResultItems(response.popupStoreList, response.loginYn))), + .just(.setupSearchResultHeader(item: owner.makeSearchResultHeaderInput( + keyword: owner.makePostPositionedText(keyword), + count: Int64(response.popupStoreList.count) + ))), + .just(.setupSearchResultTotalPageCount(count: 0)), + .just(.updateCurrentPage(to: 0)), + .just(.updateSearchingState(to: true)), + .just(.updateClearButtonIsHidden(to: true)), + .just(.updateEditingState), + .just(.updateSearchResultSection) + ]) + } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/SectionType/Section.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/SectionType/Section.swift new file mode 100644 index 00000000..bd858660 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/SectionType/Section.swift @@ -0,0 +1,9 @@ +import Foundation + +enum PopupSearchSection: CaseIterable, Hashable { + case recentSearch + case category + case searchResultHeader + case searchResult + case searchResultEmpty +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultEmptyCollectionViewCell.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultEmptyCollectionViewCell.swift new file mode 100644 index 00000000..9a82752c --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultEmptyCollectionViewCell.swift @@ -0,0 +1,59 @@ +import UIKit + +import DesignSystem +import Infrastructure + +import SnapKit +import Then + +final class SearchResultEmptyCollectionViewCell: UICollectionViewCell { + + // MARK: - Properties + private let emptyLabel = PPLabel( + style: .medium, + fontSize: 14, + lineHeight: 1.5 + ).then { + $0.textAlignment = .center + $0.numberOfLines = 2 + $0.textColor = .g400 + } + + // MARK: - init + override init(frame: CGRect) { + super.init(frame: frame) + + self.addViews() + self.setupConstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension SearchResultEmptyCollectionViewCell { + func addViews() { + [emptyLabel].forEach { + self.addSubview($0) + } + } + + func setupConstraints() { + emptyLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(145) + make.horizontalEdges.equalToSuperview() + make.height.equalTo(42) + } + } + + func configureUI() { } +} + +extension SearchResultEmptyCollectionViewCell { + func configureCell(title: String) { + self.emptyLabel.text = title + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultHeaderCollectionViewCell.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultHeaderCollectionViewCell.swift new file mode 100644 index 00000000..518bac1a --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/Component/Cell/SearchResultHeaderCollectionViewCell.swift @@ -0,0 +1,125 @@ +import UIKit + +import DesignSystem + +import RxSwift +import SnapKit +import Then + +public final class SearchResultHeaderCollectionViewCell: UICollectionViewCell { + + // MARK: - Properties + + var disposeBag = DisposeBag() + + private let afterSearchTitleLabel = PPLabel(style: .bold, fontSize: 16).then { + $0.isHidden = true + } + + private let cellCountLabel = PPLabel(style: .regular, fontSize: 13).then { + $0.textColor = .g400 + } + + private let filterStatusLabel = PPLabel(style: .regular, fontSize: 13) + + private let dropDownImageView = UIImageView().then { + $0.image = UIImage(named: "icon_dropdown") + $0.isUserInteractionEnabled = false + } + + let filterStatusButton = UIButton() + + // MARK: - init + override init(frame: CGRect) { + super.init(frame: .zero) + + self.addViews() + self.setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } + + public override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } +} + +// MARK: - SetUp +private extension SearchResultHeaderCollectionViewCell { + func addViews() { + [afterSearchTitleLabel, cellCountLabel, filterStatusButton].forEach { + addSubview($0) + } + + [filterStatusLabel, dropDownImageView].forEach { + filterStatusButton.addSubview($0) + } + } + + func setupConstraints() { + afterSearchTitleLabel.snp.makeConstraints { make in + make.height.equalTo(0) + make.horizontalEdges.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalTo(cellCountLabel.snp.top).offset(-4) + } + + cellCountLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.height.equalTo(20) + make.bottom.equalToSuperview() + } + + filterStatusButton.snp.makeConstraints { make in + make.trailing.equalToSuperview() + make.height.equalTo(22) + make.bottom.equalToSuperview() + } + + filterStatusLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.centerY.equalToSuperview() + } + + dropDownImageView.snp.makeConstraints { make in + make.verticalEdges.equalToSuperview() + make.width.equalTo(dropDownImageView.snp.height) + make.leading.equalTo(filterStatusLabel.snp.trailing).offset(6) + make.trailing.equalToSuperview() + } + } +} + +extension SearchResultHeaderCollectionViewCell { + func configureCell(title: String?, count: Int?, filterText: String?) { + if let afterSearchTitle = title, + let count = count { + filterStatusButton.isHidden = true + afterSearchTitleLabel.isHidden = false + afterSearchTitleLabel.text = afterSearchTitle + " 포함된 팝업" + cellCountLabel.text = "총 \(count)개를 찾았어요." + + if count == 0 { self.isHidden = true } else { + self.isHidden = false + afterSearchTitleLabel.snp.updateConstraints { make in + make.height.equalTo(24) + } + } + + } else if let count, let filterText { + filterStatusButton.isHidden = false + afterSearchTitleLabel.isHidden = true + cellCountLabel.text = "총 \(count)개" + filterStatusLabel.text = filterText + + self.isHidden = false + afterSearchTitleLabel.snp.updateConstraints { make in + make.height.equalTo(0) + } + } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchView.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchView.swift new file mode 100644 index 00000000..bcd5b3e8 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchView.swift @@ -0,0 +1,322 @@ +import UIKit + +import DesignSystem + +import RxCocoa +import RxRelay +import RxSwift +import SnapKit +import Then + +final class PopupSearchView: UIView { + + // MARK: - Properties + private var dataSource: UICollectionViewDiffableDataSource? + private var layoutFactory: PopupSearchLayoutFactory = PopupSearchLayoutFactory() + + let recentSearchTagRemoveButtonTapped = PublishRelay() + let recentSearchTagRemoveAllButtonTapped = PublishRelay() + let categoryTagRemoveButtonTapped = PublishRelay() + let filterStatusButtonTapped = PublishRelay() + let bookmarkButtonTapped = PublishRelay() + + let tapGestureRecognizer = UITapGestureRecognizer().then { + $0.cancelsTouchesInView = false + } + + public let searchBar = PPSearchBarView() + + lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()).then { + layoutFactory.setSectionProvider { [weak self] index in + guard let self, let dataSource else { return nil } + return dataSource.sectionIdentifier(for: index) + } + + $0.setCollectionViewLayout(layoutFactory.makeCollectionViewLayout(), animated: false) + + $0.register( + TagCollectionHeaderView.self, + forSupplementaryViewOfKind: SectionHeaderKind.recentSearch.rawValue, + withReuseIdentifier: TagCollectionHeaderView.identifiers + ) + + $0.register( + TagCollectionHeaderView.self, + forSupplementaryViewOfKind: SectionHeaderKind.category.rawValue, + withReuseIdentifier: TagCollectionHeaderView.identifiers + ) + + $0.register( + PPTagCollectionViewCell.self, + forCellWithReuseIdentifier: PPTagCollectionViewCell.identifiers + ) + + $0.register( + SearchResultHeaderCollectionViewCell.self, + forCellWithReuseIdentifier: SearchResultHeaderCollectionViewCell.identifiers + ) + + $0.register( + PPPopupGridCollectionViewCell.self, + forCellWithReuseIdentifier: PPPopupGridCollectionViewCell.identifiers + ) + + $0.register( + SearchResultEmptyCollectionViewCell.self, + forCellWithReuseIdentifier: SearchResultEmptyCollectionViewCell.identifiers + ) + + // UICollectionView 최 상/하단 빈 영역 + $0.contentInset = UIEdgeInsets(top: 24, left: 0, bottom: 48, right: 0) + $0.contentInsetAdjustmentBehavior = .never + } + + // MARK: - init + init() { + super.init(frame: .zero) + + self.addViews() + self.setupConstraints() + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension PopupSearchView { + func addViews() { + [searchBar, collectionView].forEach { + self.addSubview($0) + } + + [tapGestureRecognizer].forEach { + self.addGestureRecognizer($0) + } + } + + func setupConstraints() { + searchBar.snp.makeConstraints { make in + make.top.equalTo(self.safeAreaLayoutGuide) + make.horizontalEdges.equalToSuperview() + make.height.equalTo(56) + } + + collectionView.snp.makeConstraints { make in + make.top.equalTo(searchBar.snp.bottom) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview() + } + } + + func configureUI() { + self.configurationDataSourceItem() + self.configureDataSourceHeader() + } +} + +// MARK: - DataSource +extension PopupSearchView { + private func configurationDataSourceItem() { + self.dataSource = UICollectionViewDiffableDataSource< + PopupSearchSection, + PopupSearchView.SectionItem + >( + collectionView: collectionView + ) { (collectionView, indexPath, item) -> UICollectionViewCell? in + + switch item { + case .recentSearchItem(let item): + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: PPTagCollectionViewCell.identifiers, + for: indexPath + ) as! PPTagCollectionViewCell + + cell.configureCell( + title: item.title, + id: item.id, + isSelected: item.isSelected, + isCancelable: item.isCancelable + ) + + cell.cancelButton.rx.tap + .compactMap { cell.titleLabel.text } + .bind(to: self.recentSearchTagRemoveButtonTapped) + .disposed(by: cell.disposeBag) + + return cell + + case .categoryItem(let item): + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: PPTagCollectionViewCell.identifiers, + for: indexPath + ) as! PPTagCollectionViewCell + + cell.configureCell( + title: item.title, + id: item.id, + isSelected: item.isSelected, + isCancelable: item.isCancelable + ) + + cell.cancelButton.rx.tap + .compactMap { item.id } + .bind(to: self.categoryTagRemoveButtonTapped) + .disposed(by: cell.disposeBag) + + return cell + + case .searchResultHeaderItem(let item): + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: SearchResultHeaderCollectionViewCell.identifiers, + for: indexPath + ) as! SearchResultHeaderCollectionViewCell + + cell.configureCell(title: item.title, count: item.count, filterText: item.filterText) + + cell.filterStatusButton.rx.tap + .bind(to: self.filterStatusButtonTapped) + .disposed(by: cell.disposeBag) + + return cell + + case .searchResultItem(let item): + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: PPPopupGridCollectionViewCell.identifiers, + for: indexPath + ) as! PPPopupGridCollectionViewCell + + cell.configureCell( + imagePath: item.imagePath, + id: item.id, + category: item.category, + title: item.title, + address: item.address, + startDate: item.startDate, + endDate: item.endDate, + isBookmark: item.isBookmark, + isLogin: item.isLogin, + isPopular: item.isPopular, + row: item.row + ) + + cell.bookmarkButton.rx.tap + .map { indexPath } + .bind(to: self.bookmarkButtonTapped) + .disposed(by: cell.disposeBag) + + return cell + + case .searchResultEmptyItem(let title): + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: SearchResultEmptyCollectionViewCell.identifiers, + for: indexPath + ) as! SearchResultEmptyCollectionViewCell + + cell.configureCell(title: title) + + return cell + } + } + + self.collectionView.dataSource = self.dataSource + } + + private func configureDataSourceHeader() { + dataSource?.supplementaryViewProvider = { [weak self] (collectionView, elementKind, indexPath) -> UICollectionReusableView? in + guard let self else { return nil } + switch SectionHeaderKind(rawValue: elementKind)! { + case .recentSearch: + guard let header = collectionView.dequeueReusableSupplementaryView( + ofKind: elementKind, + withReuseIdentifier: TagCollectionHeaderView.identifiers, + for: indexPath + ) as? TagCollectionHeaderView else { fatalError("\(#file), \(#function) Error") } + header.configureHeader(title: "최근 검색어", buttonTitle: "모두삭제") + + header.removeAllButton.rx.tap + .bind(to: self.recentSearchTagRemoveAllButtonTapped) + .disposed(by: header.disposeBag) + + return header + + case .category: + guard let header = collectionView.dequeueReusableSupplementaryView( + ofKind: elementKind, + withReuseIdentifier: TagCollectionHeaderView.identifiers, + for: indexPath + ) as? TagCollectionHeaderView else { fatalError("\(#file), \(#function) Error") } + header.configureHeader(title: "팝업스토어 찾기") + + return header + } + } + } + + func updateSectionSnapshot(at section: PopupSearchSection, with items: [SectionItem]) { + if items.isEmpty { + guard var snapshot = dataSource?.snapshot() else { return } + snapshot.deleteSections([section]) + dataSource?.apply(snapshot, animatingDifferences: false) + return + } else { + if var snapshot = dataSource?.snapshot(for: section) { + snapshot.deleteAll() + snapshot.append(items) + dataSource?.apply(snapshot, to: section) + } else { + guard var snapshot = dataSource?.snapshot() else { return } + snapshot.appendSections([section]) + snapshot.appendItems(items, toSection: section) + dataSource?.apply(snapshot) + } + } + } + + func updateSearchResultSectionSnapshot( + with items: [SectionItem], + header: SectionItem, + empty: SectionItem? = nil + ) { + guard var snapshot = dataSource?.snapshot() else { return } + snapshot.deleteSections([.searchResultHeader, .searchResult, .searchResultEmpty]) + + if let empty { + snapshot.appendSections([.searchResultHeader, .searchResultEmpty]) + snapshot.appendItems([header], toSection: .searchResultHeader) + snapshot.appendItems([empty], toSection: .searchResultEmpty) + collectionView.isScrollEnabled = false + } else { + snapshot.appendSections([.searchResultHeader, .searchResult]) + snapshot.appendItems([header], toSection: .searchResultHeader) + snapshot.appendItems(items, toSection: .searchResult) + collectionView.isScrollEnabled = true + } + + dataSource?.apply(snapshot, animatingDifferences: false) + } + + func getSectionsFromDataSource() -> [PopupSearchSection] { + return dataSource?.snapshot().sectionIdentifiers ?? [] + } +} + +// MARK: - Section information +extension PopupSearchView { + /// Section에 들어갈 Item을 정의한 변수 + enum SectionItem: Hashable { + case recentSearchItem(TagModel) + case categoryItem(TagModel) + case searchResultHeaderItem(SearchResultHeaderModel) + case searchResultItem(SearchResultModel) + case searchResultEmptyItem(String) + } + + /// Section의 헤더를 구분하기 위한 변수 + enum SectionHeaderKind: String { + case recentSearch = "recentSearchElementKind" + case category = "categoryElementKind" + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchViewController.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchViewController.swift new file mode 100644 index 00000000..f95cb7d0 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeature/PopupSearch/View/PopupSearchViewController.swift @@ -0,0 +1,225 @@ +import UIKit + +import DesignSystem +import DomainInterface +import Infrastructure +import PresentationInterface +import SearchFeatureInterface + +import ReactorKit +import RxCocoa +import RxSwift +import Then + +public final class PopupSearchViewController: BaseViewController, View { + + public typealias Reactor = PopupSearchReactor + + // MARK: - Properties + public var disposeBag = DisposeBag() + + private let mainView = PopupSearchView() + +} + +// MARK: - Life Cycle +extension PopupSearchViewController { + public override func loadView() { + self.view = mainView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tabBarController?.tabBar.isHidden = true + } +} + +// MARK: - Bind +extension PopupSearchViewController { + public func bind(reactor: Reactor) { + self.bindAction(reactor: reactor) + self.bindState(reactor: reactor) + } + + private func bindAction(reactor: Reactor) { + rx.viewDidLoad + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.tapGestureRecognizer.rx.event + .map { _ in Reactor.Action.searchBarEndEditing } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.searchBar.searchBar.searchTextField.rx.controlEvent([.editingDidBegin, .editingChanged]) + .withLatestFrom(mainView.searchBar.searchBar.searchTextField.rx.text.orEmpty) + .map(Reactor.Action.searchBarEditing) + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.searchBar.searchBar.searchTextField.rx.controlEvent(.editingDidEndOnExit) + .withLatestFrom(mainView.searchBar.searchBar.searchTextField.rx.text.orEmpty) + .map(Reactor.Action.searchBarExitEditing) + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.searchBar.clearButton.rx.tap + .map { Reactor.Action.searchBarClearButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.searchBar.cancelButton.rx.tap + .map { _ in Reactor.Action.searchBarCancelButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.recentSearchTagRemoveButtonTapped + .map(Reactor.Action.recentSearchTagRemoveButtonTapped) + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.recentSearchTagRemoveAllButtonTapped + .map { Reactor.Action.recentSearchTagRemoveAllButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.collectionView.rx.itemSelected + .compactMap { indexPath in + let sections = self.mainView.getSectionsFromDataSource() + guard indexPath.section < sections.count else { return nil } + + switch sections[indexPath.section] { + case .recentSearch: + return Reactor.Action.recentSearchTagButtonTapped(indexPath: indexPath) + + case .category: + return Reactor.Action.categoryTagButtonTapped + + case .searchResultHeader: + return nil + + case .searchResult: + return Reactor.Action.searchResultItemTapped(indexPath: indexPath) + + case .searchResultEmpty: + return nil + } + } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.categoryTagRemoveButtonTapped + .map { Reactor.Action.categoryTagRemoveButtonTapped(categoryID: $0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + Category.valueChanged + .map { _ in Reactor.Action.categoryChangedBySelector } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.filterStatusButtonTapped + .map { Reactor.Action.searchResultFilterButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + Filter.valueChanged + .map { _ in Reactor.Action.searchResultFilterChangedBySelector } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.bookmarkButtonTapped + .map(Reactor.Action.searchResultBookmarkButtonTapped) + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.collectionView.rx.prefetchItems + .throttle(.milliseconds(100), latest: false, scheduler: MainScheduler.asyncInstance) + .map(Reactor.Action.searchResultPrefetchItems) + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindState(reactor: Reactor) { + reactor.pulse(\.$endEditing) + .withUnretained(self) + .subscribe { (owner, _) in owner.mainView.endEditing(true) } + .disposed(by: disposeBag) + + reactor.pulse(\.$clearButtonIsHidden) + .compactMap { $0 } + .withUnretained(self) + .subscribe { (owner, state) in owner.mainView.searchBar.clearButton.isHidden = state } + .disposed(by: disposeBag) + + reactor.pulse(\.$present) + .withUnretained(self) + .skip(1) + .subscribe { owner, target in + switch target! { + case .categorySelector: + @Dependency var factory: CategorySelectorFactory + owner.PPPresent(factory.make()) + + case .filterSelector: + @Dependency var factory: FilterSelectorFactory + owner.PPPresent(factory.make()) + + case .popupDetail(let popupID): + @Dependency var factory: DetailFactory + owner.navigationController?.pushViewController( + factory.make(popupID: popupID), + animated: true + ) + + case .before: + owner.navigationController?.popViewController(animated: true) + } + } + .disposed(by: disposeBag) + + reactor.pulse(\.$searchBarText) + .withUnretained(self) + .subscribe { (owner, text) in owner.mainView.searchBar.searchBar.text = text } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.recentSearchItems) + .compactMap { $0.recentSearchItems } + .withUnretained(self) + .subscribe { (owner, items) in + owner.mainView.updateSectionSnapshot( + at: .recentSearch, + with: items.map(PopupSearchView.SectionItem.recentSearchItem) + ) + } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.categoryItems) + .compactMap { $0.categoryItems } + .withUnretained(self) + .subscribe { (owner, items) in + owner.mainView.updateSectionSnapshot( + at: .category, + with: items.map(PopupSearchView.SectionItem.categoryItem) + ) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$updateSearchResultSection) + .withLatestFrom(reactor.state) + .withUnretained(self) + .subscribe { (owner, state) in + let isEmpty = state.updateSearchResultSection == nil + let emptyCaseTitle = state.updateSearchResultSection + + owner.mainView.updateSearchResultSectionSnapshot( + with: state.searchResultItems.map(PopupSearchView.SectionItem.searchResultItem), + header: PopupSearchView.SectionItem.searchResultHeaderItem(state.searchResultHeader), + empty: isEmpty ? nil : PopupSearchView.SectionItem.searchResultEmptyItem(emptyCaseTitle!) + ) + } + .disposed(by: disposeBag) + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/AppDelegate.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/AppDelegate.swift new file mode 100644 index 00000000..d26b08f7 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/AppDelegate.swift @@ -0,0 +1,74 @@ +import UIKit + +import Data +import Domain +import DomainInterface +import Infrastructure +import Presentation +import PresentationInterface +import SearchFeature +import SearchFeatureInterface + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + self.registerDependencies() + self.registerFactory() + + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } +} + +// MARK: - Dependency +extension AppDelegate { + /// 의존성 등록을 위한 메서드 + private func registerDependencies() { + // MARK: Register Service + DIContainer.register(Provider.self) { return ProviderImpl() } + DIContainer.register(UserDefaultService.self) { return UserDefaultService() } + DIContainer.register(KeyChainService.self) { return KeyChainService() } + + // MARK: Resolve service + @Dependency var provider: Provider + @Dependency var userDefaultService: UserDefaultService + + // MARK: Register repository + DIContainer.register(UserAPIRepository.self) { return UserAPIRepositoryImpl(provider: provider) } + DIContainer.register(PopUpAPIRepository.self) { return PopUpAPIRepositoryImpl(provider: provider) } + DIContainer.register(CommentAPIRepository.self) { return CommentAPIRepositoryImpl(provider: provider) } + DIContainer.register(PreSignedRepository.self) { return PreSignedRepositoryImpl() } + DIContainer.register(CategoryRepository.self) { return CategoryRepositoryImpl(provider: provider) } + DIContainer.register(SearchAPIRepository.self) { return SearchAPIRepositoryImpl(provider: provider, userDefaultService: userDefaultService) } + + // MARK: Resolve repository + @Dependency var userAPIRepository: UserAPIRepository + @Dependency var popUpAPIRepository: PopUpAPIRepository + @Dependency var commentAPIRepository: CommentAPIRepository + @Dependency var preSignedRepository: PreSignedRepository + @Dependency var categoryRepository: CategoryRepository + @Dependency var searchAPIRepository: SearchAPIRepository + + // MARK: Register UseCase + DIContainer.register(UserAPIUseCase.self) { return UserAPIUseCaseImpl(repository: userAPIRepository) } + DIContainer.register(PopUpAPIUseCase.self) { return PopUpAPIUseCaseImpl(repository: popUpAPIRepository) } + DIContainer.register(CommentAPIUseCase.self) { return CommentAPIUseCaseImpl(repository: commentAPIRepository) } + DIContainer.register(PreSignedUseCase.self) { return PreSignedUseCaseImpl(repository: preSignedRepository) } + DIContainer.register(FetchCategoryListUseCase.self) { return FetchCategoryListUseCaseImpl(repository: categoryRepository) } + DIContainer.register(FetchKeywordBasePopupListUseCase.self) { return FetchKeywordBasePopupListUseCaseImpl(repository: searchAPIRepository) } + } + + private func registerFactory() { + DIContainer.register(PopupSearchFactory.self) { return PopupSearchFactoryImpl() } + DIContainer.register(CategorySelectorFactory.self) { return CategorySelectorFactoryImpl() } + DIContainer.register(FilterSelectorFactory.self) { return FilterSelectorFactoryImpl() } + DIContainer.register(DetailFactory.self) { return DetailFactoryImpl() } + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/SceneDelegate.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/SceneDelegate.swift new file mode 100644 index 00000000..ded75ea4 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/App/SceneDelegate.swift @@ -0,0 +1,28 @@ +import UIKit + +import Domain +import DomainInterface +import Infrastructure +import SearchFeatureInterface + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + window = UIWindow(windowScene: windowScene) + + let navigationController = UINavigationController() + @Dependency var popupSearchFactory: PopupSearchFactory + + navigationController.pushViewController( + popupSearchFactory.make(), + animated: false + ) + + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/Contents.json new file mode 100644 index 00000000..f64986bb --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "solid.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/solid.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/solid.svg new file mode 100644 index 00000000..71a10b2e --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_clear_button.imageset/solid.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/Contents.json new file mode 100644 index 00000000..d86eb988 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "line.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/line.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/line.svg new file mode 100644 index 00000000..6dd06be5 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/SearchBar/icon_search_gray.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/Contents.json new file mode 100644 index 00000000..f64986bb --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "solid.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/solid.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/solid.svg new file mode 100644 index 00000000..1ef76aed --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark.imageset/solid.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/Contents.json new file mode 100644 index 00000000..44808aa2 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "solid-1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/solid-1.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/solid-1.svg new file mode 100644 index 00000000..6d836093 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_bookmark_fill.imageset/solid-1.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/Contents.json new file mode 100644 index 00000000..f64986bb --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "solid.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/solid.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/solid.svg new file mode 100644 index 00000000..ec4c794b --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_dropdown.imageset/solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/Contents.json new file mode 100644 index 00000000..d86eb988 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "line.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/line.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/line.svg new file mode 100644 index 00000000..c95be2c3 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/Contents.json new file mode 100644 index 00000000..d86eb988 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "line.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/line.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/line.svg new file mode 100644 index 00000000..6a318745 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_gray.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/Contents.json b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/Contents.json new file mode 100644 index 00000000..d86eb988 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "line.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/line.svg b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/line.svg new file mode 100644 index 00000000..b55257a1 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Assets.xcassets/icon_xmark_white.imageset/line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Base.lproj/LaunchScreen.storyboard b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Info.plist b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Info.plist new file mode 100644 index 00000000..a343d54d --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureDemo/Resource/Info.plist @@ -0,0 +1,46 @@ + + + + + UIUserInterfaceStyle + Light + UIAppFonts + + GothicA1-Bold.ttf + GothicA1-Light.ttf + GothicA1-Medium.ttf + GothicA1-Regular.ttf + Poppins-Bold.ttf + Poppins-Light.ttf + Poppins-Medium.ttf + Poppins-Regular.ttf + + NAVER_MAP_CLIENT_ID + ${NAVER_MAP_CLIENT_ID} + POPPOOL_S3_BASE_URL + ${POPPOOL_S3_BASE_URL} + POPPOOL_BASE_URL + ${POPPOOL_BASE_URL} + POPPOOL_API_KEY + ${POPPOOL_API_KEY} + KAKAO_AUTH_APP_KEY + ${KAKAO_AUTH_APP_KEY} + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/CategorySelectorFactory.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/CategorySelectorFactory.swift new file mode 100644 index 00000000..5e4a1a14 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/CategorySelectorFactory.swift @@ -0,0 +1,7 @@ +import UIKit + +import DesignSystem + +public protocol CategorySelectorFactory { + func make() -> BaseViewController & PPModalPresentable +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/FilterSelectorFactory.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/FilterSelectorFactory.swift new file mode 100644 index 00000000..415c8b00 --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/FilterSelectorFactory.swift @@ -0,0 +1,7 @@ +import UIKit + +import DesignSystem + +public protocol FilterSelectorFactory { + func make() -> BaseViewController & PPModalPresentable +} diff --git a/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/PopupSearchFactory.swift b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/PopupSearchFactory.swift new file mode 100644 index 00000000..0de8d2cf --- /dev/null +++ b/Poppool/PresentationLayer/SearchFeature/SearchFeatureInterface/Source/Factory/PopupSearchFactory.swift @@ -0,0 +1,7 @@ +import UIKit + +import DesignSystem + +public protocol PopupSearchFactory { + func make() -> BaseViewController +} diff --git a/README.md b/README.md index 96a2a2fa..88cedaba 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# iOS-renew \ No newline at end of file +
+ + # Poppool + 5 5_1 + +

+ + + + +

흩어져 있는 팝업스토어 정보를 제공하고 인스타그램 연동으로 부가 기능을 제공하는 서비스입니다. + +

+ +
+ +
+ +##  iOS Members  + +|[준영](https://github.com/dongglehada)|[기현](https://github.com/zzangzzangguy)|[영훈](https://github.com/0Hooni)| +|:-----:|:-----:|:-----:| + | | | + +
+
diff --git a/back/search/ico/ico/line.png b/back/search/ico/ico/line.png deleted file mode 100644 index 4a809c58..00000000 Binary files a/back/search/ico/ico/line.png and /dev/null differ diff --git a/back/search/ico/ico/search/ico/ico/line.png b/back/search/ico/ico/search/ico/ico/line.png deleted file mode 100644 index 10972352..00000000 Binary files a/back/search/ico/ico/search/ico/ico/line.png and /dev/null differ diff --git a/back/search/ico/ico/search/ico/ico/line@2x.png b/back/search/ico/ico/search/ico/ico/line@2x.png deleted file mode 100644 index 4d632200..00000000 Binary files a/back/search/ico/ico/search/ico/ico/line@2x.png and /dev/null differ diff --git a/back/search/ico/ico/search/ico/ico/line@3x.png b/back/search/ico/ico/search/ico/ico/line@3x.png deleted file mode 100644 index fbea4688..00000000 Binary files a/back/search/ico/ico/search/ico/ico/line@3x.png and /dev/null differ