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..7c12c739
--- /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_PROJECT: Poppool/Poppool.xcodeproj
+ 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 -project $XC_PROJECT -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..3dd82707 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,65 +1,43 @@
-# 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
diff --git a/Poppool/.swiftlint.yml b/Poppool/.swiftlint.yml
new file mode 100644
index 00000000..9e132e30
--- /dev/null
+++ b/Poppool/.swiftlint.yml
@@ -0,0 +1,19 @@
+# 기본 활성화된 룰 중에 비활성화할 룰을 지정
+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
+
+# 기본(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/Poppool.xcodeproj/project.pbxproj b/Poppool/Poppool.xcodeproj/project.pbxproj
index a38282f7..22a67e02 100644
--- a/Poppool/Poppool.xcodeproj/project.pbxproj
+++ b/Poppool/Poppool.xcodeproj/project.pbxproj
@@ -97,13 +97,6 @@
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 */; };
@@ -236,7 +229,6 @@
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 */; };
@@ -270,6 +262,8 @@
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 */; };
+ 089B4FD82D9A57AE00FC0CC3 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B4FD72D9A57AE00FC0CC3 /* ImageLoader.swift */; };
+ 089B4FDF2D9A8F9A00FC0CC3 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B4FDE2D9A8F9A00FC0CC3 /* MemoryStorage.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 */; };
@@ -343,10 +337,7 @@
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 */; };
+ 08B191C22CF615CA0057BC04 /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B191C12CF615CA0057BC04 /* KeyPath.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 */; };
@@ -354,6 +345,7 @@
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 */; };
+ 08CFD3922D9BDE99004CDD50 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CFD3912D9BDE99004CDD50 /* DiskStorage.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 */; };
@@ -365,7 +357,6 @@
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 */; };
@@ -374,6 +365,10 @@
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 */; };
+ 4E15142A2D99480200DFD08F /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 4E1514292D99480200DFD08F /* NMapsMap */; };
+ 4E15142C2D994A3A00DFD08F /* KakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 4E15142B2D994A3A00DFD08F /* KakaoSDK */; };
+ 4E15142E2D994A3A00DFD08F /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */; };
+ 4E1514302D994A3A00DFD08F /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 4E15142F2D994A3A00DFD08F /* KakaoSDKCommon */; };
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 */; };
@@ -419,7 +414,6 @@
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 */; };
@@ -429,11 +423,11 @@
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 */; };
+ 4EAB809D2D3F78AA0041AF30 /* NMFMapViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAB809C2D3F78AA0041AF30 /* NMFMapViewDelegateProxy.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 */; };
+ 4EE360FD2D91876300D2441D /* NMapsMap in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE360FC2D91876300D2441D /* NMapsMap */; };
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 */; };
@@ -469,11 +463,7 @@
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 */; };
@@ -484,24 +474,9 @@
BDCA42102CF35FF5005EECF6 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDCA420F2CF35FF5005EECF6 /* Lottie */; };
/* 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 */
+ 05229DD02D99519200D88E73 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; };
+ 057151572D9D2E0800260615 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
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 = ""; };
@@ -592,13 +567,6 @@
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 = ""; };
@@ -762,6 +730,8 @@
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 = ""; };
+ 089B4FD72D9A57AE00FC0CC3 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; };
+ 089B4FDE2D9A8F9A00FC0CC3 /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.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 = ""; };
@@ -835,7 +805,7 @@
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 = ""; };
+ 08B191C12CF615CA0057BC04 /* KeyPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPath.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 = ""; };
@@ -843,6 +813,7 @@
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 = ""; };
+ 08CFD3912D9BDE99004CDD50 /* DiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorage.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 = ""; };
@@ -854,7 +825,6 @@
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 = ""; };
@@ -916,7 +886,7 @@
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 = ""; };
+ 4EAB809C2D3F78AA0041AF30 /* NMFMapViewDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NMFMapViewDelegateProxy.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 = ""; };
@@ -956,11 +926,6 @@
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 */
@@ -971,37 +936,23 @@
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 */,
+ 4E15142E2D994A3A00DFD08F /* KakaoSDKAuth in Frameworks */,
083A25D02CF364B70099B58E /* Alamofire in Frameworks */,
- 088DE2472D12DB5C0030FA9E /* GoogleMaps in Frameworks */,
+ 4E1514302D994A3A00DFD08F /* KakaoSDKCommon 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 */,
+ 4EE360FD2D91876300D2441D /* NMapsMap in Frameworks */,
BDCA42012CF35EFE005EECF6 /* RxKeyboard in Frameworks */,
+ 4E15142A2D99480200DFD08F /* NMapsMap in Frameworks */,
088DE2442D104EE70030FA9E /* SwiftSoup in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- BDCA41D02CF35AC1005EECF6 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- BDCA41DA2CF35AC1005EECF6 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
+ 4E15142C2D994A3A00DFD08F /* KakaoSDK in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1395,7 +1346,8 @@
isa = PBXGroup;
children = (
08DE8A132D525A4A0049BCAC /* Strings */,
- 08B191C12CF615CA0057BC04 /* Secrets.swift */,
+ 08B191C12CF615CA0057BC04 /* KeyPath.swift */,
+ 057151572D9D2E0800260615 /* Debug.xcconfig */,
08B191532CF41D6F0057BC04 /* PP_loading.json */,
08B191542CF41D6F0057BC04 /* PP_splash.json */,
08B1914A2CF41D680057BC04 /* Font */,
@@ -1411,7 +1363,6 @@
children = (
4E755B1B2D2B9ABF00ADFB21 /* Admin */,
4E685ECD2D12CEB6001EF91C /* Map */,
- 083A258B2CF361F90099B58E /* Convention */,
08B1915F2CF430D40057BC04 /* Components */,
083A25C22CF3635B0099B58E /* Scene */,
083A259D2CF3620B0099B58E /* Utills */,
@@ -1429,20 +1380,6 @@
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 = (
@@ -1494,6 +1431,7 @@
083A25A02CF3623C0099B58E /* Infrastructure */ = {
isa = PBXGroup;
children = (
+ 089B4FD62D9A576F00FC0CC3 /* ImageLoader */,
0841BA832CF9F61500049E31 /* PreSignedService */,
083A25B12CF362670099B58E /* NetworkLayer */,
08DC61F42CF765B5002A2F44 /* UserDefaultService.swift */,
@@ -2311,6 +2249,16 @@
path = View;
sourceTree = "";
};
+ 089B4FD62D9A576F00FC0CC3 /* ImageLoader */ = {
+ isa = PBXGroup;
+ children = (
+ 089B4FD72D9A57AE00FC0CC3 /* ImageLoader.swift */,
+ 089B4FDE2D9A8F9A00FC0CC3 /* MemoryStorage.swift */,
+ 08CFD3912D9BDE99004CDD50 /* DiskStorage.swift */,
+ );
+ path = ImageLoader;
+ sourceTree = "";
+ };
08A2E4772D1B069300102313 /* ImageDetail */ = {
isa = PBXGroup;
children = (
@@ -2390,7 +2338,6 @@
08B191362CF366670057BC04 /* UITableViewCell+.swift */,
08B1913A2CF366A00057BC04 /* UIApplication+.swift */,
08B191772CF442230057BC04 /* UIImage+.swift */,
- 08DC62122CF8B833002A2F44 /* Optional+.swift */,
0841BAAB2CFA35F300049E31 /* UILabel+.swift */,
0841BABD2CFB5AA600049E31 /* Date?+.swift */,
086DD8C72CFDEA9200B97D3B /* UIView+.swift */,
@@ -2884,7 +2831,7 @@
4EED9BAA2D2272F500B288E7 /* Common */ = {
isa = PBXGroup;
children = (
- 4EAB809C2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift */,
+ 4EAB809C2D3F78AA0041AF30 /* NMFMapViewDelegateProxy.swift */,
4EAB809E2D3F8EF50041AF30 /* ViewportBounds.swift */,
4EED9BAB2D22730400B288E7 /* FilterType.swift */,
4E6C07052D4B6E56008A962A /* RegionDefinitions.swift */,
@@ -3009,9 +2956,8 @@
BDCA41B42CF35AC0005EECF6 = {
isa = PBXGroup;
children = (
+ 05229DD02D99519200D88E73 /* .swiftlint.yml */,
BDCA41BF2CF35AC0005EECF6 /* Poppool */,
- BDCA41D62CF35AC1005EECF6 /* PoppoolTests */,
- BDCA41E02CF35AC1005EECF6 /* PoppoolUITests */,
BDCA41BE2CF35AC0005EECF6 /* Products */,
);
sourceTree = "";
@@ -3020,8 +2966,6 @@
isa = PBXGroup;
children = (
BDCA41BD2CF35AC0005EECF6 /* Poppool.app */,
- BDCA41D32CF35AC1005EECF6 /* PoppoolTests.xctest */,
- BDCA41DD2CF35AC1005EECF6 /* PoppoolUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -3040,23 +2984,6 @@
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,6 +2991,7 @@
isa = PBXNativeTarget;
buildConfigurationList = BDCA41E72CF35AC2005EECF6 /* Build configuration list for PBXNativeTarget "Poppool" */;
buildPhases = (
+ 05229DCF2D99507C00D88E73 /* Run SwiftLint */,
BDCA41B92CF35AC0005EECF6 /* Sources */,
BDCA41BA2CF35AC0005EECF6 /* Frameworks */,
BDCA41BB2CF35AC0005EECF6 /* Resources */,
@@ -3075,7 +3003,6 @@
name = Poppool;
packageProductDependencies = (
BDCA41F12CF35D0D005EECF6 /* SnapKit */,
- BDCA41F42CF35D33005EECF6 /* Kingfisher */,
BDCA41F72CF35D9A005EECF6 /* RxSwift */,
BDCA41FD2CF35EE7005EECF6 /* ReactorKit */,
BDCA42002CF35EFE005EECF6 /* RxKeyboard */,
@@ -3085,55 +3012,19 @@
BDCA420C2CF35FD2005EECF6 /* RxGesture */,
BDCA420F2CF35FF5005EECF6 /* Lottie */,
083A25CF2CF364B70099B58E /* Alamofire */,
- 08B191B92CF609AE0057BC04 /* RxKakaoSDK */,
- 08B191BB2CF609AE0057BC04 /* RxKakaoSDKAuth */,
- 08B191BD2CF609AE0057BC04 /* RxKakaoSDKUser */,
088DE2432D104EE70030FA9E /* SwiftSoup */,
- 088DE2462D12DB5C0030FA9E /* GoogleMaps */,
4E5825662D1951DF00EE83EF /* FloatingPanel */,
4EA9989C2D21C404009DC30B /* RxDataSources */,
082197A02D426DCB0054094A /* Then */,
+ 4E1514292D99480200DFD08F /* NMapsMap */,
+ 4E15142B2D994A3A00DFD08F /* KakaoSDK */,
+ 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */,
+ 4E15142F2D994A3A00DFD08F /* KakaoSDKCommon */,
);
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,19 +3033,11 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1540;
- LastUpgradeCheck = 1540;
+ LastUpgradeCheck = 1620;
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" */;
@@ -3168,9 +3051,7 @@
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" */,
@@ -3180,18 +3061,17 @@
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" */,
);
productRefGroup = BDCA41BE2CF35AC0005EECF6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
BDCA41BC2CF35AC0005EECF6 /* Poppool */,
- BDCA41D22CF35AC1005EECF6 /* PoppoolTests */,
- BDCA41DC2CF35AC1005EECF6 /* PoppoolUITests */,
);
};
/* End PBXProject section */
@@ -3217,21 +3097,29 @@
);
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 */ = {
@@ -3275,7 +3163,6 @@
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 */,
@@ -3316,11 +3203,13 @@
4E685EDB2D12CEB6001EF91C /* MapAPIEndpoint.swift in Sources */,
086F89CC2D1E42B000CA4FC9 /* CommentUserBlockController.swift in Sources */,
08DC62032CF8AC06002A2F44 /* HomeView.swift in Sources */,
+ 089B4FD82D9A57AE00FC0CC3 /* ImageLoader.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 */,
+ 089B4FDF2D9A8F9A00FC0CC3 /* MemoryStorage.swift in Sources */,
0899526E2D0474340022AEF9 /* GetSearchPopUpListResponse.swift in Sources */,
08B191392CF366680057BC04 /* UITableViewCell+.swift in Sources */,
08A2E48F2D1BF6E500102313 /* CommentListView.swift in Sources */,
@@ -3333,7 +3222,6 @@
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 */,
@@ -3350,8 +3238,7 @@
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 */,
+ 08B191C22CF615CA0057BC04 /* KeyPath.swift in Sources */,
083C86242D087A44003F441C /* DetailTitleSection.swift in Sources */,
08A2E4822D1BCDEA00102313 /* CommentDetailController.swift in Sources */,
0841BAA32CFA31A300049E31 /* SpacingSection.swift in Sources */,
@@ -3396,7 +3283,6 @@
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 */,
@@ -3458,7 +3344,6 @@
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 */,
@@ -3474,7 +3359,7 @@
BD9103652CF6149D00BBCCAE /* HomeAPIEndpoint.swift in Sources */,
086DD8E32CFF356300B97D3B /* HomeCardGridSection.swift in Sources */,
0841BABE2CFB5AA600049E31 /* Date?+.swift in Sources */,
- 083A258D2CF361F90099B58E /* ConventionCollectionViewCell.swift in Sources */,
+ 08CFD3922D9BDE99004CDD50 /* DiskStorage.swift in Sources */,
4E685EE12D12CEB6001EF91C /* StoreListReactor.swift in Sources */,
4E685EE52D12CEB6001EF91C /* MapMarker.swift in Sources */,
081898FB2D33D9320067BF01 /* GetBlockUserListResponseDTO.swift in Sources */,
@@ -3587,7 +3472,6 @@
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 */,
@@ -3653,7 +3537,7 @@
086F89CA2D1E42A700CA4FC9 /* CommentUserBlockView.swift in Sources */,
086DD8D02CFDFEB900B97D3B /* HomeListReactor.swift in Sources */,
081899202D34DF880067BF01 /* MyPageNoticeDetailController.swift in Sources */,
- 4EAB809D2D3F78AA0041AF30 /* GMSMapViewDelegateProxy.swift in Sources */,
+ 4EAB809D2D3F78AA0041AF30 /* NMFMapViewDelegateProxy.swift in Sources */,
083C86472D0DCDFB003F441C /* AddCommentTitleSection.swift in Sources */,
0841BAB62CFABEDC00049E31 /* HomePopularCardSectionCell.swift in Sources */,
082197A92D4E3EE90054094A /* CommentMyMenuController.swift in Sources */,
@@ -3664,7 +3548,6 @@
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 */,
@@ -3679,40 +3562,8 @@
);
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;
@@ -3824,6 +3675,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 +3701,17 @@
};
BDCA41E82CF35AC2005EECF6 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 057151572D9D2E0800260615 /* 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_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 +3728,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.1;
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 +3740,23 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
+ VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
BDCA41E92CF35AC2005EECF6 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 057151572D9D2E0800260615 /* 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_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 +3773,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0.0;
+ MARKETING_VERSION = 1.0.1;
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 +3785,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,24 +3810,6 @@
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 */
@@ -4073,12 +3837,20 @@
minimumVersion = 2.7.6;
};
};
- 088DE2452D12DB5C0030FA9E /* XCRemoteSwiftPackageReference "ios-maps-sdk" */ = {
+ 08F403312D884F4D00BFA61A /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/kakao/kakao-ios-sdk.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.24.0;
+ };
+ };
+ 4E1514282D99480200DFD08F /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/googlemaps/ios-maps-sdk";
+ repositoryURL = "https://github.com/navermaps/SPM-NMapsMap";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 9.2.0;
+ minimumVersion = 3.21.0;
};
};
4E5825652D1951DF00EE83EF /* XCRemoteSwiftPackageReference "FloatingPanel" */ = {
@@ -4097,20 +3869,20 @@
minimumVersion = 5.0.2;
};
};
- BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
+ 4EE360FB2D91876300D2441D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/SnapKit/SnapKit.git";
+ repositoryURL = "https://github.com/navermaps/SPM-NMapsMap";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 5.7.1;
+ minimumVersion = 3.21.0;
};
};
- BDCA41F32CF35D33005EECF6 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
+ BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/onevcat/Kingfisher.git";
+ repositoryURL = "https://github.com/SnapKit/SnapKit.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 8.1.1;
+ minimumVersion = 5.7.1;
};
};
BDCA41F62CF35D9A005EECF6 /* XCRemoteSwiftPackageReference "RxSwift" */ = {
@@ -4121,14 +3893,6 @@
minimumVersion = 6.8.0;
};
};
- BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */ = {
- isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/kakao/kakao-ios-sdk-rx";
- requirement = {
- kind = upToNextMajorVersion;
- minimumVersion = 2.23.0;
- };
- };
BDCA41FC2CF35EE7005EECF6 /* XCRemoteSwiftPackageReference "ReactorKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ReactorKit/ReactorKit.git";
@@ -4203,25 +3967,22 @@
package = 088DE2422D104EE70030FA9E /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
- 088DE2462D12DB5C0030FA9E /* GoogleMaps */ = {
+ 4E1514292D99480200DFD08F /* NMapsMap */ = {
isa = XCSwiftPackageProductDependency;
- package = 088DE2452D12DB5C0030FA9E /* XCRemoteSwiftPackageReference "ios-maps-sdk" */;
- productName = GoogleMaps;
+ package = 4E1514282D99480200DFD08F /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */;
+ productName = NMapsMap;
};
- 08B191B92CF609AE0057BC04 /* RxKakaoSDK */ = {
+ 4E15142B2D994A3A00DFD08F /* KakaoSDK */ = {
isa = XCSwiftPackageProductDependency;
- package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */;
- productName = RxKakaoSDK;
+ productName = KakaoSDK;
};
- 08B191BB2CF609AE0057BC04 /* RxKakaoSDKAuth */ = {
+ 4E15142D2D994A3A00DFD08F /* KakaoSDKAuth */ = {
isa = XCSwiftPackageProductDependency;
- package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */;
- productName = RxKakaoSDKAuth;
+ productName = KakaoSDKAuth;
};
- 08B191BD2CF609AE0057BC04 /* RxKakaoSDKUser */ = {
+ 4E15142F2D994A3A00DFD08F /* KakaoSDKCommon */ = {
isa = XCSwiftPackageProductDependency;
- package = BDCA41F92CF35EC0005EECF6 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */;
- productName = RxKakaoSDKUser;
+ productName = KakaoSDKCommon;
};
4E5825662D1951DF00EE83EF /* FloatingPanel */ = {
isa = XCSwiftPackageProductDependency;
@@ -4233,16 +3994,16 @@
package = 4EA9989B2D21C404009DC30B /* XCRemoteSwiftPackageReference "RxDataSources" */;
productName = RxDataSources;
};
+ 4EE360FC2D91876300D2441D /* NMapsMap */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4EE360FB2D91876300D2441D /* XCRemoteSwiftPackageReference "SPM-NMapsMap" */;
+ productName = NMapsMap;
+ };
BDCA41F12CF35D0D005EECF6 /* SnapKit */ = {
isa = XCSwiftPackageProductDependency;
package = BDCA41F02CF35D0D005EECF6 /* XCRemoteSwiftPackageReference "SnapKit" */;
productName = SnapKit;
};
- BDCA41F42CF35D33005EECF6 /* Kingfisher */ = {
- isa = XCSwiftPackageProductDependency;
- package = BDCA41F32CF35D33005EECF6 /* XCRemoteSwiftPackageReference "Kingfisher" */;
- productName = Kingfisher;
- };
BDCA41F72CF35D9A005EECF6 /* RxSwift */ = {
isa = XCSwiftPackageProductDependency;
package = BDCA41F62CF35D9A005EECF6 /* XCRemoteSwiftPackageReference "RxSwift" */;
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..858b58f7 100644
--- a/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme
+++ b/Poppool/Poppool.xcodeproj/xcshareddata/xcschemes/Poppool.xcscheme
@@ -1,6 +1,6 @@
Bool {
- RxKakaoSDK.initSDK(appKey: Secrets.kakaoAuthAppkey.rawValue, loggingEnable: false)
- GMSServices.provideAPIKey(Secrets.popPoolApiKey.rawValue)
+ KakaoSDK.initSDK(appKey: KeyPath.kakaoAuthAppKey)
+ NMFAuthManager.shared().clientId = KeyPath.naverMapClientID
+
let locationManager = CLLocationManager()
- locationManager.requestWhenInUseAuthorization() // 권한 요청 초기화
- return true
+ locationManager.requestWhenInUseAuthorization()
- }
+ 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.
- }
-
-
}
-
diff --git a/Poppool/Poppool/Application/SceneDelegate.swift b/Poppool/Poppool/Application/SceneDelegate.swift
index 8f18f6ac..5a7be40e 100644
--- a/Poppool/Poppool/Application/SceneDelegate.swift
+++ b/Poppool/Poppool/Application/SceneDelegate.swift
@@ -1,86 +1,5 @@
-////
-//// 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 KakaoSDKAuth
import RxSwift
@@ -91,7 +10,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 +38,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
index c6847136..528e61d1 100644
--- a/Poppool/Poppool/Application/TestViewController.swift
+++ b/Poppool/Poppool/Application/TestViewController.swift
@@ -7,87 +7,86 @@
import UIKit
-import SnapKit
-import RxSwift
-import RxGesture
import RxCocoa
+import RxGesture
+import RxSwift
+import SnapKit
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
+ return PPButton(style: .secondary, text: "리스트 버튼")
}()
-
+
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)
@@ -96,7 +95,7 @@ class TestViewController: UIViewController {
make.centerX.equalToSuperview()
}
}
-
+
func bind() {
listButton.rx.tap
.withUnretained(self)
@@ -110,11 +109,11 @@ class TestViewController: UIViewController {
}
}
.disposed(by: disposeBag)
-
+
gestureBar.rx.swipeGesture(.up)
.skip(1)
.withUnretained(self)
- .subscribe { (owner, gesture) in
+ .subscribe { (owner, _) in
print("swipe up")
UIView.animate(withDuration: 0.3) {
owner.bottomViewTopConstraints?.update(offset: 0)
@@ -124,11 +123,11 @@ class TestViewController: UIViewController {
}
}
.disposed(by: disposeBag)
-
+
gestureBar.rx.swipeGesture(.down)
.skip(1)
.withUnretained(self)
- .subscribe { (owner, gesture) in
+ .subscribe { (owner, _) in
print("swipe down")
switch owner.modalState {
case .top:
diff --git a/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift b/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift
index ead5cbee..4647490b 100644
--- a/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift
+++ b/Poppool/Poppool/Data/Network/AuthAPI/AuthAPIEndPoint.swift
@@ -8,9 +8,9 @@
import Foundation
struct AuthAPIEndPoint {
-
+
// MARK: - Auth API
-
+
/// 로그인을 시도합니다.
/// - Parameters:
/// - userCredential: 사용자 자격 증명
@@ -18,17 +18,17 @@ struct AuthAPIEndPoint {
/// - Returns: Endpoint
static func auth_tryLogin(with userCredential: Encodable, path: String) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/auth/\(path)",
method: .post,
bodyParameters: userCredential,
headers: ["Content-Type": "application/json"]
)
}
-
+
static func postTokenReissue() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/auth/token/reissue",
method: .post
)
diff --git a/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift b/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift
index fda16452..02f4d689 100644
--- a/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift
+++ b/Poppool/Poppool/Data/Network/CommentAPI/CommentAPIEndPoint.swift
@@ -10,28 +10,28 @@ import Foundation
import RxSwift
struct CommentAPIEndPoint {
-
+
static func postCommentAdd(request: PostCommentRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/comments",
method: .post,
bodyParameters: request
)
}
-
+
static func deleteComment(request: DeleteCommentRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/comments",
method: .delete,
queryParameters: request
)
}
-
+
static func editComment(request: PutCommentRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/comments",
method: .put,
bodyParameters: request
diff --git a/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift b/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift
index 94a63b0c..30e19486 100644
--- a/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift
+++ b/Poppool/Poppool/Data/Network/HomeAPI/HomeAPIEndpoint.swift
@@ -8,45 +8,45 @@
import Foundation
struct HomeAPIEndpoint {
-
+
static func fetchHome(
request: SortedRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/home",
method: .get,
queryParameters: request
)
}
-
+
static func fetchPopularPopUp(
request: SortedRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/home/popular/popup-stores",
method: .get,
queryParameters: request
)
}
-
+
static func fetchNewPopUp(
request: SortedRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/home/new/popup-stores",
method: .get,
queryParameters: request
)
}
-
+
static func fetchCustomPopUp(
request: SortedRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/home/custom/popup-stores",
method: .get,
queryParameters: request
diff --git a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift b/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift
index 01660653..d33857b7 100644
--- a/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/HomeAPI/ResponseDTO/GetHomeInfoResponseDTO.swift
@@ -40,4 +40,3 @@ extension GetHomeInfoResponseDTO {
)
}
}
-
diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift b/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift
index e163f336..788b52d8 100644
--- a/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift
+++ b/Poppool/Poppool/Data/Network/PopUpAPI/PopUpAPIEndPoint.swift
@@ -10,46 +10,46 @@ import Foundation
import RxSwift
struct PopUpAPIEndPoint {
-
+
static func getClosePopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/popup/closed",
method: .get,
queryParameters: request
)
}
-
+
static func getOpenPopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/popup/open",
method: .get,
queryParameters: request
)
}
-
+
static func getSearchPopUpList(request: GetSearchPopUpListRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/search/popup-stores",
method: .get,
queryParameters: request
)
}
-
+
static func getPopUpDetail(request: GetPopUpDetailRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/popup/\(request.popUpStoreId)/detail",
method: .get,
queryParameters: request
)
}
-
+
static func getPopUpComment(request: GetPopUpCommentRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/popup/\(request.popUpStoreId)/comments",
method: .get,
queryParameters: request
diff --git a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift b/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift
index b3deaf5f..60495f23 100644
--- a/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/PopUpAPI/ResponseDTO/GetSearchPopUpListResponseDTO.swift
@@ -17,5 +17,3 @@ extension GetSearchPopUpListResponseDTO {
return .init(popUpStoreList: popUpStoreList.map { $0.toDomain() }, loginYn: loginYn)
}
}
-
-
diff --git a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift b/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift
index dc29870e..147877c2 100644
--- a/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift
+++ b/Poppool/Poppool/Data/Network/SignUpAPI/SignUpAPIEndpoint.swift
@@ -8,35 +8,35 @@
import Foundation
struct SignUpAPIEndpoint {
-
+
/// 닉네임 중복을 확인합니다.
/// - Parameter request: 닉네임 체크 요청 DTO
/// - Returns: Endpoint
static func signUp_checkNickName(with request: CheckNickNameRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/signup/check-nickname",
method: .get,
queryParameters: request
)
}
-
+
/// 관심사 목록을 가져옵니다.
/// - Returns: Endpoint
static func signUp_getCategoryList() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.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: KeyPath.popPoolBaseURL,
path: "/signup",
method: .post,
bodyParameters: request
diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift
index 26323fbd..cd3b71ea 100644
--- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyCommentedPopUpResponseDTO.swift
@@ -5,7 +5,6 @@
// Created by SeoJunYoung on 1/12/25.
//
-
import Foundation
struct GetMyCommentedPopUpResponseDTO: Decodable {
diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift
index 02b18df9..5704cf9b 100644
--- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetMyProfileResponseDTO.swift
@@ -20,6 +20,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/GetOtherUserCommentedPopUpListResponseDTO.swift b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift
index 45948742..455b5d26 100644
--- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetOtherUserCommentedPopUpListResponseDTO.swift
@@ -42,6 +42,3 @@ extension GetOtherUserCommentedPopUpResponseDTO {
)
}
}
-
-
-
diff --git a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift
index f50372d7..862bccb1 100644
--- a/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift
+++ b/Poppool/Poppool/Data/Network/UserAPI/ResponseDTO/GetWithdrawlListResponseDTO.swift
@@ -26,8 +26,6 @@ extension GetWithdrawlListDataResponseDTO {
}
}
-
-
struct PostWithdrawlListRequestDTO: Encodable {
var checkedSurveyList: [GetWithdrawlListDataResponseDTO]
}
diff --git a/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift b/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift
index fce30a7d..9ca2708d 100644
--- a/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift
+++ b/Poppool/Poppool/Data/Network/UserAPI/UserAPIEndPoint.swift
@@ -10,184 +10,184 @@ import Foundation
import RxSwift
struct UserAPIEndPoint {
-
+
static func postBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/bookmark-popupstores",
method: .post,
queryParameters: request
)
}
-
+
static func deleteBookmarkPopUp(request: PostBookmarkPopUpRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/bookmark-popupstores",
method: .delete,
queryParameters: request
)
}
-
+
static func postCommentLike(request: CommentLikeRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/likes",
method: .post,
queryParameters: request
)
}
-
+
static func deleteCommentLike(request: CommentLikeRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/likes",
method: .delete,
queryParameters: request
)
}
-
+
static func postUserBlock(request: PostUserBlockRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/block",
method: .post,
queryParameters: request
)
}
-
+
static func deleteUserBlock(request: PostUserBlockRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/unblock",
method: .delete,
queryParameters: request
)
}
-
+
static func getOtherUserCommentPopUpList(request: GetOtherUserCommentListRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/\(request.commenterId ?? "")/comments",
method: .get,
queryParameters: request
)
}
-
+
static func getMyPage() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/my-page",
method: .get
)
}
-
+
static func postLogout() -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/logout",
method: .post
)
}
-
+
static func getWithdrawlList() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/withdrawl/surveys",
method: .get
)
}
-
+
static func postWithdrawl(request: PostWithdrawlListRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/delete",
method: .post,
bodyParameters: request
)
}
-
+
static func getMyProfile() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/profiles",
method: .get
)
}
-
+
static func putUserTailoredInfo(request: PutUserTailoredInfoRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/tailored-info",
method: .put,
bodyParameters: request
)
- }
-
+ }
+
static func putUserCategory(request: PutUserCategoryRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/interests",
method: .put,
bodyParameters: request
)
- }
-
+ }
+
static func putUserProfile(request: PutUserProfileRequestDTO) -> RequestEndpoint {
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/profiles",
method: .put,
bodyParameters: request
)
}
-
+
static func getMyCommentedPopUp(request: SortedRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/commented/popup",
method: .get,
queryParameters: request
)
}
-
+
static func getBlockUserList(request: GetBlockUserListRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/blocked",
method: .get,
queryParameters: request
)
}
-
+
static func getNoticeList() -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/notice/list",
method: .get
)
}
-
+
static func getNoticeDetail(noticeID: Int64) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/notice/\(noticeID)",
method: .get
)
- }
-
+ }
+
static func getRecentPopUp(request: SortedRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/recent-popupstores",
method: .get,
queryParameters: request
)
}
-
+
static func getBookmarkPopUp(request: SortedRequestDTO) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/users/bookmark-popupstores",
method: .get,
queryParameters: request
diff --git a/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift
index 7c9e2628..7e01f977 100644
--- a/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift
+++ b/Poppool/Poppool/Data/Repository/AuthAPIRepositoryImpl.swift
@@ -9,15 +9,15 @@ import Foundation
import RxSwift
final class AuthAPIRepositoryImpl {
-
+
var provider: Provider
-
+
var tokenInterceptor = TokenInterceptor()
-
+
init(provider: Provider) {
self.provider = provider
}
-
+
func tryLogIn(userCredential: Encodable, socialType: String) -> Observable {
let endPoint = AuthAPIEndPoint.auth_tryLogin(with: userCredential, path: socialType)
return provider
@@ -26,7 +26,7 @@ final class AuthAPIRepositoryImpl {
return responseDTO.toDomain()
}
}
-
+
func postTokenReissue() -> Observable {
let endPoint = AuthAPIEndPoint.postTokenReissue()
return provider.requestData(with: endPoint, interceptor: tokenInterceptor)
diff --git a/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift b/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift
index 0f3bd9ae..f5264dbf 100644
--- a/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift
+++ b/Poppool/Poppool/Data/Repository/CommentAPIRepository.swift
@@ -10,24 +10,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/HomeAPIRepository.swift b/Poppool/Poppool/Data/Repository/HomeAPIRepository.swift
index 17c9a453..2bd55980 100644
--- a/Poppool/Poppool/Data/Repository/HomeAPIRepository.swift
+++ b/Poppool/Poppool/Data/Repository/HomeAPIRepository.swift
@@ -9,29 +9,29 @@ import Foundation
import RxSwift
final class HomeAPIRepository {
-
+
private let provider: Provider
private let tokenInterceptor = TokenInterceptor()
-
+
init(provider: Provider) {
self.provider = provider
}
-
+
func fetchHome(request: SortedRequestDTO) -> Observable {
let endPoint = HomeAPIEndpoint.fetchHome(request: request)
return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() })
}
-
+
func fetchCustomPopUp(request: SortedRequestDTO) -> Observable {
let endPoint = HomeAPIEndpoint.fetchCustomPopUp(request: request)
return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() })
}
-
+
func fetchNewPopUp(request: SortedRequestDTO) -> Observable {
let endPoint = HomeAPIEndpoint.fetchNewPopUp(request: request)
return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() })
}
-
+
func fetchPopularPopUp(request: SortedRequestDTO) -> Observable {
let endPoint = HomeAPIEndpoint.fetchPopularPopUp(request: request)
return provider.requestData(with: endPoint, interceptor: tokenInterceptor).map({ $0.toDomain() })
diff --git a/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift
index 2ee2ed50..3c786005 100644
--- a/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift
+++ b/Poppool/Poppool/Data/Repository/PopUpAPIRepositoryImpl.swift
@@ -12,36 +12,36 @@ 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/SignUpRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift
index 87455fbd..6402b3dc 100644
--- a/Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift
+++ b/Poppool/Poppool/Data/Repository/SignUpRepositoryImpl.swift
@@ -9,25 +9,25 @@ import Foundation
import RxSwift
final class SignUpRepositoryImpl {
-
+
var provider: Provider
-
+
init(provider: Provider) {
self.provider = provider
}
-
+
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]> {
let endPoint = SignUpAPIEndpoint.signUp_getCategoryList()
return provider.requestData(with: endPoint, interceptor: TokenInterceptor()).map { responseDTO in
return responseDTO.categoryResponseList.map({ $0.toDomain() })
}
}
-
+
func trySignUp(
nickName: String,
gender: String,
diff --git a/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift b/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift
index 5e4cdab2..55a66ddb 100644
--- a/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift
+++ b/Poppool/Poppool/Data/Repository/UserAPIRepositoryImpl.swift
@@ -10,64 +10,64 @@ 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)
@@ -77,47 +77,47 @@ final class UserAPIRepositoryImpl {
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/GetMyCommentResponse.swift b/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift
index 85d346d8..6d47e920 100644
--- a/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift
+++ b/Poppool/Poppool/Domain/Entities/GetMyCommentResponse.swift
@@ -11,7 +11,6 @@ struct GetMyCommentedPopUpResponse {
var popUpInfoList: [GetMyCommentedPopUpDataResponse]
}
-
struct GetMyCommentedPopUpDataResponse {
var popUpStoreId: Int64
var popUpStoreName: String?
diff --git a/Poppool/Poppool/Domain/Repository/AuthRepository.swift b/Poppool/Poppool/Domain/Repository/AuthRepository.swift
index 4aea0f51..0a8c43be 100644
--- a/Poppool/Poppool/Domain/Repository/AuthRepository.swift
+++ b/Poppool/Poppool/Domain/Repository/AuthRepository.swift
@@ -8,7 +8,7 @@
import Foundation
import RxSwift
-//protocol AuthRepository {
+// protocol AuthRepository {
//
// /// 네트워크 요청을 처리하는 프로바이더
// var provider: Provider { get set }
@@ -19,4 +19,4 @@ import RxSwift
// /// - 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
index bb96f2c2..e15877e6 100644
--- a/Poppool/Poppool/Domain/UseCase/AuthAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/AuthAPIUseCaseImpl.swift
@@ -9,17 +9,17 @@ 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
index 1cd85b34..f6d83409 100644
--- a/Poppool/Poppool/Domain/UseCase/CommentAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/CommentAPIUseCaseImpl.swift
@@ -10,21 +10,21 @@ 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
index 3c28efac..1664dd60 100644
--- a/Poppool/Poppool/Domain/UseCase/HomeAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/HomeAPIUseCaseImpl.swift
@@ -10,7 +10,7 @@ import RxSwift
final class HomeAPIUseCaseImpl {
var repository = HomeAPIRepository(provider: ProviderImpl())
-
+
func fetchHome(
page: Int32?,
size: Int32?,
@@ -18,7 +18,7 @@ final class HomeAPIUseCaseImpl {
) -> Observable {
return repository.fetchHome(request: .init(page: page, size: size, sort: sort))
}
-
+
func fetchCustomPopUp(
page: Int32?,
size: Int32?,
@@ -26,7 +26,7 @@ final class HomeAPIUseCaseImpl {
) -> Observable {
return repository.fetchCustomPopUp(request: .init(page: page, size: size, sort: sort))
}
-
+
func fetchNewPopUp(
page: Int32?,
size: Int32?,
@@ -34,7 +34,7 @@ final class HomeAPIUseCaseImpl {
) -> Observable {
return repository.fetchNewPopUp(request: .init(page: page, size: size, sort: sort))
}
-
+
func fetchPopularPopUp(
page: Int32?,
size: Int32?,
diff --git a/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift
index 2b00740c..6c405d4a 100644
--- a/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/PopUpAPIUseCaseImpl.swift
@@ -10,13 +10,13 @@ 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 {
@@ -29,17 +29,17 @@ final class PopUpAPIUseCaseImpl {
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)
+ 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/SignUpAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift
index e67b1868..006d48fe 100644
--- a/Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/SignUpAPIUseCaseImpl.swift
@@ -10,7 +10,7 @@ import RxSwift
final class SignUpAPIUseCaseImpl {
var repository: SignUpRepositoryImpl
-
+
init(repository: SignUpRepositoryImpl) {
self.repository = repository
}
@@ -36,7 +36,7 @@ final class SignUpAPIUseCaseImpl {
func checkNickName(nickName: String) -> Observable {
return repository.checkNickName(nickName: nickName)
}
-
+
func fetchCategoryList() -> Observable<[Category]> {
return repository.fetchCategoryList()
}
diff --git a/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift b/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift
index 2eab0c45..1c907205 100644
--- a/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift
+++ b/Poppool/Poppool/Domain/UseCase/UserAPIUseCaseImpl.swift
@@ -8,37 +8,37 @@
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?,
@@ -56,31 +56,31 @@ final class UserAPIUseCaseImpl {
)
.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],
@@ -94,31 +94,31 @@ final class UserAPIUseCaseImpl {
)
)
}
-
+
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 {
+
+ 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/AppleLoginService.swift b/Poppool/Poppool/Infrastructure/AppleLoginService.swift
index e58e20b5..ecee526f 100644
--- a/Poppool/Poppool/Infrastructure/AppleLoginService.swift
+++ b/Poppool/Poppool/Infrastructure/AppleLoginService.swift
@@ -5,25 +5,25 @@
// Created by SeoJunYoung on 8/20/24.
//
-import RxSwift
import AuthenticationServices
+import RxSwift
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
@@ -48,7 +48,7 @@ extension AppleLoginService: ASAuthorizationControllerPresentationContextProvidi
}
return window
}
-
+
// 인증 성공 시 호출되는 함수
func authorizationController(
controller: ASAuthorizationController,
@@ -58,29 +58,18 @@ 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
}
diff --git a/Poppool/Poppool/Infrastructure/AuthServiceable.swift b/Poppool/Poppool/Infrastructure/AuthServiceable.swift
index 79c2330e..cd20f3ee 100644
--- a/Poppool/Poppool/Infrastructure/AuthServiceable.swift
+++ b/Poppool/Poppool/Infrastructure/AuthServiceable.swift
@@ -24,5 +24,5 @@ struct AuthServiceResponse: Encodable {
enum AuthError: Error {
case notInstalled
- case unknownError
+ case unknownError(description: String?)
}
diff --git a/Poppool/Poppool/Infrastructure/ImageLoader/DiskStorage.swift b/Poppool/Poppool/Infrastructure/ImageLoader/DiskStorage.swift
new file mode 100644
index 00000000..127639b3
--- /dev/null
+++ b/Poppool/Poppool/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/Poppool/Infrastructure/ImageLoader/ImageLoader.swift b/Poppool/Poppool/Infrastructure/ImageLoader/ImageLoader.swift
new file mode 100644
index 00000000..efaecd9d
--- /dev/null
+++ b/Poppool/Poppool/Infrastructure/ImageLoader/ImageLoader.swift
@@ -0,0 +1,149 @@
+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)
+ completion(.success(image))
+ } else {
+ completion(.failure(ImageLoaderError.convertError(description: "Failed to convert data to UIImage")))
+ }
+ case .failure(let error):
+ 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/Poppool/Infrastructure/ImageLoader/MemoryStorage.swift b/Poppool/Poppool/Infrastructure/ImageLoader/MemoryStorage.swift
new file mode 100644
index 00000000..2d4b30dc
--- /dev/null
+++ b/Poppool/Poppool/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/Poppool/Infrastructure/KakaoLoginService.swift b/Poppool/Poppool/Infrastructure/KakaoLoginService.swift
index 951c56b5..382133e9 100644
--- a/Poppool/Poppool/Infrastructure/KakaoLoginService.swift
+++ b/Poppool/Poppool/Infrastructure/KakaoLoginService.swift
@@ -1,24 +1,11 @@
-//
-// KakaoLoginService.swift
-// MomsVillage
-//
-// Created by SeoJunYoung on 8/13/24.
-//
-
-import RxSwift
-import KakaoSDKUser
-import RxKakaoSDKUser
import KakaoSDKAuth
+import KakaoSDKUser
+import RxSwift
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
@@ -30,13 +17,14 @@ final class KakaoLoginService: AuthServiceable {
observer.onCompleted()
}
}
+
return Disposables.create()
}
}
-
+
func fetchUserCredential() -> Observable {
return Observable.create { [weak self] observer in
- guard let self = self else {
+ guard let self else {
Logger.log(
message: "KakaoTalk login Error",
category: .error,
@@ -45,7 +33,8 @@ final class KakaoLoginService: AuthServiceable {
)
return Disposables.create()
}
- // 카카오톡 설치 유무
+
+ // 카카오톡 설치 유무 확인
guard UserApi.isKakaoTalkLoginAvailable() else {
Logger.log(
message: "KakaoTalk is not install",
@@ -53,79 +42,62 @@ final class KakaoLoginService: AuthServiceable {
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)
- }
- }
- }
+
+ // 카카오톡 미설치시 웹으로 인증 시도
+ loginWithKakaoTalkWeb(observer: observer)
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)
-
+
+ // 카카오톡 설치시 앱으로 인증 시도
+ loginWithKakaoTalkApp(observer: observer)
+
return Disposables.create()
}
}
}
private extension KakaoLoginService {
-
+
+ /// 제공된 액세스 토큰을 사용하여 사용자의 카카오 ID를 가져옵니다.
+ /// - Parameters:
+ /// - observer: 인증 응답을 처리할 옵저버.
+ /// - accessToken: 카카오 로그인 과정에서 얻은 액세스 토큰.
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)
+ 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()
+ }
+ }
}
- 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
- )
+ /// 카카오톡 앱을 사용하여 로그인하고 액세스 토큰을 가져옵니다.
+ /// - 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)
+ }
}
+ }
}
-
- 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
- )
+
+ /// 카카오톡 웹을 사용하여 로그인하고 액세스 토큰을 가져옵니다.
+ /// - 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/KeyChainService.swift b/Poppool/Poppool/Infrastructure/KeyChainService.swift
index 7717acea..ebcfcb70 100644
--- a/Poppool/Poppool/Infrastructure/KeyChainService.swift
+++ b/Poppool/Poppool/Infrastructure/KeyChainService.swift
@@ -18,10 +18,10 @@ final class KeyChainService {
case unhandledError(status: OSStatus) // 예상치 못한 OSStatus 오류가 발생했을 때 발생
case dataConversionError(message: String) // 데이터 변환 중 오류가 발생했을 때 발생
}
-
+
// KeyChain 서비스 이름
private let service = "keyChain"
-
+
/// KeyChain에서 특정 타입의 토큰을 가져오는 메서드
/// - Parameter type: 가져오려는 토큰의 타입 (`accessToken` 또는 `refreshToken`)
/// - Returns: 가져온 토큰을 담은 `Single`
@@ -34,11 +34,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."))
@@ -72,7 +72,7 @@ final class KeyChainService {
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,11 +80,11 @@ 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 {
@@ -99,7 +99,7 @@ final class KeyChainService {
return .failure(KeyChainError.unhandledError(status: status))
}
}
-
+
/// KeyChain에서 특정 타입의 토큰을 삭제하는 메서드
/// - Parameter type: 삭제하려는 토큰의 타입 (`accessToken` 또는 `refreshToken`)
/// - Returns: 완료 시 `Completable`
@@ -110,10 +110,10 @@ final class KeyChainService {
kSecAttrService: self.service,
kSecAttrAccount: type.rawValue
]
-
+
// 2. Delete
let status = SecItemDelete(keyChainQuery)
-
+
if status == errSecSuccess {
Logger.log(
message: "Successfully deleted \(type.rawValue) from KeyChain",
diff --git a/Poppool/Poppool/Infrastructure/Logger/Logger.swift b/Poppool/Poppool/Infrastructure/Logger/Logger.swift
index cf3cbadb..9ad233b2 100644
--- a/Poppool/Poppool/Infrastructure/Logger/Logger.swift
+++ b/Poppool/Poppool/Infrastructure/Logger/Logger.swift
@@ -15,7 +15,7 @@ struct Logger {
case error
case event
case custom(categoryName: String)
-
+
var categoryName: String {
switch self {
case .info:
@@ -32,7 +32,7 @@ struct Logger {
return categoryName
}
}
-
+
var categoryIcon: String {
switch self {
case .info:
@@ -50,13 +50,13 @@ struct Logger {
}
}
}
-
+
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,
diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift
index cd902b29..9933fba3 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/Common/Requestable.swift
@@ -24,14 +24,14 @@ extension Requestable {
/// - Returns: URLRequest 반환
func getUrlRequest() throws -> URLRequest {
let url = try url()
-
+
Logger.log(
message: "\(url) URL 생성",
category: .network,
fileName: #file,
line: #line
)
-
+
var urlRequest = URLRequest(url: url)
// httpBody
if let bodyParameters = try bodyParameters?.toDictionary() {
@@ -46,7 +46,7 @@ extension Requestable {
headers?.forEach { urlRequest.setValue($1, forHTTPHeaderField: $0) }
return urlRequest
}
-
+
/// APIEndpoint에서 전달받은 DTO를 URL로 변환하는 메서드
/// - Returns: URL 반환
func url() throws -> URL {
@@ -80,7 +80,7 @@ extension Requestable {
}
extension Encodable {
-
+
/// URL에 요청할 쿼리 데이터를 JSON 형식에 맞게 딕셔너리 구조로 변환하는 메서드
/// - Returns: jsonData
func toDictionary() throws -> [String: Any]? {
diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift
index cdbe56d6..fcc83980 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/Endpoint.swift
@@ -13,7 +13,7 @@ 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/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift
index 42d83dd0..75b4ef2e 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/EndPoint/MultipartEndPoint.swift
@@ -17,7 +17,7 @@ class MultipartEndPoint: URLRequestConvertible {
var jsonData: [String: Any]?
var images: [UIImage]
var headers: [String: String]?
-
+
init(
baseURL: String,
path: String,
@@ -35,22 +35,22 @@ class MultipartEndPoint: URLRequestConvertible {
self.images = images
self.headers = headers
}
-
+
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL().appendingPathComponent(path)
var request = URLRequest(url: url)
Logger.log(message: "\(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 {
@@ -65,7 +65,7 @@ class MultipartEndPoint: URLRequestConvertible {
Logger.log(message: "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/IndicatorMaker/IndicatorMaker.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift
index f25705e3..997125a1 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/IndicatorMaker/IndicatorMaker.swift
@@ -11,26 +11,26 @@ 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()
@@ -44,7 +44,7 @@ struct IndicatorMaker {
topVC.view.isUserInteractionEnabled = false
}
}
-
+
static func hideIndicator() {
DispatchQueue.main.async {
indicatorImageView.stop()
diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift
index efd1d123..f057df7d 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/FormDataInterceptor.swift
@@ -5,20 +5,20 @@
// Created by SeoJunYoung on 10/25/24.
//
-import Foundation
import Alamofire
+import Foundation
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,
diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift
index 9b4d8c55..9ad8e40f 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/Interceptor/TokenInterceptor.swift
@@ -5,12 +5,12 @@
// Created by SeoJunYoung on 10/14/24.
//
-import Foundation
import Alamofire
+import Foundation
import RxSwift
final class TokenInterceptor: RequestInterceptor {
-
+
func adapt(
_ urlRequest: URLRequest,
for session: Session,
@@ -29,7 +29,7 @@ final class TokenInterceptor: RequestInterceptor {
completion(.success(urlRequest))
}
}
-
+
func retry(
_ request: Request,
for session: Session,
diff --git a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift b/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift
index edc12bfd..1a2e4394 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/Provider.swift
@@ -7,11 +7,11 @@
import Foundation
-import RxSwift
import Alamofire
+import RxSwift
protocol Provider {
-
+
/// 네트워크 요청을 수행하고 결과를 반환하는 메서드
/// - Parameters:
/// - endpoint: 요청할 엔드포인트
@@ -22,7 +22,7 @@ protocol Provider {
with endpoint: E,
interceptor: RequestInterceptor?
) -> Observable where R == E.Response
-
+
/// 네트워크 요청을 수행하고 결과를 반환하는 메서드
/// - Parameters:
/// - request: 요청할 Requestable 객체
@@ -33,7 +33,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/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift
index efa81ca6..a7fa9517 100644
--- a/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift
+++ b/Poppool/Poppool/Infrastructure/NetworkLayer/Provider/ProviderImpl.swift
@@ -1,13 +1,6 @@
-//
-// ProviderImpl.swift
-// MomsVillage
-//
-// Created by SeoJunYoung on 8/16/24.
-//
-
+import Alamofire
import Foundation
import RxSwift
-import Alamofire
final class ProviderImpl: Provider {
@@ -50,9 +43,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 디코딩
diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift b/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift
index a1bba549..e6e12f83 100644
--- a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift
+++ b/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedAPIEndPoint.swift
@@ -11,7 +11,7 @@ struct PreSignedAPIEndPoint {
static func presigned_upload(request: PresignedURLRequestDTO) -> Endpoint {
Logger.log(message: "Presigned URL 생성 - Request: \(request)", category: .debug)
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/files/upload-preSignedUrl",
method: .post,
bodyParameters: request
@@ -21,7 +21,7 @@ struct PreSignedAPIEndPoint {
static func presigned_download(request: PresignedURLRequestDTO) -> Endpoint {
Logger.log(message: "Presigned Download URL 생성 - Request: \(request)", category: .debug)
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/files/download-preSignedUrl",
method: .post,
bodyParameters: request
@@ -31,7 +31,7 @@ struct PreSignedAPIEndPoint {
static func presigned_delete(request: PresignedURLRequestDTO) -> RequestEndpoint {
Logger.log(message: "Presigned Delete 생성 - Request: \(request)", category: .debug)
return RequestEndpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/files/delete",
method: .post,
bodyParameters: request
diff --git a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift b/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift
index 73343587..26efb286 100644
--- a/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift
+++ b/Poppool/Poppool/Infrastructure/PreSignedService/PreSignedService.swift
@@ -8,9 +8,9 @@
import Foundation
import UIKit
-import RxSwift
-import RxCocoa
import Alamofire
+import RxCocoa
+import RxSwift
class ImageCache {
static let shared = NSCache()
@@ -24,9 +24,9 @@ class PreSignedService {
}
let tokenInterceptor = TokenInterceptor()
-
+
let provider = ProviderImpl()
-
+
let disposeBag = DisposeBag()
func tryDelete(targetPaths: PresignedURLRequestDTO) -> Completable {
@@ -82,7 +82,6 @@ class PreSignedService {
}
}
-
func tryDownload(filePaths: [String]) -> Single<[UIImage]> {
return Single.create { [weak self] observer in
@@ -149,7 +148,6 @@ class PreSignedService {
}
}
-
private extension PreSignedService {
func uploadFromS3(url: String, image: UIImage) -> Single {
@@ -204,8 +202,6 @@ private extension PreSignedService {
}
}
-
-
func getUploadLinks(request: PresignedURLRequestDTO) -> Observable {
Logger.log(message: "Presigned URL 생성 요청 데이터: \(request)", category: .debug)
let provider = ProviderImpl()
@@ -291,7 +287,7 @@ extension PreSignedService {
}
}
func fullImageURL(from filePath: String) -> URL? {
- let baseURL = Secrets.popPoolS3BaseURL.rawValue
+ let baseURL = KeyPath.popPoolS3BaseURL
// URL 인코딩 처리를 더 엄격하게
guard let encodedPath = filePath
@@ -308,4 +304,3 @@ extension PreSignedService {
}
}
-
diff --git a/Poppool/Poppool/Infrastructure/UserDefaultService.swift b/Poppool/Poppool/Infrastructure/UserDefaultService.swift
index ffb4c2de..ff64c2a6 100644
--- a/Poppool/Poppool/Infrastructure/UserDefaultService.swift
+++ b/Poppool/Poppool/Infrastructure/UserDefaultService.swift
@@ -10,27 +10,27 @@ 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) {
+ 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]) {
+ func save(key: String, value: [String]) {
UserDefaults.standard.set(value, forKey: key)
}
-
+
/// Userdefault 데이터 발견 메서드
/// - Parameters:
/// - key: 찾는 데이터의 키 값 i.e) 유저 id 등
@@ -42,7 +42,7 @@ final class UserDefaultService {
}
return nil
}
-
+
/// Userdefault 데이터 발견 메서드
/// - Parameters:
/// - key: 찾는 데이터의 키 값 i.e) 유저 id 등
@@ -54,7 +54,7 @@ final class UserDefaultService {
}
return nil
}
-
+
/// Userdefault 데이터 삭제 메서드
/// - Parameters:
/// - key: 삭제하는 데이터의 키 값 i.e) 유저 id 등
diff --git a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift b/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift
index 5876db70..b23047dc 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetView.swift
@@ -1,15 +1,15 @@
-import UIKit
-import SnapKit
-import RxSwift
-import RxCocoa
import ReactorKit
+import RxCocoa
+import RxSwift
+import SnapKit
+import UIKit
final class AdminBottomSheetView: UIView {
-
+
// MARK: - Properties
private var contentHeightConstraint: Constraint?
typealias Reactor = AdminBottomSheetReactor
-
+
// MARK: - Components
let containerView: UIView = {
let view = UIView()
@@ -19,44 +19,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 +65,7 @@ final class AdminBottomSheetView: UIView {
subitems: [item]
)
group.interItemSpacing = .fixed(12)
-
+
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(
top: 20,
@@ -75,10 +74,10 @@ final class AdminBottomSheetView: UIView {
trailing: 20
)
section.interGroupSpacing = 16
-
+
return section
}
-
+
let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: layout
@@ -87,14 +86,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 +105,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 +123,7 @@ final class AdminBottomSheetView: UIView {
)
return button
}()
-
+
private let buttonStack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
@@ -132,74 +131,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 +206,20 @@ final class AdminBottomSheetView: UIView {
make.height.equalTo(52)
}
}
-
+
// MARK: - Public Methods
func updateContentVisibility(isCategorySelected: Bool) {
Logger.log(message: "높이 변경 시작: \(isCategorySelected ? "카테고리" : "상태값")", category: .debug)
-
+
let newHeight: CGFloat = isCategorySelected ? 200 : 160
-
+
// 애니메이션 없이 바로 적용
contentHeightConstraint?.update(offset: newHeight)
contentCollectionView.invalidateIntrinsicContentSize()
-
+
setNeedsLayout()
layoutIfNeeded()
-
+
Logger.log(message: "높이 변경 완료", category: .debug)
}
}
diff --git a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift b/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift
index dbee91f3..4f3a1a7b 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminBottomSheet/AdminBottomSheetViewController.swift
@@ -1,9 +1,8 @@
-import UIKit
-import SnapKit
-import RxSwift
-import RxCocoa
import ReactorKit
-
+import RxCocoa
+import RxSwift
+import SnapKit
+import UIKit
final class AdminBottomSheetViewController: BaseViewController, View {
@@ -28,8 +27,6 @@ final class AdminBottomSheetViewController: BaseViewController, View {
fatalError("init(coder:) has not been implemented")
}
-
-
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
@@ -74,7 +71,7 @@ final class AdminBottomSheetViewController: BaseViewController, View {
Logger.log(message: "최종 뷰 계층:", category: .debug)
}
-
+
private func setupCollectionView() {
mainView.contentCollectionView.register(
TagSectionCell.self,
@@ -85,7 +82,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)
diff --git a/Poppool/Poppool/Presentation/Admin/AdminReactor.swift b/Poppool/Poppool/Presentation/Admin/AdminReactor.swift
index 1d0f2cbd..e258e6df 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminReactor.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminReactor.swift
@@ -1,6 +1,6 @@
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
final class AdminReactor: Reactor {
diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift
index b02ef0fe..f3acf044 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterReactor.swift
@@ -1,7 +1,6 @@
import ReactorKit
-import RxSwift
import RxCocoa
-
+import RxSwift
final class PopUpStoreRegisterReactor: Reactor {
diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift
index 5c25cf83..5b4b04fc 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterView.swift
@@ -1,7 +1,7 @@
-import UIKit
-import SnapKit
-import RxSwift
import RxCocoa
+import RxSwift
+import SnapKit
+import UIKit
final class PopUpStoreRegisterView: UIView {
// 상단 네비게이션 영역
diff --git a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift
index a559ee54..3c21a2e2 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminRegister/PopUpStoreRegisterViewController.swift
@@ -1,12 +1,13 @@
+import CoreLocation
+import PhotosUI
import UIKit
-import SnapKit
+
+import Alamofire
import ReactorKit
-import RxSwift
import RxCocoa
-import PhotosUI
-import Alamofire
-import GoogleMaps
-import CoreLocation
+import RxSwift
+import SnapKit
+import Then
final class PopUpStoreRegisterViewController: BaseViewController {
@@ -24,8 +25,11 @@ final class PopUpStoreRegisterViewController: BaseViewController {
private var lonField: UITextField?
private var descTV: UITextView?
-
private let popupName: String = ""
+ private var originalImageIds: [String: Int64] = [:]
+ private var deletedImageIds: [Int64] = []
+ private var deletedImagePaths: [String] = []
+
private let editingStore: GetAdminPopUpStoreListResponseDTO.PopUpStore?
let presignedService = PreSignedService()
@@ -76,7 +80,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
return lbl
}()
-
private let menuButton: UIButton = {
let btn = UIButton(type: .system)
btn.setImage(UIImage(systemName: "adminlist"), for: .normal)
@@ -101,7 +104,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
return lbl
}()
-
private let addImageButton = UIButton(type: .system).then {
$0.setTitle("이미지 추가", for: .normal)
$0.setTitleColor(.systemBlue, for: .normal)
@@ -117,14 +119,13 @@ final class PopUpStoreRegisterViewController: BaseViewController {
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 formBackgroundView = UIView().then {
+ $0.backgroundColor = .white
+ $0.layer.borderWidth = 1
+ $0.layer.borderColor = UIColor.lightGray.cgColor
+ $0.layer.cornerRadius = 8
+ }
+
private let verticalStack = UIStackView()
// MARK: - Bottom Save Button
@@ -153,12 +154,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
let btn = UIButton(type: .system)
btn.setTitle("카테고리 선택 ▾", for: .normal)
btn.setTitleColor(.darkGray, for: .normal)
- btn.titleLabel?.font = UIFont.systemFont(ofSize:14)
+ 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)
+ btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8)
return btn
}()
@@ -166,12 +167,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
let btn = UIButton(type: .system)
btn.setTitle("기간 선택 ▾", for: .normal)
btn.setTitleColor(.darkGray, for: .normal)
- btn.titleLabel?.font = UIFont.systemFont(ofSize:14)
+ 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)
+ btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8)
return btn
}()
@@ -179,27 +180,39 @@ final class PopUpStoreRegisterViewController: BaseViewController {
let btn = UIButton(type: .system)
btn.setTitle("시간 선택 ▾", for: .normal)
btn.setTitleColor(.darkGray, for: .normal)
- btn.titleLabel?.font = UIFont.systemFont(ofSize:14)
+ 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)
+ 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)
+
+ view.backgroundColor = UIColor(white: 0.95, alpha: 1)
+
if let store = editingStore {
- loadStoreDetail(for: store.id)
- }
+ // 삭제된 이미지 ID 복원
+ if let savedIds = UserDefaults.standard.array(forKey: "deletedImageIds_\(store.id)") as? [Int64] {
+ self.deletedImageIds = savedIds
+ Logger.log(message: "저장된 삭제 이미지 ID 복원: \(savedIds.count)개", category: .debug)
+ }
+
+ // 삭제된 이미지 경로 복원
+ if let savedPaths = UserDefaults.standard.array(forKey: "deletedImagePaths_\(store.id)") as? [String] {
+ self.deletedImagePaths = savedPaths
+ Logger.log(message: "저장된 삭제 이미지 경로 복원: \(savedPaths.count)개", category: .debug)
+ }
+ loadStoreDetail(for: store.id)
+ }
setupNavigation()
setupLayout()
setupRows()
@@ -207,11 +220,10 @@ final class PopUpStoreRegisterViewController: BaseViewController {
setupImageCollectionActions()
setupKeyboardHandling()
setupAddressField()
-
+ setupAllFieldListeners()
}
-
// MARK: - Navigation
private func setupNavigation() {
backButton.addTarget(self, action: #selector(onBack), for: .touchUpInside)
@@ -235,6 +247,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
}
private func fillFormWithExistingData(_ storeDetail: GetAdminPopUpStoreDetailResponseDTO) {
+ // 기본 필드 채우기
nameField?.text = storeDetail.name
categoryButton.setTitle("\(storeDetail.categoryName) ▾", for: .normal)
addressField?.text = storeDetail.address
@@ -242,30 +255,119 @@ final class PopUpStoreRegisterViewController: BaseViewController {
lonField?.text = String(storeDetail.longitude)
descTV?.text = storeDetail.desc
+ // 중요: ID와 URL 매핑 초기화 및 설정
+ self.originalImageIds.removeAll()
+ // deletedImageIds와 deletedImagePaths는 초기화하지 않음 (기존 삭제 정보 유지)
+
+ // 중요: 여기서 originalImageIds 맵을 세팅합니다
+ for image in storeDetail.imageList {
+ self.originalImageIds[image.imageUrl] = image.id
+ Logger.log(message: "이미지 ID 매핑: \(image.imageUrl) -> \(image.id)", category: .debug)
+ }
+
+ // 날짜 설정
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()
+ self.updatePeriodButtonTitle()
+ }
+
+ // 중요: 기존 이미지는 먼저 모두 제거
+ self.images.removeAll()
+
+ // 이미지 목록 디버깅 - 실제 서버에서 받은 목록 확인
+ Logger.log(message: "서버에서 받은 이미지 목록 (총 \(storeDetail.imageList.count)개):", category: .debug)
+ for (index, image) in storeDetail.imageList.enumerated() {
+ Logger.log(message: " \(index+1). ID: \(image.id), URL: \(image.imageUrl)", category: .debug)
+ }
+
+ // 삭제된 이미지 ID 집합 생성 (빠른 검색을 위해)
+ let deletedIdSet = Set(self.deletedImageIds)
+ Logger.log(message: "삭제된 이미지 ID 목록: \(deletedIdSet)", category: .debug)
+
+ // 중복 및 삭제된 이미지 제외를 위한 집합
+ var loadedImageUrls = Set()
+
+ let dispatchGroup = DispatchGroup()
+ let mainImageUrl = storeDetail.mainImageUrl
+ Logger.log(message: "대표 이미지 URL: \(mainImageUrl)", category: .debug)
+
+ for imageData in storeDetail.imageList {
+ // 중복 이미지 건너뛰기
+ if loadedImageUrls.contains(imageData.imageUrl) {
+ Logger.log(message: "중복 이미지 스킵: \(imageData.imageUrl)", category: .debug)
+ continue
+ }
+
+ // 삭제된 이미지 건너뛰기
+ if deletedIdSet.contains(imageData.id) {
+ Logger.log(message: "삭제된 이미지 스킵: ID \(imageData.id), URL: \(imageData.imageUrl)", category: .debug)
+ continue
+ }
+
+ loadedImageUrls.insert(imageData.imageUrl)
+
+ dispatchGroup.enter()
+
+ if let imageURL = presignedService.fullImageURL(from: imageData.imageUrl) {
+ Logger.log(message: "이미지 로드 시작: \(imageData.imageUrl)", category: .debug)
+
+ URLSession.shared.dataTask(with: imageURL) { [weak self] data, response, error in
+ defer { dispatchGroup.leave() }
+
+ // 응답 상태 코드 확인 추가
+ if let httpResponse = response as? HTTPURLResponse {
+ Logger.log(message: "이미지 로드 응답 코드: \(httpResponse.statusCode) - URL: \(imageData.imageUrl)", category: .debug)
+ if httpResponse.statusCode != 200 {
+ Logger.log(message: "이미지 로드 실패 - 상태 코드: \(httpResponse.statusCode)", category: .error)
+ return
+ }
+ }
+
+ if let error = error {
+ Logger.log(message: "이미지 로드 오류: \(error.localizedDescription)", category: .error)
+ return
+ }
+
+ guard let self = self,
+ let data = data,
+ let image = UIImage(data: data) else {
+ Logger.log(message: "이미지 변환 실패", category: .error)
+ return
+ }
+
+ let isMain = (imageData.imageUrl == mainImageUrl)
+
+ let extendedImage = ExtendedImage(filePath: imageData.imageUrl, image: image, isMain: isMain)
+
+ DispatchQueue.main.async {
+ self.images.append(extendedImage)
+ Logger.log(message: "이미지 로드 완료: \(imageData.imageUrl), isMain: \(isMain)", category: .debug)
+ }
+ }.resume()
+ } else {
+ Logger.log(message: "이미지 URL 생성 실패: \(imageData.imageUrl)", category: .error)
+ dispatchGroup.leave()
+ }
+ }
+
+ dispatchGroup.notify(queue: .main) { [weak self] in
+ guard let self = self else { return }
+
+ if !self.images.isEmpty && !self.images.contains(where: { $0.isMain }) {
+ self.images[0].isMain = true
+ Logger.log(message: "대표 이미지가 없어 첫 번째 이미지를 대표로 설정", category: .debug)
+ }
+
+ Logger.log(message: "모든 이미지 로드 완료: 총 \(self.images.count)개", category: .debug)
+ self.imagesCollectionView.reloadData()
+ self.updateSaveButtonState()
}
}
+
func loadStoreDetail(for storeId: Int64) {
Logger.log(message: "상세 정보 요청 시작 - Store ID: \(storeId)", category: .debug)
@@ -281,7 +383,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
private func setupKeyboardHandling() {
- // 키보드 Notification 등록
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
@@ -338,8 +439,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
}
-
-
// MARK: - Layout
private func setupLayout() {
// (1) 상단 컨테이너
@@ -449,10 +548,40 @@ final class PopUpStoreRegisterViewController: BaseViewController {
make.height.equalTo(44)
}
}
+ private func getCurrentFormattedTime() -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy.MM.dd HH:mm"
+ formatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 한국 시간대 명시적 설정
+ return formatter.string(from: Date())
+ }
+ private func setupCreationTimeLabel() -> UILabel {
+ let currentTime = getCurrentFormattedTime()
+ return makeSimpleLabel(currentTime)
+ }
+
+ private func setupAllFieldListeners() {
+ // 이름 필드
+ nameField?.addTarget(self, action: #selector(anyFieldDidChange(_:)), for: .editingChanged)
+
+ // 주소, 위도, 경도 필드
+ addressField?.addTarget(self, action: #selector(anyFieldDidChange(_:)), for: .editingChanged)
+ latField?.addTarget(self, action: #selector(anyFieldDidChange(_:)), for: .editingChanged)
+ lonField?.addTarget(self, action: #selector(anyFieldDidChange(_:)), for: .editingChanged)
+
+ // 설명 필드 (UITextView는 다르게 처리해야 함)
+ descTV?.delegate = self
+
+ // 로그 추가
+ Logger.log(message: "모든 필드에 변경 리스너가 설정되었습니다", category: .debug)
+ }
+ @objc private func anyFieldDidChange(_ textField: UITextField) {
+ Logger.log(message: "\(textField.accessibilityIdentifier ?? "알 수 없는 필드") 값 변경: \(textField.text ?? "nil")", category: .debug)
+ updateSaveButtonState()
+ }
// MARK: - Setup Rows
private func setupRows() {
addRowTextField(leftTitle: "이름", placeholder: "팝업스토어 이름을 입력해 주세요.")
- addRowTextField(leftTitle: "이미지", placeholder: "팝업스토어 대표 이미지를 업로드 해주세요.")
+// addRowTextField(leftTitle: "이미지", placeholder: "팝업스토어 대표 이미지를 업로드 해주세요.")
categoryButton.addTarget(self, action: #selector(didTapCategoryButton), for: .touchUpInside)
addRowCustom(leftTitle: "카테고리", rightView: categoryButton)
@@ -461,7 +590,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
// 1) 주소 (TextField)
let addressField = makeRoundedTextField("팝업스토어 주소를 입력해 주세요.")
self.addressField = addressField
- addressField.snp.makeConstraints { make in
+ addressField.snp.makeConstraints { _ in
addressField.addTarget(self, action: #selector(fieldDidChange(_:)), for: .editingChanged)
}
@@ -473,14 +602,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
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
@@ -502,16 +629,15 @@ final class PopUpStoreRegisterViewController: BaseViewController {
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
@@ -531,7 +657,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
markerVStack.spacing = 8
markerVStack.distribution = .fillEqually
-
// 한 행 => "마커" 라벨, 오른쪽 2줄 (마커명, 스니펫)
addRowCustom(leftTitle: "마커", rightView: markerVStack, rowHeight: nil, totalHeight: 80)
@@ -548,7 +673,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
addRowCustom(leftTitle: "작성자", rightView: writerLbl)
// (13) 작성시간
- let timeLbl = makeSimpleLabel("2025.01.06 10:30")
+ let timeLbl = setupCreationTimeLabel()
addRowCustom(leftTitle: "작성시간", rightView: timeLbl)
// (14) 상태값
@@ -562,7 +687,6 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
-
// MARK: - Row
private func addRowTextField(leftTitle: String, placeholder: String) {
@@ -641,17 +765,21 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
.disposed(by: disposeBag)
}
- // 저장 버튼 활성화 여부 갱신
private func updateSaveButtonState() {
- let isFormValid = validateForm() // 폼 유효성 검사 결과
+ // 디버깅을 위한 로깅 추가
+ Logger.log(message: "updateSaveButtonState 호출됨", category: .debug)
+
+ let isFormValid = validateForm()
+
+ // 이전 상태와 새 상태가 다를 때만 로그 출력
+ if saveButton.isEnabled != isFormValid {
+ Logger.log(message: "저장 버튼 상태 변경: \(saveButton.isEnabled) -> \(isFormValid)", category: .debug)
+ }
+
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,
@@ -744,13 +872,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
}
}
-
private func updatePeriodButtonTitle() {
- guard let s = selectedStartDate, let e = selectedEndDate else { return }
+ guard let selectedStartDate = selectedStartDate, let selectedEndDate = selectedEndDate else { return }
let df = DateFormatter()
df.dateFormat = "yyyy.MM.dd"
- let sStr = df.string(from: s)
- let eStr = df.string(from: e)
+ let sStr = df.string(from: selectedStartDate)
+ let eStr = df.string(from: selectedEndDate)
periodButton.setTitle("\(sStr) ~ \(eStr)", for: .normal)
}
@@ -845,13 +972,14 @@ final class PopUpStoreRegisterViewController: BaseViewController {
private func updateCategoryButtonTitle(with category: String) {
categoryButton.setTitle("\(category) ▾", for: .normal)
+ updateSaveButtonState()
}
// MARK: - UI Helpers
private func makeRoundedTextField(_ placeholder: String) -> UITextField {
let tf = UITextField()
tf.placeholder = placeholder
- tf.font = UIFont.systemFont(ofSize:14)
+ tf.font = UIFont.systemFont(ofSize: 14)
tf.textColor = .darkGray
tf.borderStyle = .none
tf.layer.cornerRadius = 8
@@ -865,12 +993,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
let btn = UIButton(type: .system)
btn.setTitle(title, for: .normal)
btn.setTitleColor(.darkGray, for: .normal)
- btn.titleLabel?.font = UIFont.systemFont(ofSize:14)
+ 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)
+ btn.contentEdgeInsets = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8)
return btn
}
@@ -879,7 +1007,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
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)
+ btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 0)
}
return btn
}
@@ -887,7 +1015,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
private func makeSimpleLabel(_ text: String) -> UILabel {
let lbl = UILabel()
lbl.text = text
- lbl.font = UIFont.systemFont(ofSize:14)
+ lbl.font = UIFont.systemFont(ofSize: 14)
lbl.textColor = .darkGray
return lbl
}
@@ -896,7 +1024,7 @@ final class PopUpStoreRegisterViewController: BaseViewController {
// 작은 라벨(위도/경도/마커명/스니펫 등)
let lbl = UILabel()
lbl.text = text
- lbl.font = UIFont.systemFont(ofSize:14)
+ lbl.font = UIFont.systemFont(ofSize: 14)
lbl.textColor = .darkGray
lbl.textAlignment = .right
lbl.setContentHuggingPriority(.required, for: .horizontal)
@@ -905,12 +1033,12 @@ final class PopUpStoreRegisterViewController: BaseViewController {
private func makeRoundedTextView() -> UITextView {
let tv = UITextView()
- tv.font = UIFont.systemFont(ofSize:14)
+ 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.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
tv.isScrollEnabled = true
return tv
}
@@ -918,8 +1046,8 @@ final class PopUpStoreRegisterViewController: BaseViewController {
// MARK: - Padding
private extension UITextField {
- func setLeftPaddingPoints(_ amount: CGFloat){
- let paddingView = UIView(frame: CGRect(x:0, y:0, width:amount, height: frame.size.height))
+ func setLeftPaddingPoints(_ amount: CGFloat) {
+ let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: frame.size.height))
leftView = paddingView
leftViewMode = .always
}
@@ -957,18 +1085,51 @@ extension PopUpStoreRegisterViewController: UICollectionViewDataSource, UICollec
private extension PopUpStoreRegisterViewController {
/// 대표이미지를 단 하나만 허용 -> 누른 index만 isMain = true
func toggleMainImage(index: Int) {
- for i in 0.. Bool {
// (1) 팝업스토어 이름
@@ -1046,7 +1227,6 @@ private extension PopUpStoreRegisterViewController {
return false
}
-
// (4) 위도/경도
Logger.log(message: "latField.text = \(latField?.text ?? "nil")", category: .debug)
Logger.log(message: "lonField.text = \(lonField?.text ?? "nil")", category: .debug)
@@ -1122,7 +1302,6 @@ private extension PopUpStoreRegisterViewController {
}
}
-
// 폼 데이터 검증
private func validateFormData() -> Bool {
guard let name = nameField?.text,
@@ -1138,14 +1317,86 @@ private extension PopUpStoreRegisterViewController {
return true
}
private func updateStore(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) {
- // 이미지가 수정되었다면 먼저 이미지 업로드
- if !images.isEmpty {
- uploadImagesForUpdate(store)
+ // 기존에 저장된 이미지는 재업로드하지 않고 그대로 사용
+ // 새로 추가된 이미지만 업로드
+
+ // 새로 추가된 이미지만 필터링
+ let newImages = images.filter { image in
+ return !originalImageIds.keys.contains(image.filePath)
+ }
+
+ if !newImages.isEmpty {
+ // 새 이미지만 업로드
+ uploadNewImagesForUpdate(store, newImages: newImages)
} else {
- // 이미지 수정이 없다면 바로 스토어 정보 업데이트
+ // 새 이미지가 없으면 바로 스토어 정보 업데이트
updateStoreInfo(store, updatedImagePaths: nil)
}
}
+ private func uploadNewImagesForUpdate(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore, newImages: [ExtendedImage]) {
+ let uuid = UUID().uuidString
+ let updatedImages = newImages.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)
+
+ // 모든 이미지 경로 (기존 이미지 + 새 이미지)
+ var allPaths: [String] = []
+
+ // 삭제되지 않은 기존 이미지 경로 추가
+ let deletedIdSet = Set(self.deletedImageIds)
+ for (path, id) in self.originalImageIds {
+ if !deletedIdSet.contains(id) {
+ allPaths.append(path)
+ }
+ }
+
+ // 새로 업로드된 이미지 경로 추가
+ let newPaths = updatedImages.map { $0.filePath }
+ allPaths.append(contentsOf: newPaths)
+
+ // 메인 이미지 경로 결정
+ let mainImage: String
+ if let mainImg = self.images.first(where: { $0.isMain }) {
+ if self.originalImageIds.keys.contains(mainImg.filePath) {
+ // 기존 이미지가 메인
+ mainImage = mainImg.filePath
+ } else {
+ // 새 이미지가 메인인 경우, 새 경로 찾기
+ let idx = self.images.firstIndex(where: { $0.filePath == mainImg.filePath }) ?? 0
+ if idx < updatedImages.count {
+ mainImage = updatedImages[idx].filePath
+ } else {
+ mainImage = updatedImages.first?.filePath ?? ""
+ }
+ }
+ } else if !allPaths.isEmpty {
+ mainImage = allPaths[0]
+ } else {
+ mainImage = ""
+ }
+
+ self.updateStoreInfo(store, updatedImagePaths: allPaths, mainImage: mainImage)
+ },
+ onError: { [weak self] error in
+ Logger.log(message: "이미지 업로드 실패: \(error.localizedDescription)", category: .error)
+ self?.showErrorAlert(message: "이미지 업로드 실패: \(error.localizedDescription)")
+ }
+ )
+ .disposed(by: disposeBag)
+ }
+
private func uploadImagesForUpdate(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) {
let uuid = UUID().uuidString
let updatedImages = images.enumerated().map { index, image in
@@ -1174,7 +1425,7 @@ private extension PopUpStoreRegisterViewController {
.disposed(by: disposeBag)
}
- private func updateStoreInfo(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore, updatedImagePaths: [String]?) {
+ private func updateStoreInfo(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore, updatedImagePaths: [String]?, mainImage: String? = nil) {
guard let name = nameField?.text,
let address = addressField?.text,
let latitude = Double(latField?.text ?? ""),
@@ -1184,9 +1435,27 @@ private extension PopUpStoreRegisterViewController {
return
}
- // 업데이트할 이미지가 있다면 첫 번째 값을 대표 이미지로, 없으면 기존 스토어의 mainImageUrl 사용
- let mainImage = updatedImagePaths?.first ?? store.mainImageUrl
+ // 메인 이미지 결정
+ let finalMainImage: String
+ if let mainImagePath = mainImage {
+ finalMainImage = mainImagePath
+ } else if let firstImage = updatedImagePaths?.first {
+ finalMainImage = firstImage
+ } else {
+ // 이미지가 없는 경우 기존 메인 이미지 사용
+ finalMainImage = store.mainImageUrl
+ }
+
+ // 이미지 URL 목록 결정
+ let imageUrls: [String]
+ if let paths = updatedImagePaths, !paths.isEmpty {
+ imageUrls = paths
+ } else {
+ // 이미지 변동이 없을 경우
+ imageUrls = [store.mainImageUrl]
+ }
+ // 서버에 스토어 정보 업데이트 요청
let request = UpdatePopUpStoreRequestDTO(
popUpStore: .init(
id: store.id,
@@ -1196,9 +1465,9 @@ private extension PopUpStoreRegisterViewController {
address: address,
startDate: getFormattedDate(from: selectedStartDate),
endDate: getFormattedDate(from: selectedEndDate),
- mainImageUrl: mainImage,
- bannerYn: !mainImage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
- imageUrl: updatedImagePaths ?? [store.mainImageUrl],
+ mainImageUrl: finalMainImage,
+ bannerYn: !finalMainImage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
+ imageUrl: imageUrls,
startDateBeforeEndDate: true
),
location: .init(
@@ -1207,17 +1476,34 @@ private extension PopUpStoreRegisterViewController {
markerTitle: "마커 제목",
markerSnippet: "마커 설명"
),
- imagesToAdd: updatedImagePaths ?? [],
- imagesToDelete: [] // 필요한 경우 기존 이미지 삭제 로직 추가
- )
-
+ imagesToAdd: updatedImagePaths?.filter { !originalImageIds.keys.contains($0) } ?? [],
+ imagesToDelete: deletedImageIds
+ )
+ // 요청 데이터 로깅
+ Logger.log(message: "업데이트 요청 데이터: \(request)", category: .debug)
adminUseCase.updateStore(request: request)
.subscribe(
onNext: { [weak self] _ in
+ guard let self = self else { return }
Logger.log(message: "updateStore API 호출 성공", category: .info)
- self?.showSuccessAlert(isUpdate: true)
+
+ // 서버 응답이 성공하면 S3에서 이미지 삭제 수행
+ self.deleteImagesFromS3()
+
+ // 성공 시 저장된 삭제 이미지 정보 초기화
+ if let storeId = self.editingStore?.id {
+ UserDefaults.standard.removeObject(forKey: "deletedImageIds_\(storeId)")
+ UserDefaults.standard.removeObject(forKey: "deletedImagePaths_\(storeId)")
+ Logger.log(message: "삭제된 이미지 정보 영구 저장소에서 제거 완료", category: .debug)
+ }
+
+ // 메모리 내 삭제 목록도 초기화
+ self.deletedImageIds.removeAll()
+ self.deletedImagePaths.removeAll()
+
+ self.showSuccessAlert(isUpdate: true)
},
onError: { [weak self] error in
Logger.log(message: "updateStore API 호출 실패: \(error.localizedDescription)", category: .error)
@@ -1225,6 +1511,23 @@ private extension PopUpStoreRegisterViewController {
}
)
.disposed(by: disposeBag)
+
+ }
+ private func deleteImagesFromS3() {
+ // 삭제해야 할 이미지가 없으면 바로 리턴
+ guard !deletedImagePaths.isEmpty else { return }
+
+ // 모든 이미지 한 번에 삭제
+ presignedService.tryDelete(targetPaths: .init(objectKeyList: deletedImagePaths))
+ .subscribe(
+ onCompleted: {
+ Logger.log(message: "S3에서 모든 이미지 삭제 성공: \(self.deletedImagePaths.count)개", category: .info)
+ },
+ onError: { error in
+ Logger.log(message: "S3에서 이미지 삭제 실패: \(error.localizedDescription)", category: .error)
+ }
+ )
+ .disposed(by: disposeBag)
}
private func showSuccessAlert(isUpdate: Bool = false) {
@@ -1325,7 +1628,6 @@ private extension PopUpStoreRegisterViewController {
startDateBeforeEndDate: isValidDateOrder
)
-
adminUseCase.createStore(request: request)
.subscribe(
onNext: { [weak self] _ in
@@ -1366,7 +1668,6 @@ private extension PopUpStoreRegisterViewController {
}
}
-
private func createDateTime(date: Date?, time: Date?) -> Date? {
guard let date = date else { return nil }
@@ -1405,9 +1706,6 @@ private extension PopUpStoreRegisterViewController {
return formatter.string(from: date)
}
-
-
-
private func prepareDateTime() -> (startDate: String, endDate: String) {
// 시작일/시간 결합
let startDateTime = createDateTime(date: selectedStartDate, time: selectedStartTime)
@@ -1446,10 +1744,6 @@ private extension PopUpStoreRegisterViewController {
return start < end
}
-
-
-
-
private func showSuccessAlert() {
let alert = UIAlertController(
title: "등록 성공",
@@ -1499,7 +1793,7 @@ extension PopUpStoreRegisterViewController: UITextFieldDelegate {
return Observable.create { observer in
let geocoder = CLGeocoder()
let fullAddress = "\(address), Korea"
-
+
geocoder.geocodeAddressString(
fullAddress,
in: nil,
@@ -1511,7 +1805,7 @@ extension PopUpStoreRegisterViewController: UITextFieldDelegate {
observer.onCompleted()
return
}
-
+
if let location = placemarks?.first?.location {
observer.onNext(location)
} else {
@@ -1519,7 +1813,7 @@ extension PopUpStoreRegisterViewController: UITextFieldDelegate {
}
observer.onCompleted()
}
-
+
return Disposables.create()
}
}
@@ -1532,23 +1826,22 @@ extension PopUpStoreRegisterViewController: UITextFieldDelegate {
})
.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)
@@ -1556,5 +1849,11 @@ extension PopUpStoreRegisterViewController: UITextFieldDelegate {
}
}
}
-
+
+}
+extension PopUpStoreRegisterViewController: UITextViewDelegate {
+ func textViewDidChange(_ textView: UITextView) {
+ Logger.log(message: "설명 필드 값 변경: \(textView.text.count) 글자", category: .debug)
+ updateSaveButtonState()
+ }
}
diff --git a/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift b/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift
index 706addaf..5ab3d4c4 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminStoreCell.swift
@@ -1,6 +1,6 @@
-import UIKit
-import SnapKit
import RxSwift
+import SnapKit
+import UIKit
final class AdminStoreCell: UITableViewCell {
private let disposeBag = DisposeBag()
@@ -83,7 +83,7 @@ final class AdminStoreCell: UITableViewCell {
statusChip.text = "운영"
// mainImageUrl에서 baseURL 부분 제거
- let imagePath = store.mainImageUrl.replacingOccurrences(of: Secrets.popPoolS3BaseURL.rawValue, with: "")
+ let imagePath = store.mainImageUrl.replacingOccurrences(of: KeyPath.popPoolS3BaseURL, with: "")
Logger.log(message: "이미지 경로: \(imagePath)", category: .debug)
storeImageView.setPPImage(path: imagePath)
}
diff --git a/Poppool/Poppool/Presentation/Admin/AdminView.swift b/Poppool/Poppool/Presentation/Admin/AdminView.swift
index 3ec4149e..2d13cc17 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminView.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminView.swift
@@ -1,6 +1,6 @@
-import UIKit
import SnapKit
import Then
+import UIKit
final class AdminView: UIView {
@@ -18,7 +18,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/Poppool/Presentation/Admin/AdminViewController.swift
index 24e86807..5e5474f4 100644
--- a/Poppool/Poppool/Presentation/Admin/AdminViewController.swift
+++ b/Poppool/Poppool/Presentation/Admin/AdminViewController.swift
@@ -1,7 +1,7 @@
-import UIKit
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
+import UIKit
final class AdminViewController: BaseViewController, View {
@@ -182,21 +182,46 @@ final class AdminViewController: BaseViewController, View {
}
private func deleteStore(_ store: GetAdminPopUpStoreListResponseDTO.PopUpStore) {
- let imageService = PreSignedService()
-
- imageService.tryDelete(targetPaths: .init(objectKeyList: [store.mainImageUrl]))
- .andThen(adminUseCase.deleteStore(id: store.id))
+ // 먼저 스토어 상세 정보를 가져와 모든 이미지 URL을 확인
+ 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.imageList.map { $0.imageUrl }
+ allImageUrls.append(contentsOf: otherImageUrls)
+
+ allImageUrls = Array(Set(allImageUrls))
+
+ Logger.log(message: "삭제할 이미지: \(allImageUrls.count)개", category: .debug)
+
+ let imageService = PreSignedService()
+ imageService.tryDelete(targetPaths: .init(objectKeyList: allImageUrls))
+ .andThen(self.adminUseCase.deleteStore(id: store.id))
+ .observe(on: MainScheduler.instance)
+ .subscribe(
+ onNext: { [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(
@@ -238,11 +263,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)
diff --git a/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift b/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift
index 3e251d01..b1de4138 100644
--- a/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift
+++ b/Poppool/Poppool/Presentation/Admin/Common/DateTimePickerManager.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class DateTimePickerManager {
diff --git a/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift b/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift
index 5924f809..329934c5 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/DTO/AdminResponseDTO.swift
@@ -13,7 +13,6 @@ struct GetAdminPopUpStoreListResponseDTO: Decodable {
let mainImageUrl: String
}
-
}
// MARK: - Store Detail Response
diff --git a/Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift b/Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift
index 8be73a2b..6d14302a 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/DTO/GetAdminPopUpStoreListResponseDTO.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/DTO/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/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift
index e9da01f6..3b5c7107 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapAPIEndpoint.swift
@@ -5,8 +5,8 @@
// Created by 김기현 on 12/4/24.
//
-import Foundation
import Alamofire
+import Foundation
struct MapAPIEndpoint {
/// 뷰 바운즈 내에 있는 팝업 스토어 정보를 조회
@@ -26,7 +26,7 @@ struct MapAPIEndpoint {
)
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/locations/popup-stores",
method: .get,
queryParameters: params
@@ -44,7 +44,7 @@ struct MapAPIEndpoint {
)
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/locations/search",
method: .get,
queryParameters: params
@@ -84,4 +84,3 @@ struct SearchQueryDTO: Encodable {
let query: String
let categories: [Int64]?
}
-
diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift
index bcc448f4..2be1694e 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStore.swift
@@ -1,11 +1,5 @@
-//
-// MapPopUpStore.swift
-// Poppool
-//
-// Created by 김기현 on 12/3/24.
-//
import Foundation
-import CoreLocation
+import NMapsMap
struct MapPopUpStore: Equatable {
let id: Int64
@@ -19,35 +13,30 @@ struct MapPopUpStore: Equatable {
let markerId: Int64
let markerTitle: String
let markerSnippet: String
- let mainImageUrl: String? // 이미지 URL 추가
-
+ let mainImageUrl: String?
- var coordinate: CLLocationCoordinate2D {
- CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
+ var nmgCoordinate: NMGLatLng {
+ NMGLatLng(lat: latitude, lng: longitude)
}
-}
-
- extension MapPopUpStore {
- func toMarkerInput() -> MapMarker.Input {
- return MapMarker.Input(
- isSelected: false,
- isCluster: false,
- regionName: self.markerTitle, // 또는 name이나 다른 적절한 필드
- count: 0
- )
- }
-
+ func toMarkerInput() -> MapMarker.Input {
+ return MapMarker.Input(
+ isSelected: false,
+ isCluster: false,
+ regionName: self.markerTitle,
+ count: 0
+ )
+ }
func toStoreItem() -> StoreItem {
return StoreItem(
id: id,
- thumbnailURL: mainImageUrl ?? "", // 이미지 URL 매핑
+ thumbnailURL: mainImageUrl ?? "",
category: category,
title: name,
location: address,
dateRange: "\(startDate) ~ \(endDate)",
- isBookmarked: false // 기본값
+ isBookmarked: false
)
}
}
diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift
index 1db431dd..cd7bb5de 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/MapPopUpStoreDTO.swift
@@ -15,7 +15,6 @@ struct MapPopUpStoreDTO: Codable {
let mainImageUrl: String?
let bookmarkYn: Bool?
- // toDomain() 메서드 추가
func toDomain() -> MapPopUpStore {
return MapPopUpStore(
id: id,
diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift
index 53164882..a10bffc5 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/Repository/MapRepository.swift
@@ -33,7 +33,6 @@ class DefaultMapRepository: MapRepository {
self.provider = provider
}
-
func fetchStoresInBounds(
northEastLat: Double,
northEastLon: Double,
@@ -49,7 +48,7 @@ class DefaultMapRepository: MapRepository {
southWestLon: southWestLon,
categories: categories
),
- interceptor: TokenInterceptor() // ← 토큰 누락 해결
+ interceptor: TokenInterceptor()
)
.map { $0.popUpStoreList }
}
@@ -63,12 +62,11 @@ class DefaultMapRepository: MapRepository {
query: query,
categories: categories
),
- interceptor: TokenInterceptor() // ← 토큰 누락 해결
+ interceptor: TokenInterceptor()
)
.map { $0.popUpStoreList }
}
-
func fetchCategories() -> Observable<[Category]> {
Logger.log(message: "카테고리 매핑 요청을 시작합니다.", category: .network)
diff --git a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift
index 1d68c933..660ee9d0 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/MapDomain/UseCase/MapUseCase.swift
@@ -36,7 +36,7 @@ class DefaultMapUseCase: MapUseCase {
northEastLon: Double,
southWestLat: Double,
southWestLon: Double,
- categories: [Int64]
+ categories: [Int64]
) -> Observable<[MapPopUpStore]> {
return repository.fetchStoresInBounds(
@@ -49,7 +49,6 @@ class DefaultMapUseCase: MapUseCase {
.map { $0.map { $0.toDomain() } }
}
-
func searchStores(
query: String,
categories: [Int64]
diff --git a/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift b/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift
index cfb9baf3..b5ba885d 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/Remote/AdminAPIEndpoint.swift
@@ -14,7 +14,7 @@ struct AdminAPIEndpoint {
size: size
)
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/popup-stores/list",
method: .get,
queryParameters: params
@@ -26,7 +26,7 @@ struct AdminAPIEndpoint {
id: Int64
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/popup-stores",
method: .get,
queryParameters: ["popUpStoreId": id]
@@ -38,7 +38,7 @@ struct AdminAPIEndpoint {
request: CreatePopUpStoreRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/popup-stores",
method: .post,
bodyParameters: request
@@ -50,7 +50,7 @@ struct AdminAPIEndpoint {
request: UpdatePopUpStoreRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/popup-stores",
method: .put,
bodyParameters: request
@@ -62,7 +62,7 @@ struct AdminAPIEndpoint {
id: Int64
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/popup-stores",
method: .delete,
queryParameters: ["popUpStoreId": id]
@@ -74,7 +74,7 @@ struct AdminAPIEndpoint {
request: CreateNoticeRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/notice",
method: .post,
bodyParameters: request
@@ -86,7 +86,7 @@ struct AdminAPIEndpoint {
request: UpdateNoticeRequestDTO
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/notice/\(id)",
method: .put,
bodyParameters: request
@@ -97,7 +97,7 @@ struct AdminAPIEndpoint {
id: Int64
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/admin/notice/\(id)",
method: .delete
)
diff --git a/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift b/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift
index c11757bb..7d0bd1e6 100644
--- a/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift
+++ b/Poppool/Poppool/Presentation/Admin/Data/Repository/AdminRepository.swift
@@ -1,7 +1,7 @@
+import Alamofire
import Foundation
import RxSwift
import UIKit
-import Alamofire
protocol AdminRepository {
func fetchStoreList(query: String?, page: Int, size: Int) -> Observable
@@ -54,7 +54,6 @@ final class DefaultAdminRepository: AdminRepository {
}
}
-
func createStore(request: CreatePopUpStoreRequestDTO) -> Observable {
Logger.log(message: "createStore API 호출 시작", category: .info)
let endpoint = AdminAPIEndpoint.createStore(request: request)
@@ -84,8 +83,6 @@ final class DefaultAdminRepository: AdminRepository {
)
}
-
-
func updateStore(request: UpdatePopUpStoreRequestDTO) -> Observable {
let endpoint = AdminAPIEndpoint.updateStore(request: request)
@@ -125,12 +122,11 @@ final class DefaultAdminRepository: AdminRepository {
})
}
-
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()))
+ .andThen(Observable.just(EmptyResponse()))
.do(
onNext: { _ in
Logger.log(message: "deleteStore API 호출 성공", category: .info)
@@ -140,7 +136,6 @@ final class DefaultAdminRepository: AdminRepository {
}
)
}
-
// MARK: - Notice Methods
func createNotice(request: CreateNoticeRequestDTO) -> Observable {
diff --git a/Poppool/Poppool/Presentation/Admin/ImageCell.swift b/Poppool/Poppool/Presentation/Admin/ImageCell.swift
index 98f6fbf1..04704d15 100644
--- a/Poppool/Poppool/Presentation/Admin/ImageCell.swift
+++ b/Poppool/Poppool/Presentation/Admin/ImageCell.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class ImageCell: UICollectionViewCell {
static let identifier = "ImageCell"
diff --git a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift
index 99d29f89..6e355394 100644
--- a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift
+++ b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterReactor.swift
@@ -5,12 +5,12 @@
//// Created by 김기현 on 1/14/25.
////
//
-//import Foundation
-//import ReactorKit
-//import RxSwift
-//import UIKit
+// import Foundation
+// import ReactorKit
+// import RxSwift
+// import UIKit
//
-//final class PopUpStoreRegisterReactor: Reactor {
+// final class PopUpStoreRegisterReactor: Reactor {
//
// // MARK: - Action
// enum Action {
@@ -303,4 +303,4 @@
// default: return 100
// }
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift
index 55c20d1c..6255da1a 100644
--- a/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift
+++ b/Poppool/Poppool/Presentation/Admin/PopUpStoreRegisterView.swift
@@ -5,11 +5,11 @@
//// Created by 김기현 on 1/14/25.
////
//
-//import UIKit
-//import SnapKit
-//import Then
+// import UIKit
+// import SnapKit
+// import Then
//
-//final class PopUpStoreRegisterView: UIView {
+// final class PopUpStoreRegisterView: UIView {
//
// // MARK: - Callbacks (Closure)
// /// "이미지 추가" 버튼 탭
@@ -320,10 +320,10 @@
//
// return row
// }
-//}
+// }
//
//// MARK: - UICollectionViewDataSource
-//extension PopUpStoreRegisterView: UICollectionViewDataSource {
+// extension PopUpStoreRegisterView: UICollectionViewDataSource {
// func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// return images.count
// }
@@ -347,15 +347,15 @@
// }
// return cell
// }
-//}
+// }
//
//// MARK: - UICollectionViewDelegateFlowLayout
-//extension PopUpStoreRegisterView: UICollectionViewDelegateFlowLayout {
+// extension PopUpStoreRegisterView: UICollectionViewDelegateFlowLayout {
// // 혹시 셀 사이즈/간격을 동적으로 조정하고 싶다면 여기서
-//}
+// }
//
//// MARK: - PopUpImageCell (같은 파일)
-//final class PopUpImageCell: UICollectionViewCell {
+// final class PopUpImageCell: UICollectionViewCell {
// static let identifier = "PopUpImageCell"
//
// // 콜백
@@ -421,4 +421,4 @@
// thumbImageView.image = item.image
// mainCheckButton.backgroundColor = item.isMain ? .systemRed : .gray
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Components/PPButton.swift b/Poppool/Poppool/Presentation/Components/PPButton.swift
index 87063272..5d99f435 100644
--- a/Poppool/Poppool/Presentation/Components/PPButton.swift
+++ b/Poppool/Poppool/Presentation/Components/PPButton.swift
@@ -8,14 +8,14 @@
import UIKit
class PPButton: UIButton {
-
+
enum ButtonStyle {
case primary
case secondary
case tertiary
case kakao
case apple
-
+
var backgroundColor: UIColor {
switch self {
case .primary:
@@ -30,7 +30,7 @@ class PPButton: UIButton {
return .g900
}
}
-
+
var textColor: UIColor {
switch self {
case .primary:
@@ -45,7 +45,7 @@ class PPButton: UIButton {
return .w100
}
}
-
+
var disabledBackgroundColor: UIColor {
switch self {
case .primary:
@@ -56,7 +56,7 @@ class PPButton: UIButton {
return .blu500
}
}
-
+
var disabledTextColor: UIColor {
switch self {
case .primary:
@@ -68,35 +68,34 @@ class PPButton: UIButton {
}
}
}
-
-
+
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: 색상
@@ -109,7 +108,7 @@ class PPButton: UIButton {
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
-
+
self.setBackgroundImage(backgroundImage, for: state)
}
}
diff --git a/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift b/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift
index a033274a..4e1bd069 100644
--- a/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift
+++ b/Poppool/Poppool/Presentation/Components/PPCancelHeaderView.swift
@@ -10,7 +10,7 @@ import UIKit
import SnapKit
final class PPCancelHeaderView: UIView {
-
+
// MARK: - Components
let backButton: UIButton = {
let button = UIButton(type: .system)
@@ -18,21 +18,21 @@ final class PPCancelHeaderView: UIView {
button.tintColor = .black
return button
}()
-
+
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() {
super.init(frame: .zero)
setUpConstraints()
}
-
+
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -40,7 +40,7 @@ final class PPCancelHeaderView: UIView {
// MARK: - SetUp
private extension PPCancelHeaderView {
-
+
func setUpConstraints() {
self.addSubview(backButton)
backButton.snp.makeConstraints { make in
@@ -48,7 +48,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/Poppool/Presentation/Components/PPLabel.swift
index 41030705..614638ac 100644
--- a/Poppool/Poppool/Presentation/Components/PPLabel.swift
+++ b/Poppool/Poppool/Presentation/Components/PPLabel.swift
@@ -8,15 +8,15 @@
import UIKit
class PPLabel: UILabel {
-
+
init(
- style: UIFont.FontStyle,
+ 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 +24,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/Poppool/Presentation/Components/PPPicker.swift
index e5164c9c..acaa9456 100644
--- a/Poppool/Poppool/Presentation/Components/PPPicker.swift
+++ b/Poppool/Poppool/Presentation/Components/PPPicker.swift
@@ -5,13 +5,13 @@
// Created by SeoJunYoung on 7/3/24.
//
+import RxCocoa
+import RxSwift
import SnapKit
import UIKit
-import RxSwift
-import RxCocoa
final class PPPicker: UIView {
-
+
// MARK: - Components
private let components: [String]
let pickerView = UIPickerView()
@@ -24,7 +24,7 @@ final class PPPicker: UIView {
private let disposeBag = DisposeBag()
/// 항목 선택 이벤트를 전달하는 PublishSubject입니다.
let itemSelectObserver: PublishSubject = .init()
-
+
// MARK: - init
/// PickerCPNT init
/// - Parameter components: UIPickerView에 표시할 문자열 배열입니다.
@@ -35,7 +35,7 @@ final class PPPicker: UIView {
setUpConstraints()
bind()
}
-
+
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@@ -49,7 +49,7 @@ extension PPPicker {
pickerView.delegate = self
pickerView.dataSource = self
}
-
+
func setUpConstraints() {
self.addSubview(selectView)
self.addSubview(pickerView)
@@ -62,7 +62,7 @@ extension PPPicker {
make.center.equalToSuperview()
}
}
-
+
func bind() {
pickerView.rx.itemSelected
.withUnretained(self)
@@ -75,7 +75,7 @@ extension PPPicker {
// MARK: - Methods
extension PPPicker {
-
+
/// 지정된 인덱스로 UIPickerView를 설정
/// - Parameter index: 설정할 인덱스 값
func setIndex(index: Int) {
@@ -88,19 +88,19 @@ extension PPPicker: UIPickerViewDelegate, UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
-
+
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 {
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
@@ -110,7 +110,7 @@ extension PPPicker: UIPickerViewDelegate, UIPickerViewDataSource {
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
return 48
}
-
+
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.reloadAllComponents()
}
diff --git a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift b/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift
index 4908fec6..ec709b83 100644
--- a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift
+++ b/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressIndicator.swift
@@ -6,18 +6,17 @@
//
import Foundation
-import UIKit
-import SnapKit
-import RxSwift
import RxCocoa
-
+import RxSwift
+import SnapKit
+import UIKit
final class PPProgressIndicator: UIStackView {
-
+
// MARK: - Properties
private var progressViews: [PPProgressView]
private var progressIndex: Int
-
+
// MARK: - init
/// 전체 단계 수와 시작 지점을 기반으로 CMPTProgressIndicator를 초기화
/// - Parameters:
@@ -32,7 +31,7 @@ final class PPProgressIndicator: UIStackView {
setUp()
setUpConstraints()
}
-
+
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -40,14 +39,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,7 +57,7 @@ private extension PPProgressIndicator {
// MARK: - Methods
extension PPProgressIndicator {
-
+
/// 진행 인디케이터를 한 단계 앞으로 이동
func increaseIndicator() {
if progressIndex < progressViews.count {
@@ -67,7 +66,7 @@ extension PPProgressIndicator {
progressViews[progressIndex - 1].fillAnimation(option: .fromLeft)
}
}
-
+
/// 진행 인디케이터를 한 단계 뒤로 이동
func decreaseIndicator() {
if progressIndex - 1 > 0 {
diff --git a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift b/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift
index 46cf39cc..60a3137a 100644
--- a/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift
+++ b/Poppool/Poppool/Presentation/Components/PPProgressIndicator/PPProgressView.swift
@@ -6,33 +6,32 @@
//
import Foundation
-import UIKit
import SnapKit
+import UIKit
final class PPProgressView: UIView {
-
-
+
/// CMTPProgressView Animation Type
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) {
@@ -41,7 +40,7 @@ final class PPProgressView: UIView {
setUpConstraints()
setUp()
}
-
+
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -49,17 +48,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 +74,7 @@ private extension PPProgressView {
}
extension PPProgressView {
-
+
/// 선택된 뷰를 채우는 애니메이션
/// - Parameter option: 애니메이션 시작 방향
func fillAnimation(option: ProgressFillAnimation) {
@@ -89,7 +88,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 +103,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 +112,7 @@ extension PPProgressView {
})
}
}
-
+
/// 선택된 뷰를 사라지게 하는 애니메이션
/// - Parameter option: 애니메이션 시작 방향
func disappearAnimation(option: ProgressFillAnimation) {
diff --git a/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift b/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift
index f56b0aae..f8589886 100644
--- a/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift
+++ b/Poppool/Poppool/Presentation/Components/PPReturnHeaderView.swift
@@ -5,11 +5,11 @@
// Created by Porori on 11/27/24.
//
-import UIKit
import SnapKit
+import UIKit
final class PPReturnHeaderView: UIView {
-
+
// MARK: - Components
let backButton: UIButton = {
let button = UIButton(type: .system)
@@ -17,24 +17,24 @@ final class PPReturnHeaderView: UIView {
button.tintColor = .black
return button
}()
-
+
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() {
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 +42,7 @@ final class PPReturnHeaderView: UIView {
// MARK: - SetUp
private extension PPReturnHeaderView {
-
+
func setUpConstraints() {
self.addSubview(backButton)
backButton.snp.makeConstraints { make in
@@ -50,7 +50,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/Poppool/Presentation/Components/PPSegmentedControl.swift b/Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift
index 7699c7fe..e9cde785 100644
--- a/Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift
+++ b/Poppool/Poppool/Presentation/Components/PPSegmentedControl.swift
@@ -5,11 +5,11 @@
// Created by SeoJunYoung on 6/27/24.
//
-import UIKit
import SnapKit
+import UIKit
final class PPSegmentedControl: UISegmentedControl {
-
+
/// 세그먼트 컨트롤 타입
enum SegmentedControlType {
case radio
@@ -31,24 +31,24 @@ final class PPSegmentedControl: UISegmentedControl {
bottomLineView.addSubview(view)
return view
}()
-
+
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() {
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,7 +61,7 @@ final class PPSegmentedControl: UISegmentedControl {
// MARK: - SetUp
private extension PPSegmentedControl {
-
+
/// 세그먼트 설정 메서드
/// - Parameters:
/// - type: 세그먼트 컨트롤 타입
@@ -85,8 +85,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 +100,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,11 +118,11 @@ 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: 폰트 색상
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/Date?+.swift b/Poppool/Poppool/Presentation/Extension/Date?+.swift
index f8117e7e..550cd427 100644
--- a/Poppool/Poppool/Presentation/Extension/Date?+.swift
+++ b/Poppool/Poppool/Presentation/Extension/Date?+.swift
@@ -19,7 +19,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 +28,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/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/Reactive+.swift b/Poppool/Poppool/Presentation/Extension/Reactive+.swift
index b3c44553..74665b05 100644
--- a/Poppool/Poppool/Presentation/Extension/Reactive+.swift
+++ b/Poppool/Poppool/Presentation/Extension/Reactive+.swift
@@ -11,27 +11,27 @@ import RxCocoa
import RxSwift
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/Poppool/Presentation/Extension/String?+.swift
index c1c5a916..5e7c7831 100644
--- a/Poppool/Poppool/Presentation/Extension/String?+.swift
+++ b/Poppool/Poppool/Presentation/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 {
/// 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()
diff --git a/Poppool/Poppool/Presentation/Extension/UIColor+.swift b/Poppool/Poppool/Presentation/Extension/UIColor+.swift
index 5de4edb1..1ea48e23 100644
--- a/Poppool/Poppool/Presentation/Extension/UIColor+.swift
+++ b/Poppool/Poppool/Presentation/Extension/UIColor+.swift
@@ -8,7 +8,7 @@
import UIKit
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/Poppool/Presentation/Extension/UIFont+.swift b/Poppool/Poppool/Presentation/Extension/UIFont+.swift
index 4d131df8..75bdb25b 100644
--- a/Poppool/Poppool/Presentation/Extension/UIFont+.swift
+++ b/Poppool/Poppool/Presentation/Extension/UIFont+.swift
@@ -1,23 +1,15 @@
-//
-// UIFont+.swift
-// PopPool
-//
-// Created by Porori on 6/20/24.
-//
-
import Foundation
import UIKit
extension UIFont {
-
- static func KorFont(style: FontStyle, size: CGFloat) -> 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? {
+ 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"
@@ -25,4 +17,3 @@ extension UIFont {
case light = "-Light"
}
}
-
diff --git a/Poppool/Poppool/Presentation/Extension/UIImage+.swift b/Poppool/Poppool/Presentation/Extension/UIImage+.swift
index be80b2a2..dfeb7e32 100644
--- a/Poppool/Poppool/Presentation/Extension/UIImage+.swift
+++ b/Poppool/Poppool/Presentation/Extension/UIImage+.swift
@@ -22,7 +22,7 @@ extension UIImage {
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/Poppool/Presentation/Extension/UIImageView+.swift b/Poppool/Poppool/Presentation/Extension/UIImageView+.swift
index 8040bddb..bd1738d3 100644
--- a/Poppool/Poppool/Presentation/Extension/UIImageView+.swift
+++ b/Poppool/Poppool/Presentation/Extension/UIImageView+.swift
@@ -1,50 +1,34 @@
-//
-// 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
+ let imageURLString = KeyPath.popPoolS3BaseURL + 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
+ ImageLoader.shared.loadImage(with: cenvertimageURL, defaultImage: UIImage(named: "image_default"), imageQuality: .origin) { [weak self] image in
+ DispatchQueue.main.async {
+ self?.image = image
}
}
}
}
-
+
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
+ let imageURLString = KeyPath.popPoolS3BaseURL + 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
+ ImageLoader.shared.loadImage(with: cenvertimageURL, defaultImage: UIImage(named: "image_default"), imageQuality: .origin) { [weak self] image in
+ DispatchQueue.main.async {
+ completion()
+ self?.image = image
}
}
}
diff --git a/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift b/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift
index 306392bb..ad3b265f 100644
--- a/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/ClusteringManager.swift
@@ -1,23 +1,22 @@
-import CoreLocation
-import UIKit
+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 +34,40 @@ class ClusteringManager {
}
}
- // 수정: 항상 구 단위 클러스터링만 사용하도록 변경
func clusterStores(_ stores: [MapPopUpStore], at zoomLevel: Float) -> [ClusterMarkerData] {
- // 줌 레벨 무시하고 항상 구 단위 클러스터링 실행
- return clusterByDistrictOnly(stores)
- }
-
- // 새로운 함수: 모든 스토어를 구 단위로만 클러스터링
- private func clusterByDistrictOnly(_ stores: [MapPopUpStore]) -> [ClusterMarkerData] {
- var clusters: [String: MutableCluster] = [:]
-
- // 서울 구 클러스터 초기화
- 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)
- }
+ let level = MapZoomLevel.getLevel(from: zoomLevel)
- // 각 스토어를 적절한 클러스터에 할당
- 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 +85,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 +123,18 @@ class ClusteringManager {
let combined = Array(seoulClusters.values) + Array(gyeonggiClusters.values) + Array(otherClusters.values)
let filtered = combined.filter { $0.storeCount > 0 }
+
+ Logger.log(message: "구 단위 클러스터 생성 결과: \(filtered.count)개", category: .debug)
+ for cluster in filtered {
+ Logger.log(message: "- \(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 +149,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 +157,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 +165,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 +186,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/Poppool/Presentation/Map/Common/ClusteringModels.swift
index 3d5a1c94..b0f80b7d 100644
--- a/Poppool/Poppool/Presentation/Map/Common/ClusteringModels.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/ClusteringModels.swift
@@ -1,4 +1,4 @@
-import CoreLocation
+import NMapsMap
enum MapZoomLevel {
case country
@@ -12,7 +12,7 @@ enum MapZoomLevel {
return .country
case 7..<10:
return .city
- case 10..<12:
+ case 10..<11:
return .district
default:
return .detailed
@@ -20,11 +20,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/Map/Common/FilterType.swift b/Poppool/Poppool/Presentation/Map/Common/FilterType.swift
index 354d319a..b19e7c64 100644
--- a/Poppool/Poppool/Presentation/Map/Common/FilterType.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/FilterType.swift
@@ -1,5 +1,3 @@
-
-
import Foundation
import UIKit
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/LocationPermissionBottomSheet.swift b/Poppool/Poppool/Presentation/Map/Common/LocationPermissionBottomSheet.swift
index 88dcc961..1b5d50da 100644
--- a/Poppool/Poppool/Presentation/Map/Common/LocationPermissionBottomSheet.swift
+++ b/Poppool/Poppool/Presentation/Map/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/Poppool/Presentation/Map/Common/MapFilterChips.swift
index 7745525c..8006ce31 100644
--- a/Poppool/Poppool/Presentation/Map/Common/MapFilterChips.swift
+++ b/Poppool/Poppool/Presentation/Map/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/MapPopupCardView/MapPopupCarouselView.swift b/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift
index cd217101..e6779117 100644
--- a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/MapPopupCarouselView.swift
@@ -1,6 +1,6 @@
-import UIKit
-import SnapKit
import FloatingPanel
+import SnapKit
+import UIKit
final class MapPopupCarouselView: UICollectionView {
// 스크롤 멈췄을 때의 콜백
@@ -13,11 +13,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 +142,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/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift
index 4d1ee89d..5d2f8f11 100644
--- a/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/MapPopupCardView/PopupCardCell.swift
@@ -1,6 +1,6 @@
import UIKit
+
import SnapKit
-import Kingfisher
final class PopupCardCell: UICollectionViewCell {
static let identifier = "PopupCardCell"
@@ -22,8 +22,6 @@ final class PopupCardCell: UICollectionViewCell {
configureUI()
}
-
-
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -86,7 +84,6 @@ final class PopupCardCell: UICollectionViewCell {
}
}
-
private func configureUI() {
contentView.backgroundColor = UIColor.white
categoryLabel.textColor = .systemBlue
diff --git a/Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift b/Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift
index ff6f6557..d76dafe1 100644
--- a/Poppool/Poppool/Presentation/Map/Common/MapUtilities.swift
+++ b/Poppool/Poppool/Presentation/Map/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/Poppool/Presentation/Map/Common/NMFMapViewDelegateProxy.swift b/Poppool/Poppool/Presentation/Map/Common/NMFMapViewDelegateProxy.swift
new file mode 100644
index 00000000..71e6a303
--- /dev/null
+++ b/Poppool/Poppool/Presentation/Map/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/Poppool/Presentation/Map/Common/RegionDefinitions.swift b/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift
index 4f46b43e..92e35ab1 100644
--- a/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift
+++ b/Poppool/Poppool/Presentation/Map/Common/RegionDefinitions.swift
@@ -1,23 +1,23 @@
-import CoreLocation
+import NMapsMap
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)
+ 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 {
@@ -26,241 +26,239 @@ enum RegionType {
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
- )
- ]
+ 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: 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 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: 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 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: 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 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
+ static var allClusters: [RegionCluster] {
+ seoulClusters + gyeonggiClusters + metropolitanClusters + provinceClusters
+ }
}
}
diff --git a/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift b/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift
index 611d0f3a..82fa4b7d 100644
--- a/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift
+++ b/Poppool/Poppool/Presentation/Map/CustomClusterRenderer.swift
@@ -1,6 +1,6 @@
-//import GoogleMaps
+// import GoogleMaps
//
-//class CustomClusterRenderer: GMUDefaultClusterRenderer {
+// class CustomClusterRenderer: GMUDefaultClusterRenderer {
// override func willRenderMarker(_ marker: GMSMarker) {
// super.willRenderMarker(marker)
// // 클러스터일 경우 처리
@@ -12,4 +12,4 @@
// marker.iconView = customView
// }
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift
index 3c673979..45161c19 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonBackgroundView.swift
+++ b/Poppool/Poppool/Presentation/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)
@@ -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().priority(.high)
+ }
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().priority(.high)
}
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
}
}
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift
index 3919e1bf..964e5995 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/BalloonChipCell.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class BalloonChipCell: UICollectionViewCell {
static let identifier = "BalloonChipCell"
@@ -8,9 +8,10 @@ final class BalloonChipCell: UICollectionViewCell {
let button = PPButton(
style: .secondary,
text: "",
- font: .KorFont(style: .medium, size: 11),
+ font: .korFont(style: .medium, size: 11),
cornerRadius: 15
)
+
button.titleLabel?.lineBreakMode = .byTruncatingTail
button.titleLabel?.adjustsFontSizeToFitWidth = false
return button
@@ -44,8 +45,7 @@ final class BalloonChipCell: UICollectionViewCell {
button.setBackgroundColor(.blu500, for: .normal)
button.setTitleColor(.white, for: .normal)
button.layer.borderWidth = 0
- button.titleLabel?.font = .KorFont(style: .bold, size: 11)
-
+ button.titleLabel?.font = .korFont(style: .bold, size: 11)
} else {
button.setImage(nil, for: .normal)
@@ -56,7 +56,7 @@ final class BalloonChipCell: UICollectionViewCell {
button.setTitleColor(.g400, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.g200.cgColor
- button.titleLabel?.font = .KorFont(style: .medium, size: 11)
+ button.titleLabel?.font = .korFont(style: .medium, size: 11)
}
}
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift
index b131a1b2..06fd3d99 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/CategoryFilterView.swift
@@ -1,7 +1,7 @@
-//import UIKit
-//import SnapKit
+// import UIKit
+// import SnapKit
//
-//final class CategoryFilterView: UIView {
+// final class CategoryFilterView: UIView {
// private let stackView = UIStackView()
// private let categories = ["게임", "라이프스타일", "엔터테인먼트", "패션", "음식/요리", "키즈"]
//
@@ -32,4 +32,4 @@
// stackView.addArrangedSubview(chip)
// }
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift
index 190ad281..ed37ee77 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetReactor.swift
+++ b/Poppool/Poppool/Presentation/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/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift
index 3301184c..b7414a4b 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetView.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class FilterBottomSheetView: UIView {
// MARK: - UI Components
@@ -9,15 +9,9 @@ final class FilterBottomSheetView: UIView {
view.layer.cornerRadius = 20
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
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
@@ -32,8 +26,7 @@ final class FilterBottomSheetView: UIView {
}()
let segmentedControl: PPSegmentedControl = {
- let control = PPSegmentedControl(type: .tab, segments: ["지역", "카테고리"], selectedSegmentIndex: 0)
- return control
+ return PPSegmentedControl(type: .tab, segments: ["지역", "카테고리"], selectedSegmentIndex: 0)
}()
let locationScrollView: UIScrollView = {
@@ -41,11 +34,12 @@ final class FilterBottomSheetView: UIView {
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)
@@ -81,21 +75,16 @@ final class FilterBottomSheetView: UIView {
return collectionView
}()
-
let balloonBackgroundView = BalloonBackgroundView()
let resetButton: PPButton = {
- let button = PPButton(style: .secondary, text: "초기화")
- return button
+ return PPButton(style: .secondary, text: "초기화")
}()
-
let saveButton: PPButton = {
- let button = PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장")
- return button
+ return PPButton(style: .primary, text: "옵션저장", disabledText: "옵션저장")
}()
-
private let buttonStack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
@@ -105,14 +94,13 @@ final class FilterBottomSheetView: UIView {
}()
let filterChipsView: FilterChipsView = {
- let view = FilterChipsView()
- return view
+ return FilterChipsView()
}()
private var balloonHeightConstraint: Constraint?
// MARK: - Initialization
-
+
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
@@ -127,52 +115,30 @@ 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
@@ -186,13 +152,11 @@ final class FilterBottomSheetView: UIView {
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(16)
make.leading.trailing.equalToSuperview()
}
- // 5. locationScrollView 및 contentView
locationScrollView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(20)
make.leading.trailing.equalToSuperview()
@@ -204,28 +168,24 @@ final class FilterBottomSheetView: UIView {
make.height.equalToSuperview()
}
- // 6. categoryCollectionView
categoryCollectionView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview()
-// categoryHeightConstraint = make.height.equalTo(160).constraint
+ categoryHeightConstraint = make.height.equalTo(160).constraint
}
- // 7. balloonBackgroundView
balloonBackgroundView.snp.makeConstraints { make in
make.top.equalTo(locationScrollView.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
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)
}
- // 9. buttonStack
buttonStack.snp.makeConstraints { make in
make.top.equalTo(filterChipsView.snp.bottom).offset(24)
make.leading.trailing.equalToSuperview().inset(16)
@@ -236,7 +196,7 @@ final class FilterBottomSheetView: UIView {
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 +204,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 +233,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 +266,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,31 +301,11 @@ 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: .medium, size: 13),
cornerRadius: 18
)
button.setBackgroundColor(.w100, for: .normal)
@@ -363,44 +331,41 @@ final class FilterBottomSheetView: UIView {
button.setBackgroundColor(.blu500, for: .normal)
button.setTitleColor(.w100, for: .normal)
button.layer.borderWidth = 0
- button.titleLabel?.font = .KorFont(style: .bold, size: 13)
+ 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.titleLabel?.font = .korFont(style: .medium, size: 13)
button.layer.borderWidth = 1
}
}
}
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)
-
- let buttonCenterX = buttonFrameInWindow.midX
-
- let relativeX = buttonCenterX - balloonFrameInWindow.minX
+ DispatchQueue.main.async {
+ guard let window = button.window else { return }
- let position = relativeX / balloonBackgroundView.bounds.width
+ let buttonFrameInWindow = button.convert(button.bounds, to: window)
+ let balloonFrameInWindow = self.balloonBackgroundView.convert(self.balloonBackgroundView.bounds, to: window)
- let minPosition: CGFloat = 0.1 // 왼쪽 여백
- let maxPosition: CGFloat = 0.9 // 오른쪽 여백
- let clampedPosition = min(maxPosition, max(minPosition, position))
+ 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))
- balloonBackgroundView.arrowPosition = clampedPosition
- balloonBackgroundView.setNeedsDisplay()
+ self.balloonBackgroundView.arrowPosition = clampedPosition
+ self.balloonBackgroundView.setNeedsDisplay()
+ }
}
private func updateBalloonPositionAccurately(for button: PPButton) {
@@ -409,8 +374,18 @@ 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] = []
@@ -424,18 +399,20 @@ extension FilterBottomSheetView {
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/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetViewController.swift
index c398fc99..b5261aa8 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterBottomSheetViewController.swift
+++ b/Poppool/Poppool/Presentation/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/Poppool/Presentation/Map/FillterSheetView/FilterCell.swift
index f5783bfa..5c8981b7 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterCell.swift
+++ b/Poppool/Poppool/Presentation/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/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift
index 0aa4964c..3ad18fc0 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChip.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class FilterChip: UIButton {
enum Style {
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift
index 68aa555a..9e05b052 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterChipsView.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class FilterChipsView: UIView {
// MARK: - Components
@@ -63,7 +63,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 +97,7 @@ final class FilterChipsView: UIView {
let removedFilter = filters[index]
filters.remove(at: index)
updateUI()
-
+
// 콜백 호출
onRemoveChip?(removedFilter)
}
diff --git a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift
index 85a3812c..99dfe49c 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/FilterTabsView.swift
@@ -1,8 +1,8 @@
-//import UIKit
-//import RxSwift
-//import RxCocoa
+// import UIKit
+// import RxSwift
+// import RxCocoa
//
-//final class FilterTabsView: UIView {
+// final class FilterTabsView: UIView {
// private let tabs = ["지역", "카테고리"]
// let segmentedControl = UISegmentedControl()
//
@@ -30,10 +30,10 @@
// make.edges.equalToSuperview()
// }
// }
-//}
+// }
//
-//extension Reactive where Base: FilterTabsView {
+// 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
index 4f4bbdf4..567dfd1d 100644
--- a/Poppool/Poppool/Presentation/Map/FillterSheetView/LocationFilterView.swift
+++ b/Poppool/Poppool/Presentation/Map/FillterSheetView/LocationFilterView.swift
@@ -1,7 +1,7 @@
-//import UIKit
-//import SnapKit
+// import UIKit
+// import SnapKit
//
-//final class LocationFilterView: UIView {
+// final class LocationFilterView: UIView {
// private let scrollView = UIScrollView()
// private let contentStack = UIStackView()
//
@@ -37,4 +37,4 @@
// contentStack.addArrangedSubview(chip)
// }
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift
index 973a81e5..691cf788 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FindDirectionEndPoint.swift
@@ -13,10 +13,9 @@ struct FindDirectionEndPoint {
popUpStoreId: Int64
) -> Endpoint {
return Endpoint(
- baseURL: Secrets.popPoolBaseUrl.rawValue,
+ baseURL: KeyPath.popPoolBaseURL,
path: "/popup/\(popUpStoreId)/directions",
method: .get
)
}
}
-
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift
index 690afbd7..82393654 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/FullScreenMapViewController.swift
@@ -1,186 +1,249 @@
-import Foundation
-import UIKit
-import RxSwift
-import ReactorKit
import CoreLocation
-import GoogleMaps
+import NMapsMap
+import RxCocoa
+import RxSwift
+import SnapKit
+import UIKit
-final class FullScreenMapViewController: MapViewController {
- var selectedStore: MapPopUpStore?
- var shouldAutoSelectNearestStore = false // 일반 모드와 다르게 false로 설정
+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()
-
- self.navigationController?.navigationBar.isHidden = false
+ setupFullScreenUI()
setupNavigation()
+// configureInitialMapPosition()
+ self.navigationController?.navigationBar.isHidden = false
+ Logger.log(message: "💡 초기 위치 구성 직전: initialStore=\(String(describing: initialStore?.name))", category: .debug)
+ configureInitialMapPosition()
+
+ Logger.log(message: "✅ 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)
+ }
- mainView.searchFilterContainer.isHidden = true
+ @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.delegate = self
+ mainView.mapView.snp.remakeConstraints { make in
+ make.edges.equalToSuperview()
+ }
- // 선택된 스토어가 있다면 즉시 마커 탭 처리 (요구사항 1)
- if let store = selectedStore {
- updateUI(for: store)
+ 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)
+ cameraUpdate.animation = .easeIn
+ cameraUpdate.animationDuration = 0.3
+ mainView.mapView.moveCamera(cameraUpdate)
- // MARK: - Binding
- override func bind(reactor: Reactor) {
+ 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)
- // [변경] 기존 viewportStores 관련 바인딩은 풀스크린에서 marker tap 처리와 충돌할 수 있으므로 주석처리하거나 제거
- /*
- reactor.state
- .map { $0.viewportStores }
+ // 캐러셀 상태 관찰
+ carouselView.rx.observe(Bool.self, "isHidden")
.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)
+ .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)
}
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- self.navigationController?.navigationBar.isHidden = false
+ // 마커 스타일 업데이트 함수 - 항상 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)
}
- // 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)
+ 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
}
- // (3) 단일 스토어
- else if let singleStore = marker.userData as? MapPopUpStore {
- return handleSingleStoreTap(marker, store: singleStore)
- }
- return false
- }
- override func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {
- // 카메라 이동 중 별도 처리 없음
+ super.updateMarkerStyle(marker: marker, selected: selected, isCluster: isCluster, count: count, regionName: regionName)
}
- override func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {
- // 지도 빈 공간 탭은 무시
- }
+ override func handleSingleStoreTap(_ marker: NMFMarker, store: MapPopUpStore) -> Bool {
+ isMovingToMarker = true
+ markerLocked = true
- private func findMarkerForStore(for store: MapPopUpStore) -> GMSMarker? {
- if let marker = self.currentMarker,
- let markerStore = marker.userData as? MapPopUpStore,
- markerStore.id == store.id {
- return marker
+ if let previousMarker = currentMarker, previousMarker != marker {
+ fullScreenUpdateMarkerStyle(marker: previousMarker, selected: false)
}
- 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
+ marker.iconImage = NMFOverlayImage(name: "TapMarker")
+ marker.width = 44
+ marker.height = 44
+ fullScreenUpdateMarkerStyle(marker: marker, selected: true)
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.updateCards([store])
carouselView.isHidden = false
+ mainView.setStoreCardHidden(false, animated: true)
- // 약간의 딜레이 후 마커 뷰 재갱신 (필요 시)
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
- markerView.injection(with: selectedInput)
- markerView.setNeedsLayout()
- markerView.layoutIfNeeded()
+ let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position, zoomTo: 15.0)
+ cameraUpdate.animation = .easeIn
+ cameraUpdate.animationDuration = 0.3
+ 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
+ }
- @objc private func backButtonTapped() {
- dismiss(animated: true)
+ // 카메라 이동 시작 시 호출
+ override func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) {
+ if isFullScreenMode && markerLocked {
+ return
+ }
+ super.mapView(mapView, cameraWillChangeByReason: reason, animated: animated)
}
- 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)
+ // 카메라 이동 중 호출
+ 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/GetPopUpDirectionResponseDTO.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift
index fa31b5d8..cd67dfee 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/GetPopUpDirectionResponseDTO.swift
@@ -7,7 +7,6 @@
import Foundation
-
struct GetPopUpDirectionResponseDTO: Decodable {
let id: Int64
let categoryName: String
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift
index 39b05082..fcb8e508 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapDirectionRepository.swift
@@ -8,7 +8,6 @@
import Foundation
import RxSwift
-
protocol MapDirectionRepository {
func getPopUpDirection(popUpStoreId: Int64) -> Observable
}
@@ -25,7 +24,7 @@ final class DefaultMapDirectionRepository: MapDirectionRepository {
let endpoint = FindDirectionEndPoint.fetchDirection(popUpStoreId: popUpStoreId)
// print("🌎 [Repository]: 요청 생성 - \(endpoint)")
return provider.requestData(with: endpoint, interceptor: TokenInterceptor())
- .do(onNext: { response in
+ .do(onNext: { _ in
// print("✅ [Repository]: 응답 수신 - \(response)")
}, onError: { error in
print("❌ [Repository]: 요청 실패 - \(error)")
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideAppService.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideAppService.swift
new file mode 100644
index 00000000..048c1f2f
--- /dev/null
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideAppService.swift
@@ -0,0 +1,71 @@
+import CoreLocation
+import UIKit
+
+enum MapAppType {
+ case naver
+ case kakao
+ case apple
+
+ static func from(string: String) -> MapAppType? {
+ switch string.lowercased() {
+ case "naver":
+ return .naver
+ case "kakao":
+ return .kakao
+ case "apple", "applemap":
+ return .apple
+ default:
+ return nil
+ }
+ }
+
+ func urlScheme(coordinate: CLLocationCoordinate2D, name: String, address: String) -> String {
+ let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
+ let encodedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
+
+ switch self {
+ case .naver:
+ return "nmap://place?lat=\(coordinate.latitude)&lng=\(coordinate.longitude)&name=\(encodedName)&addr=\(encodedAddress)&appname=com.poppool.app"
+ case .kakao:
+ return "kakaomap://look?p=\(coordinate.latitude),\(coordinate.longitude)"
+ case .apple:
+ return "maps://?q=\(encodedName)&ll=\(coordinate.latitude),\(coordinate.longitude)&z=16"
+ }
+ }
+
+ var appStoreURL: String {
+ switch self {
+ case .naver:
+ return "https://apps.apple.com/kr/app/id311867728"
+ case .kakao:
+ return "https://apps.apple.com/kr/app/id304608425"
+ case .apple:
+ return "https://apps.apple.com/kr/app/id1108185179"
+ }
+ }
+}
+
+class MapAppService {
+ static func openMapApp(_ appTypeString: String, coordinate: CLLocationCoordinate2D, name: String, address: String) -> Observable {
+ guard let appType = MapAppType.from(string: appTypeString) else {
+ return Observable.just("지원하지 않는 맵 앱입니다.")
+ }
+
+ let urlScheme = appType.urlScheme(coordinate: coordinate, name: name, address: address)
+
+ Logger.log(message: "🗺 맵 앱 열기 시도: \(urlScheme)", category: .debug)
+
+ if let url = URL(string: urlScheme), UIApplication.shared.canOpenURL(url) {
+ Logger.log(message: "✅ \(appType) 앱 실행", category: .debug)
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
+ return Observable.empty()
+ } else {
+ Logger.log(message: "❌ \(appType) 앱 미설치 - 앱스토어로 이동", category: .debug)
+ if let appStoreURL = URL(string: appType.appStoreURL) {
+ UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil)
+ return Observable.just("\(appTypeString) 앱이 설치되어 있지 않아 앱스토어로 이동합니다.")
+ }
+ return Observable.just("앱을 열 수 없습니다.")
+ }
+ }
+}
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift
index 089cb950..dd3fef65 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideReactor.swift
@@ -1,6 +1,6 @@
+import CoreLocation
import ReactorKit
import RxSwift
-import CoreLocation
import UIKit
final class MapGuideReactor: Reactor {
diff --git a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift
index c07ed2b8..60d2d07c 100644
--- a/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift
+++ b/Poppool/Poppool/Presentation/Map/FindMap/MapGuideView/MapGuideViewController.swift
@@ -1,133 +1,153 @@
-import UIKit
-import SnapKit
-import GoogleMaps
+import CoreLocation
+import NMapsMap
import ReactorKit
import RxSwift
-import CoreLocation
+import SnapKit
+import UIKit
final class MapGuideViewController: UIViewController, View {
// MARK: - Properties
var disposeBag = DisposeBag()
- private let popUpStoreId: Int64
- private var currentCarouselStores: [MapPopUpStore] = [] // 현재 선택된 스토어 목록
+ private let popupStoreIdentifier: Int64
+ private var currentCarouselStoreList: [MapPopUpStore] = [] // 현재 선택된 스토어 목록
- init(popUpStoreId: Int64) {
- self.popUpStoreId = popUpStoreId
- super.init(nibName: nil, bundle: nil)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
+ // MARK: - UI Components
private let dimmingView: UIView = {
- let v = UIView()
- v.backgroundColor = UIColor.gray.withAlphaComponent(0.3)
- v.alpha = 0
- return v
+ let viewInstance = UIView()
+ viewInstance.backgroundColor = UIColor.gray.withAlphaComponent(0.7)
+ viewInstance.alpha = 0
+ return viewInstance
}()
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
+ 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 lb = UILabel()
- lb.text = "찾아가는 길"
- lb.font = UIFont.boldSystemFont(ofSize: 17)
- lb.textColor = .black
- return lb
+ let labelInstance = UILabel()
+ labelInstance.text = "찾아가는 길"
+ labelInstance.font = UIFont.boldSystemFont(ofSize: 17)
+ labelInstance.textColor = .black
+ return labelInstance
}()
private let closeButton: UIButton = {
- let btn = UIButton(type: .system)
+ let buttonInstance = UIButton(type: .system)
let image = UIImage(named: "icon_xmark")?.withRenderingMode(.alwaysOriginal)
- btn.setImage(image, for: .normal)
- return btn
+ buttonInstance.setImage(image, for: .normal)
+ return buttonInstance
}()
- 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
+ private let mapView: NMFMapView = {
+ let mapViewInstance = NMFMapView()
+ mapViewInstance.layer.borderWidth = 1
+ mapViewInstance.layer.borderColor = UIColor.g100.cgColor
+ mapViewInstance.layer.cornerRadius = 12
+ return mapViewInstance
}()
- /// 지도 우상단 "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
+ 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 lb = UILabel()
- lb.text = "지도 앱으로\n바로 찾아볼까요?"
- lb.font = UIFont.systemFont(ofSize: 15, weight: .medium)
- lb.textColor = .darkGray
- lb.numberOfLines = 2
- return lb
+ 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 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
+ 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 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
+ 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 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
+ 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.popUpStoreId))
+ reactor.action.onNext(.viewDidLoad(self.popupStoreIdentifier))
// 닫기 버튼
closeButton.rx.tap
.subscribe(onNext: { [weak self] in
- self?.dismiss(animated: true, completion: nil)
+ self?.dismissModalCard()
})
.disposed(by: disposeBag)
@@ -145,45 +165,68 @@ final class MapGuideViewController: UIViewController, View {
.bind(to: reactor.action)
.disposed(by: disposeBag)
+ // 확장 버튼 탭 처리 및 지도 풀스크린 전환
expandButton.rx.tap
+ .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.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)
+ guard let strongSelf = self else { return }
+
+ let providerInstance = ProviderImpl()
+ let repositoryInstance = DefaultMapRepository(provider: providerInstance)
+ let useCaseInstance = DefaultMapUseCase(repository: repositoryInstance)
+ let directionRepositoryInstance = DefaultMapDirectionRepository(provider: providerInstance)
+ let mapReactorInstance = MapReactor(useCase: useCaseInstance, directionRepository: directionRepositoryInstance)
+
+ 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 {
- reactor.action.onNext(.viewDidLoad(self.popUpStoreId))
- reactor.state
+ mapReactorInstance.action.onNext(.viewDidLoad(strongSelf.popupStoreIdentifier))
+
+ mapReactorInstance.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)
+ .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: self.disposeBag)
+ .disposed(by: strongSelf.disposeBag)
}
})
.disposed(by: disposeBag)
+ // 목적지 좌표에 따른 마커 및 카메라 설정
reactor.state
.map { $0.destinationCoordinate }
.compactMap { $0 }
@@ -192,6 +235,17 @@ final class MapGuideViewController: UIViewController, View {
})
.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()
@@ -204,10 +258,11 @@ final class MapGuideViewController: UIViewController, View {
// MARK: - UI Setup
private func setupUI() {
- view.backgroundColor = .clear
-
+ view.backgroundColor = .clear // 배경색을 clear로 설정하여 항상 딤드 뷰가 보이도록 함
view.addSubview(dimmingView)
- dimmingView.snp.makeConstraints { $0.edges.equalToSuperview() }
+ dimmingView.snp.makeConstraints { make in
+ make.edges.equalToSuperview()
+ }
view.addSubview(modalCardView)
modalCardView.snp.makeConstraints { make in
@@ -216,21 +271,21 @@ final class MapGuideViewController: UIViewController, View {
self.modalCardBottomConstraint = make.bottom.equalToSuperview().offset(408).constraint
}
- let topContainer = UIView()
- modalCardView.addSubview(topContainer)
- topContainer.snp.makeConstraints { make in
+ let topContainerView = UIView()
+ modalCardView.addSubview(topContainerView)
+ topContainerView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.leading.trailing.equalToSuperview()
make.height.equalTo(44)
}
- topContainer.addSubview(titleLabel)
+ topContainerView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(20)
}
- topContainer.addSubview(closeButton)
+ topContainerView.addSubview(closeButton)
closeButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().inset(16)
@@ -239,47 +294,46 @@ final class MapGuideViewController: UIViewController, View {
modalCardView.addSubview(mapView)
mapView.snp.makeConstraints { make in
- make.top.equalTo(topContainer.snp.bottom).offset(20)
+ make.top.equalTo(topContainerView.snp.bottom).offset(20)
make.leading.trailing.equalToSuperview().inset(20)
- make.height.equalTo(320)
+ make.height.equalTo(240) // 높이 약간 조정
}
- mapView.addSubview(expandButton)
+ modalCardView.addSubview(expandButton)
expandButton.snp.makeConstraints { make in
- make.bottom.equalToSuperview().inset(10)
- make.trailing.equalToSuperview().inset(10)
+ make.bottom.equalTo(mapView.snp.bottom).offset(-10)
+ make.trailing.equalTo(mapView.snp.trailing).offset(-10)
make.width.height.equalTo(32)
}
- let bottomContainer = UIView()
- modalCardView.addSubview(bottomContainer)
- bottomContainer.snp.makeConstraints { make in
+ 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)
}
- bottomContainer.addSubview(promptLabel)
+ bottomContainerView.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
+ let applicationStackView = UIStackView(arrangedSubviews: [naverButton, kakaoButton, appleButton])
+ applicationStackView.axis = .horizontal
+ applicationStackView.alignment = .center
+ applicationStackView.spacing = 16
+ applicationStackView.distribution = .fillEqually
- bottomContainer.addSubview(appStack)
- appStack.snp.makeConstraints { make in
+ bottomContainerView.addSubview(applicationStackView)
+ applicationStackView.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))
+ button.snp.makeConstraints { constraint in
+ constraint.size.equalTo(CGSize(width: 48, height: 48))
}
}
}
@@ -287,6 +341,7 @@ final class MapGuideViewController: UIViewController, View {
private func presentModalCard() {
self.dimmingView.alpha = 1
+
UIView.animate(
withDuration: 0.3,
delay: 0,
@@ -300,37 +355,40 @@ final class MapGuideViewController: UIViewController, View {
}
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
+ mapView.subviews.forEach { subview in
+ if subview is NMFMarker {
+ subview.removeFromSuperview()
+ }
+ }
- CATransaction.commit()
+ // 새 마커 생성 및 설정
+ 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.dimmingView.alpha = 0
self.modalCardBottomConstraint?.update(offset: 408)
self.view.layoutIfNeeded()
} completion: { _ in
- self.navigationController?.popViewController(animated: false)
+ self.dismiss(animated: false)
}
}
}
diff --git a/Poppool/Poppool/Presentation/Map/MapStoreCard.swift b/Poppool/Poppool/Presentation/Map/MapStoreCard.swift
index 9647c3d4..a4c86d8f 100644
--- a/Poppool/Poppool/Presentation/Map/MapStoreCard.swift
+++ b/Poppool/Poppool/Presentation/Map/MapStoreCard.swift
@@ -1,7 +1,7 @@
-//import UIKit
-//import SnapKit
+// import UIKit
+// import SnapKit
//
-//final class MapStoreCard: UIView {
+// final class MapStoreCard: UIView {
// // MARK: - Components
// private let containerView: UIView = {
// let view = UIView()
@@ -43,10 +43,10 @@
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
-//}
+// }
//
//// MARK: - Setup
-//private extension MapStoreCard {
+// private extension MapStoreCard {
// func setupLayout() {
// addSubview(containerView)
//
@@ -94,10 +94,10 @@
// locationLabel.textColor = .g500
// dateLabel.textColor = .g500
// }
-//}
+// }
//
//// MARK: - Inputable
-//extension MapStoreCard: Inputable {
+// extension MapStoreCard: Inputable {
// struct Input {
// let image: UIImage?
// let category: String
@@ -113,4 +113,4 @@
// locationLabel.text = input.location
// dateLabel.text = input.date
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift b/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift
index a90eb377..be525bb3 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift
+++ b/Poppool/Poppool/Presentation/Map/MapView/MapMarker.swift
@@ -1,13 +1,12 @@
-import UIKit
+import NMapsMap
import SnapKit
-import GoogleMaps
+import UIKit
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")
@@ -181,8 +180,6 @@ extension MapMarker: Inputable {
CATransaction.commit()
}
-
-
private func setupClusterMarker(_ input: Input) {
markerImageView.isHidden = true
clusterContainer.isHidden = false
@@ -223,7 +220,19 @@ extension MapMarker {
var imageView: UIImageView {
return markerImageView
}
+
+ func asImage() -> UIImage? {
+ 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/Poppool/Presentation/Map/MapView/MapReactor.swift
index 8603c691..b0e82322 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MapReactor.swift
+++ b/Poppool/Poppool/Presentation/Map/MapView/MapReactor.swift
@@ -1,6 +1,6 @@
+import CoreLocation
import ReactorKit
import RxSwift
-import CoreLocation
final class MapReactor: Reactor {
// MARK: - Reactor
@@ -15,6 +15,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,
@@ -89,17 +90,14 @@ final class MapReactor: Reactor {
func mutate(action: Action) -> Observable {
switch action {
case .fetchCategories:
- Logger.log(message: "카테고리 매핑", category: .debug)
return useCase.fetchCategories()
.map { categories in
let mapping = categories.reduce(into: [String: Int64]()) { 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))
}
@@ -124,15 +122,6 @@ 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(
@@ -166,8 +155,6 @@ final class MapReactor: Reactor {
.just(.setLoading(false))
])
-
-
case let .updateBothFilters(locations, categories):
return .concat([
.just(.setLocationFilters(locations)),
@@ -213,20 +200,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:
@@ -244,18 +217,7 @@ final class MapReactor: Reactor {
case .viewDidLoad(let id):
return directionRepository.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(
@@ -263,19 +225,11 @@ final class MapReactor: Reactor {
category: .error
)
},
- onSubscribe: {
- Logger.log(
- message: "🌎 [네트워크]: 요청 보냄 - popUpStoreId: \(id)",
- category: .network
- )
- }
+ onSubscribe: { }
)
.map { dto in
let response = dto.toDomain()
- Logger.log(
- message: "🛠️ [도메인 매핑]: \(response)",
- category: .debug
- )
+
return MapPopUpStore(
id: response.id,
category: response.categoryName,
@@ -292,20 +246,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)),
+ useCase.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 +313,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 +339,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
@@ -387,10 +361,8 @@ 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
@@ -407,10 +379,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/Poppool/Presentation/Map/MapView/MapSearchInput.swift
index f10a3973..e5532286 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MapSearchInput.swift
+++ b/Poppool/Poppool/Presentation/Map/MapView/MapSearchInput.swift
@@ -1,7 +1,7 @@
-import UIKit
import ReactorKit
import RxCocoa
import RxSwift
+import UIKit
final class MapSearchInput: UIView, View {
// MARK: - Components
diff --git a/Poppool/Poppool/Presentation/Map/MapView/MapView.swift b/Poppool/Poppool/Presentation/Map/MapView/MapView.swift
index 30d4f7c2..af1529e9 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MapView.swift
+++ b/Poppool/Poppool/Presentation/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.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/Poppool/Presentation/Map/MapView/MapViewController.swift b/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift
index 52bf64b6..e327b463 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift
+++ b/Poppool/Poppool/Presentation/Map/MapView/MapViewController.swift
@@ -1,18 +1,16 @@
-import UIKit
+import CoreLocation
import FloatingPanel
-import SnapKit
-import RxSwift
-import RxCocoa
+import NMapsMap
import ReactorKit
-import GoogleMaps
-import CoreLocation
+import RxCocoa
import RxGesture
+import RxSwift
+import SnapKit
+import UIKit
-
-class MapViewController: BaseViewController, View {
+class MapViewController: BaseViewController, View, CLLocationManagerDelegate, NMFMapViewTouchDelegate, NMFMapViewCameraDelegate, UIGestureRecognizerDelegate {
typealias Reactor = MapReactor
-
fileprivate struct CoordinateKey: Hashable {
let lat: Int
let lng: Int
@@ -22,20 +20,18 @@ class MapViewController: BaseViewController, View {
self.lng = Int(longitude * 1_000_00)
}
}
- // 전체 스토어 목록 저장
- var allStores: [MapPopUpStore] = []
+
var currentTooltipView: UIView?
var currentTooltipStores: [MapPopUpStore] = []
- var currentTooltipCoordinate: CLLocationCoordinate2D?
-
+ var currentTooltipCoordinate: NMGLatLng?
// MARK: - Properties
private var storeDetailsCache: [Int64: StoreItem] = [:]
- private var isMovingToMarker = false
+ var isMovingToMarker = false
var currentCarouselStores: [MapPopUpStore] = []
- private var markerDictionary: [Int64: GMSMarker] = [:]
- private var individualMarkerDictionary: [Int64: GMSMarker] = [:]
- private var clusterMarkerDictionary: [String: GMSMarker] = [:]
+ private var markerDictionary: [Int64: NMFMarker] = [:]
+ private var individualMarkerDictionary: [Int64: NMFMarker] = [:]
+ private var clusterMarkerDictionary: [String: NMFMarker] = [:]
private let popUpAPIUseCase = PopUpAPIUseCaseImpl(
repository: PopUpAPIRepositoryImpl(provider: ProviderImpl()))
private let clusteringManager = ClusteringManager()
@@ -44,7 +40,7 @@ class MapViewController: BaseViewController, View {
let mainView = MapView()
let carouselView = MapPopupCarouselView()
private let locationManager = CLLocationManager()
- var currentMarker: GMSMarker?
+ var currentMarker: NMFMarker?
private let storeListReactor = StoreListReactor()
private let storeListViewController = StoreListViewController(reactor: StoreListReactor())
private var listViewTopConstraint: Constraint?
@@ -62,6 +58,7 @@ class MapViewController: BaseViewController, View {
}
private var modalState: ModalState = .bottom
+ private let idleSubject = PublishSubject()
// MARK: - Lifecycle
override func viewDidAppear(_ animated: Bool) {
@@ -72,6 +69,7 @@ class MapViewController: BaseViewController, View {
self.filterChipsTopY = frameInView.minY
}
}
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
@@ -80,60 +78,32 @@ class MapViewController: BaseViewController, View {
override func viewDidLoad() {
super.viewDidLoad()
setUp()
- mainView.mapView.padding = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0)
-
+ mainView.mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0)
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
- mainView.mapView.isMyLocationEnabled = true
+ mainView.mapView.positionMode = .compass
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) // 한국 남서쪽 끝
+ northEast: NMGLatLng(lat: 38.0, lng: 132.0),
+ southWest: NMGLatLng(lat: 33.0, lng: 124.0)
)
- // 전체 스토어 가져오기
reactor.action.onNext(.viewportChanged(
- northEastLat: koreaRegion.northEast.latitude,
- northEastLon: koreaRegion.northEast.longitude,
- southWestLat: koreaRegion.southWest.latitude,
- southWestLon: koreaRegion.southWest.longitude
+ northEastLat: koreaRegion.northEast.lat,
+ northEastLon: koreaRegion.northEast.lng,
+ southWestLat: koreaRegion.southWest.lat,
+ southWestLon: koreaRegion.southWest.lng
))
-
- // 데이터 로드 후 모든 마커 생성 및 추가
- 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)
}
-
+ setupMapViewRxObservables()
carouselView.rx.observe(Bool.self, "hidden")
.distinctUntilChanged()
@@ -153,27 +123,6 @@ class MapViewController: BaseViewController, View {
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,
@@ -182,13 +131,8 @@ class MapViewController: BaseViewController, View {
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
- ))
+ if let previousMarker = self.currentMarker {
+ self.updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false, count: 1)
}
// 스와이프한 스토어에 해당하는 마커 찾기
@@ -196,19 +140,15 @@ class MapViewController: BaseViewController, View {
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.updateMarkerStyle(marker: markerToFocus, selected: true, isCluster: false, count: 1)
self.currentMarker = markerToFocus
// 마이크로 클러스터인 경우 툴팁 처리
- if let storeArray = markerToFocus.userData as? [MapPopUpStore], storeArray.count > 1 {
- if self.currentTooltipView == nil || self.currentTooltipCoordinate != markerToFocus.position {
+ 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)
}
@@ -225,41 +165,35 @@ class MapViewController: BaseViewController, View {
}
if let reactor = self.reactor {
- bindViewport(reactor: 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
- }
+ // NMF 이벤트 설정을 위한 새로운 메서드
+ private func setupMapViewRxObservables() {
+ // 지도 이동 완료 감지
+ mainView.mapView.addCameraDelegate(delegate: self)
- Logger.log(
- message: "🔍 전체 마커 생성 및 지도에 추가 완료: \(stores.count)개",
- category: .debug
- )
+ 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: GMSMarker, stores: [MapPopUpStore]) {
+ private func configureTooltip(for marker: NMFMarker, stores: [MapPopUpStore]) {
Logger.log(message: """
툴팁 설정:
- 현재 캐러셀 스토어: \(currentCarouselStores.map { $0.name })
@@ -272,25 +206,17 @@ class MapViewController: BaseViewController, View {
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
- ))
- }
+ self.updateMarkerStyle(marker: marker, selected: true, isCluster: false, count: stores.count)
tooltipView.selectStore(at: index)
+
Logger.log(message: """
툴팁 선택:
- 선택된 스토어: \(stores[index].name)
@@ -298,11 +224,11 @@ class MapViewController: BaseViewController, View {
""", category: .debug)
}
- // 툴팁 위치 설정 (예시: 마커 우측에 위치)
- let markerPoint = self.mainView.mapView.projection.point(for: marker.position)
- let markerHeight = (marker.iconView as? MapMarker)?.imageView.frame.height ?? 32
+ let markerPoint = self.mainView.mapView.projection.point(from: marker.position)
+ let markerHeight: CGFloat = 32
+
tooltipView.frame = CGRect(
- x: markerPoint.x , // 마커 오른쪽 10포인트
+ x: markerPoint.x,
y: markerPoint.y - markerHeight - tooltipView.frame.height - 14,
width: tooltipView.frame.width,
height: tooltipView.frame.height
@@ -328,7 +254,7 @@ class MapViewController: BaseViewController, View {
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24)
}
carouselView.isHidden = true
- mainView.mapView.delegate = self
+ mainView.mapView.touchDelegate = self
addChild(storeListViewController)
view.addSubview(storeListViewController.view)
@@ -346,36 +272,13 @@ class MapViewController: BaseViewController, View {
setupPanAndSwipeGestures()
let mapViewTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMapViewTap(_:)))
+// mapViewTapGesture.cancelsTouchesInView = false // 중요: 다른 터치 이벤트를 방해하지 않음
+ mapViewTapGesture.delaysTouchesBegan = false // 터치 지연 없음
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 let defaultZoomLevel: Double = 15.0
private func setupPanAndSwipeGestures() {
storeListViewController.mainView.grabberHandle.rx.swipeGesture(.up)
.skip(1)
@@ -412,7 +315,6 @@ class MapViewController: BaseViewController, View {
// MARK: - Bind
func bind(reactor: Reactor) {
-
// 필터 관련 바인딩
mainView.filterChips.locationChip.rx.tap
.map { Reactor.Action.filterTapped(.location) }
@@ -438,65 +340,58 @@ class MapViewController: BaseViewController, View {
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)
+ // 현재 위치로 카메라 이동
+ 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.mainView.mapView.projection.visibleRegion()
+ let bounds = self.getVisibleBounds()
self.reactor?.action.onNext(.viewportChanged(
- northEastLat: bounds.farRight.latitude,
- northEastLon: bounds.farRight.longitude,
- southWestLat: bounds.nearLeft.latitude,
- southWestLon: bounds.nearLeft.longitude
+ northEastLat: bounds.northEast.lat,
+ northEastLon: bounds.northEast.lng,
+ southWestLat: bounds.southWest.lat,
+ southWestLon: bounds.southWest.lng
))
self.clearAllMarkers()
- self.clusterMarkerDictionary.values.forEach { $0.map = nil }
- self.clusterMarkerDictionary.removeAll()
+ 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.carouselView.isHidden = true
- self.carouselView.updateCards([])
- self.currentCarouselStores = []
- self.mainView.setStoreCardHidden(true, animated: true)
+ self.updateMapWithClustering()
+ }
- 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()
+ let bounds = self.getVisibleBounds()
self.reactor?.action.onNext(.viewportChanged(
- northEastLat: bounds.farRight.latitude,
- northEastLon: bounds.farRight.longitude,
- southWestLat: bounds.nearLeft.latitude,
- southWestLon: bounds.nearLeft.longitude
+ northEastLat: bounds.northEast.lat,
+ northEastLon: bounds.northEast.lng,
+ southWestLat: bounds.southWest.lat,
+ southWestLon: bounds.southWest.lng
))
self.resetSelectedMarker()
-
- // 만약 지도 위 마커를 전부 제거하고 싶다면 (상황에 따라)
- // self.clearAllMarkers()
- // self.clusterMarkerDictionary.values.forEach { $0.map = nil }
- // self.clusterMarkerDictionary.removeAll()
self.carouselView.isHidden = true
self.carouselView.updateCards([])
self.currentCarouselStores = []
@@ -530,14 +425,6 @@ class MapViewController: BaseViewController, View {
}
.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
@@ -545,7 +432,6 @@ class MapViewController: BaseViewController, View {
}
.disposed(by: disposeBag)
-
reactor.state.map { $0.activeFilterType }
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
@@ -558,49 +444,38 @@ class MapViewController: BaseViewController, View {
}
})
.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)
+
+ // 검색 결과 위치로 카메라 이동
+ 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.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)
@@ -608,7 +483,7 @@ class MapViewController: BaseViewController, View {
guard let self = self else { return }
// 이전 선택된 마커, 툴팁, 캐러셀 초기화
- self.mainView.mapView.clear()
+ self.clearAllMarkers()
self.storeListViewController.reactor?.action.onNext(.setStores([]))
self.carouselView.updateCards([])
self.carouselView.isHidden = true
@@ -634,45 +509,20 @@ class MapViewController: BaseViewController, View {
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)
+ 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)
-
-
-
-
-// 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) {
@@ -682,22 +532,54 @@ class MapViewController: BaseViewController, View {
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 }
- func addMarker(for store: MapPopUpStore) {
- let marker = GMSMarker()
- marker.position = store.coordinate
- marker.userData = store
+ Logger.log(message: "마커 터치됨! 위치: \(marker.position), 스토어: \(store.name)", category: .debug)
+
+ // 단일 스토어 마커 처리
+ return self.handleSingleStoreTap(marker, store: store)
+ }
- marker.groundAnchor = CGPoint(x: 0.5, y: 1.0)
+ marker.mapView = mainView.mapView
+ markerDictionary[store.id] = marker
+ }
+
+ func updateMarkerStyle(marker: NMFMarker, selected: Bool, isCluster: Bool, count: Int = 1, regionName: String = "") {
+ if selected {
+ marker.width = 44
+ marker.height = 44
+ marker.iconImage = NMFOverlayImage(name: "TapMarker")
+ } else if isCluster {
+ marker.width = 36
+ marker.height = 36
+ marker.iconImage = NMFOverlayImage(name: "cluster_marker")
+ } else {
+ marker.width = 32
+ marker.height = 32
+ marker.iconImage = NMFOverlayImage(name: "Marker")
+ }
- let markerView = MapMarker()
- markerView.injection(with: store.toMarkerInput())
- marker.iconView = markerView
- marker.map = mainView.mapView
- }
+ if count > 1 {
+ marker.captionText = "\(count)"
+ } else {
+ marker.captionText = ""
+ }
+
+ marker.anchor = CGPoint(x: 0.5, y: 1.0)
+ }
@objc private func handleMapViewTap(_ gesture: UITapGestureRecognizer) {
// 리스트뷰가 현재 보이는 상태(중간 또는 상단)일 때만 내림
@@ -772,11 +654,11 @@ class MapViewController: BaseViewController, View {
}
}
-
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(완전히 보임)
+ 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()
@@ -793,8 +675,6 @@ class MapViewController: BaseViewController, View {
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)
@@ -805,52 +685,33 @@ class MapViewController: BaseViewController, View {
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)
- }
+ 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(
+ message: "✅ 전체 스토어 목록으로 리스트뷰 업데이트: \(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()
@@ -860,1152 +721,1034 @@ class MapViewController: BaseViewController, View {
}
}
+ 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
+ }
- // updateMapWithClustering() 메서드 전체 구현
+ // MARK: - Helper: 클러스터용 커스텀 마커 이미지 생성 (MapMarker를 사용)
+ func createClusterMarkerImage(regionName: String, count: Int) -> UIImage? {
+ // MapMarker의 입력값에 클러스터 상태를 전달합니다.
+ let markerView = MapMarker() // 기존 커스텀 뷰, 네이버맵용으로도 사용 가능하도록 구현됨.
+ let input = MapMarker.Input(isSelected: false,
+ isCluster: true,
+ regionName: regionName,
+ count: count,
+ isMultiMarker: false)
+ markerView.injection(with: input)
+ // 프레임이 설정되어 있지 않다면 적당한 크기로 지정 (예: 80x32)
+ 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.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
-
+ let currentZoom = mainView.mapView.zoomLevel
+ let level = MapZoomLevel.getLevel(from: Float(currentZoom))
+ // 클러스터 처리 시 현재 스토어 목록(currentStores)을 사용
+ Logger.log(message: "현재 줌 레벨: \(currentZoom), 모드: \(level), 스토어 수: \(currentStores.count)", category: .debug)
CATransaction.begin()
CATransaction.setDisableActions(true)
switch level {
case .detailed:
- let newStoreIds = Set(visibleStores.map { $0.id })
- let groupedDict = groupStoresByExactLocation(visibleStores)
+ // 상세 레벨에서는 개별 마커를 사용합니다.
+ let newStoreIds = Set(currentStores.map { $0.id })
+ let groupedDict = groupStoresByExactLocation(currentStores)
- clusterMarkerDictionary.values.forEach { $0.map = nil }
+ // 클러스터 마커 제거
+ 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 != 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
- ))
+ 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 = 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
+ 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
}
} else {
+ // 여러 스토어가 동일 위치에 있으면 단일 마커로 표시하면서 count 갱신
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
- ))
- }
+ existingMarker.userInfo = ["storeData": storeGroup]
+ let isSelected = (existingMarker == currentMarker)
+ updateMarkerStyle(marker: existingMarker, selected: isSelected, 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
+ 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 }
+
+ print("마이크로 클러스터 마커 터치됨! 스토어 수: \(storeGroup.count)개")
+ return self.handleMicroClusterTap(marker, storeArray: storeGroup)
+ }
+ marker.mapView = mainView.mapView
individualMarkerDictionary[markerKey] = marker
}
}
}
- for (id, marker) in individualMarkerDictionary {
- if !newStoreIds.contains(id) {
- marker.map = nil
+ // 기존에 보이지 않는 개별 마커 제거
+ 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.map = nil }
+ individualMarkerDictionary.values.forEach { $0.mapView = nil }
+ individualMarkerDictionary.removeAll()
- // 클러스터 생성 및 업데이트
- let clusters = clusteringManager.clusterStores(visibleStores, at: currentZoom)
+ // 클러스터 생성
+ 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] {
- // 기존 마커 재사용
- 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
- ))
+ 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 {
- // 새 클러스터 마커 생성
- 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
-
+ marker = NMFMarker()
clusterMarkerDictionary[clusterKey] = marker
}
+
+ marker.position = NMGLatLng(lat: cluster.cluster.coordinate.lat, lng: cluster.cluster.coordinate.lng)
+ marker.userInfo = ["clusterData": cluster] // 중요: userInfo에 cluster 객체를 직접 저장
+
+ // 여기서 커스텀 클러스터 마커 뷰를 이미지로 변환하여 적용합니다.
+ if let clusterImage = createClusterMarkerImage(regionName: cluster.cluster.name, count: cluster.storeCount) {
+ marker.iconImage = NMFOverlayImage(image: clusterImage)
+ } else {
+ // 기본 에셋 fallback (원하는 경우)
+ marker.iconImage = NMFOverlayImage(name: "cluster_marker")
+ }
+
+ // 터치 핸들러 추가 - userInfo에서 클러스터 데이터를 직접 가져오기
+ 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.map = nil
+ marker.mapView = nil
+ clusterMarkerDictionary.removeValue(forKey: key)
}
}
}
CATransaction.commit()
}
- private func clearAllMarkers() {
- individualMarkerDictionary.values.forEach { $0.map = nil }
- individualMarkerDictionary.removeAll()
- clusterMarkerDictionary.values.forEach { $0.map = nil }
- clusterMarkerDictionary.removeAll()
+ private func clearAllMarkers() {
+ individualMarkerDictionary.values.forEach { $0.mapView = nil }
+ individualMarkerDictionary.removeAll()
- markerDictionary.values.forEach { $0.map = nil }
- markerDictionary.removeAll()
- }
+ clusterMarkerDictionary.values.forEach { $0.mapView = nil }
+ clusterMarkerDictionary.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)
+ markerDictionary.values.forEach { $0.mapView = nil }
+ markerDictionary.removeAll()
}
- return dict
- }
+ 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()
+ 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
+ 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]
- let markerView = MapMarker()
- markerView.injection(with: store.toMarkerInput())
- marker.iconView = markerView
- marker.map = mainView.mapView
+ updateMarkerStyle(marker: marker, selected: false, isCluster: false)
+ marker.mapView = mainView.mapView
- individualMarkerDictionary[store.id] = marker
+ individualMarkerDictionary[store.id] = marker
+ }
}
- }
- for (id, marker) in individualMarkerDictionary {
- if !newMarkerIDs.contains(id) {
- marker.map = nil
- individualMarkerDictionary.removeValue(forKey: id)
+ 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.latitude != fixedCoordinate.latitude ||
- marker.position.longitude != fixedCoordinate.longitude {
- marker.position = fixedCoordinate
+ if marker.position.lat != fixedCoordinate.lat || marker.position.lng != fixedCoordinate.lng {
+ marker.position = NMGLatLng(lat: fixedCoordinate.lat, lng: fixedCoordinate.lng)
}
} 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
+ 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)
- 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))
- 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.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))
+ viewController.onDismiss = { [weak self] in
+ 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.modalPresentationStyle = .overFullScreen
+ present(viewController, animated: false) {
+ viewController.showBottomSheet()
+ }
- viewController.onDismiss = { [weak self] in
- self?.reactor?.action.onNext(.filterTapped(nil))
+ currentFilterBottomSheet = viewController
}
- viewController.modalPresentationStyle = .overFullScreen
- present(viewController, animated: false) {
- viewController.showBottomSheet()
+ private func dismissFilterBottomSheet() {
+ if let bottomSheet = currentFilterBottomSheet {
+ bottomSheet.hideBottomSheet()
+ }
+ currentFilterBottomSheet = nil
}
- currentFilterBottomSheet = viewController
- }
- private func dismissFilterBottomSheet() {
- if let bottomSheet = currentFilterBottomSheet {
- bottomSheet.hideBottomSheet()
- }
- currentFilterBottomSheet = nil
- }
- //기본 마커
+ // 기본 마커
private func addMarkers(for stores: [MapPopUpStore]) {
- mainView.mapView.clear()
+ markerDictionary.values.forEach { $0.mapView = nil }
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
+ 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]) {
- // 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)
+ private func updateListView(with results: [MapPopUpStore]) {
+ // MapPopUpStore 배열을 StoreItem 배열로 변환
+ let storeItems = results.map { $0.toStoreItem() }
+ storeListViewController.reactor?.action.onNext(.setStores(storeItems))
}
- // 캐러셀 상단 Y 좌표를 지도 좌표로 변환
- let carouselTopY = carouselView.frame.minY
-
- // 화면 좌표계에서 캐러셀 상단 라인을 생성 (좌우 전체 폭)
- let leftPoint = CGPoint(x: 0, y: carouselTopY)
- let rightPoint = CGPoint(x: view.frame.width, y: carouselTopY)
+ 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)
+ }
- // 화면 좌표를 지도 좌표로 변환
- let leftCoordinate = mainView.mapView.projection.coordinate(for: leftPoint)
- let rightCoordinate = mainView.mapView.projection.coordinate(for: rightPoint)
+ private func getEffectiveViewport() -> NMGLatLngBounds {
+ let bounds = getVisibleBounds()
- // 캐러셀 영역을 제외한 북쪽 경계 결정 (원래 북쪽 경계는 그대로 유지)
- let adjustedSouthWest = CLLocationCoordinate2D(
- latitude: max(leftCoordinate.latitude, rightCoordinate.latitude),
- longitude: visibleRegion.nearLeft.longitude
- )
+ if carouselView.isHidden {
+ return NMGLatLngBounds(southWest: bounds.southWest, northEast: bounds.northEast)
+ }
- // 조정된 경계로 새 영역 생성
- return GMSCoordinateBounds(
- coordinate: visibleRegion.farLeft,
- coordinate: adjustedSouthWest
- )
- }
+ 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
+ )
- // 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
+ return NMGLatLngBounds(
+ southWest: adjustedSouthWest,
+ northEast: bounds.northEast
)
- @unknown default:
- break
}
- }
-}
-// MARK: - CLLocationManagerDelegate
-extension MapViewController: CLLocationManagerDelegate {
- func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
- guard let location = locations.last else { return }
+ // 현재 보이는 지도 영역의 경계를 가져오는 함수
+ private func getVisibleBounds() -> (northEast: NMGLatLng, southWest: NMGLatLng) {
+ let mapBounds = mainView.mapView.contentBounds
- currentMarker?.map = nil
- currentMarker = nil
- carouselView.isHidden = true
- currentCarouselStores = []
+ let northEast = NMGLatLng(lat: mapBounds.northEastLat, lng: mapBounds.northEastLng)
+ let southWest = NMGLatLng(lat: mapBounds.southWestLat, lng: mapBounds.southWestLng)
- let camera = GMSCameraPosition.camera(
- withLatitude: location.coordinate.latitude,
- longitude: location.coordinate.longitude,
- zoom: 15
- )
- mainView.mapView.animate(to: camera)
+ return (northEast: northEast, southWest: southWest)
+ }
- // 카메라 이동이 완료된 후 가장 가까운 스토어 찾기
- mainView.mapView.rx.idleAtPosition
- .take(1)
- .subscribe(onNext: { [weak self] _ in
- guard let self = self else { return }
- self.findAndShowNearestStore(from: location)
- })
- .disposed(by: disposeBag)
+ // 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(
+ message: "위치 서비스가 비활성화되었습니다. 설정에서 권한을 확인해주세요.",
+ category: .error
+ )
+ mainView.mapView.positionMode = .disabled // 내 위치 트래킹 모드 비활성화
+ @unknown default:
+ break
+ }
+ }
- locationManager.stopUpdatingLocation()
- }
+ private func updateTooltipPosition() {
+ guard let marker = currentMarker, let tooltip = currentTooltipView else { return }
+ // 마커 위치를 화면 좌표로 변환
+ let markerPoint = mainView.mapView.projection.point(from: marker.position)
+ var markerCenter = markerPoint
- private func findAndShowNearestStore(from location: CLLocation) {
- guard !currentStores.isEmpty else {
- Logger.log(message: "현재위치 표기할 스토어가 없습니다", category: .debug)
- return
- }
+ // 마커 높이 고려 (네이버 마커는 크기가 다를 수 있음)
+ markerCenter.y = markerPoint.y - 20 // 마커 이미지 높이의 절반 정도
- resetSelectedMarker()
+ // 오프셋 값 (디자인에 맞게 조정)
+ let offsetX: CGFloat = -10
+ let offsetY: CGFloat = -10
- 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)
+ tooltip.frame.origin = CGPoint(
+ x: markerCenter.x + offsetX,
+ y: markerCenter.y - tooltip.frame.height - offsetY
+ )
}
- 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)
- }
+ private func resetSelectedMarker() {
+ if let currentMarker = currentMarker {
+ // 마커 스타일 업데이트
+ updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false)
}
- }
-
-// 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()
-
- // 캐러셀 초기화
+ currentTooltipCoordinate = nil
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
- ))
+ private func updateMarkersForCluster(stores: [MapPopUpStore]) {
+ // 전체 개별 및 클러스터 마커 제거
+ for marker in individualMarkerDictionary.values {
+ marker.mapView = nil
}
+ individualMarkerDictionary.removeAll()
- // 새 마커 선택 상태로 설정
- if let markerView = marker.iconView as? MapMarker {
- markerView.injection(with: .init(
- isSelected: true,
- isCluster: false,
- count: (marker.userData as? [MapPopUpStore])?.count ?? 1
- ))
+ for marker in clusterMarkerDictionary.values {
+ marker.mapView = nil
}
+ clusterMarkerDictionary.removeAll()
- currentMarker = marker
+ // 클러스터에 포함된 스토어들만 새 마커 추가
+ 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)
- // 캐러셀에 표시할 스토어 확인
- if currentCarouselStores.isEmpty || !currentCarouselStores.contains(where: { $0.id == store.id }) {
- // 현재 뷰포트의 모든 스토어를 가져오기
- let visibleRegion = mainView.mapView.projection.visibleRegion()
- let bounds = GMSCoordinateBounds(region: visibleRegion)
+ updateMarkerStyle(marker: marker, selected: false, isCluster: false)
- let visibleStores = currentStores.filter { store in
- bounds.contains(CLLocationCoordinate2D(
- latitude: store.latitude,
- longitude: store.longitude
- ))
+ // 직접 터치 핸들러 추가
+ marker.touchHandler = { [weak self] (_) -> Bool in
+ guard let self = self else { return false }
+
+ print("클러스터 내 마커 터치됨! 스토어: \(store.name)")
+ return self.handleSingleStoreTap(marker, store: store)
}
- if !visibleStores.isEmpty {
- // 뷰포트의 모든 스토어를 캐러셀에 표시
- currentCarouselStores = visibleStores
- carouselView.updateCards(visibleStores)
+ marker.mapView = mainView.mapView
+ individualMarkerDictionary[store.id] = marker
+ }
+ }
- // 선택한 스토어의 인덱스를 찾아 스크롤
- if let index = visibleStores.firstIndex(where: { $0.id == store.id }) {
- carouselView.scrollToCard(index: index)
+ private func findMarkerForStore(for store: MapPopUpStore) -> NMFMarker? {
+ // individualMarkerDictionary에 저장된 모든 마커를 순회
+ 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
}
- } 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)
+ // 상세 레벨이 아닐 경우 clusterMarkerDictionary에도 동일하게 검색
+ for marker in clusterMarkerDictionary.values {
+ if let clusterData = marker.userInfo["clusterData"] as? ClusterMarkerData,
+ clusterData.cluster.stores.contains(where: { $0.id == store.id }) {
+ return marker
+ }
}
- } else {
- currentTooltipView?.removeFromSuperview()
- currentTooltipView = nil
+ return nil
}
- isMovingToMarker = false
- return true
- }
+ 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)
+ }
+ }
- func handleRegionalClusterTap(_ marker: GMSMarker, clusterData: ClusterMarkerData) -> Bool {
- let currentZoom = mainView.mapView.camera.zoom
- let currentLevel = MapZoomLevel.getLevel(from: currentZoom)
+ func bindViewport(reactor: MapReactor) {
+ // 카메라 이동 완료 시 이벤트 발생되는 Subject
+ let cameraObservable = PublishSubject()
+
+ // NMFMapViewCameraDelegate 메서드에서 호출할 수 있도록 설정
+
+ // 카메라 변경 감지해서 액션 전달
+ 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)
- 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
- }
+ // 현재 뷰포트 내의 스토어 업데이트 - 초기 1회
+ 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 }
- // 클러스터에 포함된 스토어들만 표시하도록 마커 업데이트
- updateMarkersForCluster(stores: clusterData.cluster.stores)
+ let effectiveViewport = self.getEffectiveViewport()
+ let bounds = self.getVisibleBounds()
- // 캐러셀 업데이트
- carouselView.updateCards(clusterData.cluster.stores)
- carouselView.isHidden = false
- self.currentCarouselStores = clusterData.cluster.stores
+ // 화면에 보이는 스토어만 필터링
+ 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
- return true
- }
+ // 개별 마커 레벨인지 확인
+ let currentZoom = self.mainView.mapView.zoomLevel
+ let level = MapZoomLevel.getLevel(from: Float(currentZoom))
- func handleMicroClusterTap(_ marker: GMSMarker, storeArray: [MapPopUpStore]) -> Bool {
- // 이미 선택된 마커를 다시 탭할 때
- if currentMarker == marker {
- // 툴팁과 캐러셀만 숨기고, 마커의 선택 상태는 유지
- currentTooltipView?.removeFromSuperview()
- currentTooltipView = nil
- currentTooltipStores = []
- currentTooltipCoordinate = nil
+ if level == .detailed && !visibleStores.isEmpty {
+ // 캐러셀에 모든 마커 정보 표시
+ let effectiveStores = visibleStores.filter { store in
+ let storePosition = NMGLatLng(lat: store.latitude, lng: store.longitude)
+ return effectiveViewport.contains(storePosition)
+ }
- carouselView.isHidden = true
- carouselView.updateCards([])
- currentCarouselStores = []
+ 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
+ }
- // 마커 상태 업데이트
- if let markerView = marker.iconView as? MapMarker {
- markerView.injection(with: .init(
- isSelected: false,
- isCluster: false,
- count: storeArray.count
- ))
- }
+ 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()
+ }
+ }
- currentMarker = nil
- isMovingToMarker = false
- return false
+ self.updateMapWithClustering()
+ })
+ .disposed(by: disposeBag)
}
- isMovingToMarker = true
+ private func findAndShowNearestStore(from location: CLLocation) {
+ guard !currentStores.isEmpty else {
+ Logger.log(message: "현재위치 표기할 스토어가 없습니다", category: .debug)
+ return
+ }
- currentTooltipView?.removeFromSuperview()
- currentTooltipView = nil
+ resetSelectedMarker()
- 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
- ))
+ 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 markerView = marker.iconView as? MapMarker {
- markerView.injection(with: .init(
- isSelected: true,
- isCluster: false,
- count: storeArray.count
- ))
- }
- currentMarker = marker
+ 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)
- currentCarouselStores = storeArray
- carouselView.updateCards(storeArray)
- carouselView.isHidden = false
- carouselView.scrollToCard(index: 0)
+ // 마커 스타일 설정
+ updateMarkerStyle(marker: marker, selected: true, isCluster: false)
+ marker.mapView = mainView.mapView
- mainView.setStoreCardHidden(false, animated: true)
+ individualMarkerDictionary[store.id] = marker
+ currentMarker = marker
+ carouselView.updateCards([store])
+ currentCarouselStores = [store]
+ carouselView.scrollToCard(index: 0)
+ mainView.setStoreCardHidden(false, animated: true)
+ }
+ }
- // 지도 이동 및 툴팁 생성
- mainView.mapView.animate(toLocation: marker.position)
+ // MARK: - Marker Handling
+ func handleSingleStoreTap(_ marker: NMFMarker, store: MapPopUpStore) -> Bool {
+ isMovingToMarker = true
- // 툴팁 생성을 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
+ // 이전 마커 선택 상태 해제
+ if let previousMarker = currentMarker {
+ updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false)
}
- }
- return true
- }
- private func showNoMarkersToast() {
- // 디자인 예정이므로 임시 구현
- Logger.log(message: "현재 지도 영역에 표시할 마커가 없습니다", category: .debug)
- }
- private func updateTooltipPosition() {
- guard let marker = currentMarker, let tooltip = currentTooltipView else { return }
+ // 새 마커 선택 상태로 설정
+ updateMarkerStyle(marker: marker, selected: true, isCluster: false)
+ currentMarker = marker
- 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
- }
+ // 캐러셀에 표시할 스토어 확인
+ if currentCarouselStores.isEmpty || !currentCarouselStores.contains(where: { $0.id == store.id }) {
+ // 현재 뷰포트의 모든 스토어를 가져오기
+ let bounds = getVisibleBounds()
- // 오프셋 값 (디자인에 맞게 조정)
- let offsetX: CGFloat = -10
- let offsetY: CGFloat = -10
+ 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)
+ }
- tooltip.frame.origin = CGPoint(
- x: markerCenter.x + offsetX,
- y: markerCenter.y - tooltip.frame.height - offsetY
- )
- }
+ if !visibleStores.isEmpty {
+ // 뷰포트의 모든 스토어를 캐러셀에 표시
+ currentCarouselStores = visibleStores
+ carouselView.updateCards(visibleStores)
- 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
- ))
+ // 선택한 스토어의 인덱스를 찾아 스크롤
+ if let index = visibleStores.firstIndex(where: { $0.id == store.id }) {
+ carouselView.scrollToCard(index: index)
+ }
+ } else {
+ // 뷰포트에 다른 스토어가 없는 경우, 선택한 스토어만 표시
+ currentCarouselStores = [store]
+ carouselView.updateCards([store])
+ }
} else {
- markerView.injection(with: .init(
- isSelected: false,
- isCluster: false
- ))
+ // 캐러셀에 이미 해당 스토어가 있는 경우, 해당 위치로 스크롤
+ if let index = currentCarouselStores.firstIndex(where: { $0.id == store.id }) {
+ carouselView.scrollToCard(index: index)
+ }
}
- }
- // 툴팁 제거
- currentTooltipView?.removeFromSuperview()
- currentTooltipView = nil
- currentTooltipStores = []
- currentTooltipCoordinate = nil
- carouselView.isHidden = true
- carouselView.updateCards([])
- currentCarouselStores = []
+ carouselView.isHidden = false
+ mainView.setStoreCardHidden(false, animated: true)
- // 현재 마커 참조 제거
- self.currentMarker = nil
- }
+ // 툴팁 처리
+ 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 {
+ print("handleRegionalClusterTap 함수 호출됨")
+ let currentZoom = mainView.mapView.zoomLevel
+ let currentLevel = MapZoomLevel.getLevel(from: Float(currentZoom))
-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
- }
+ // 디버깅
+ print("현재 줌 레벨: \(currentZoom), 모드: \(currentLevel)")
+ print("클러스터 정보: \(clusterData.cluster.name), 스토어 수: \(clusterData.storeCount)")
- 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
+ switch currentLevel {
+ case .city: // 시 단위 클러스터
+ print("시 단위 클러스터 처리")
+ 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: // 구 단위 클러스터
+ print("구 단위 클러스터 처리")
+ 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("기타 레벨 클러스터 처리")
}
- // 뷰포트가 변경될 때마다 액션 전달
- 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)
+ // 클러스터에 포함된 스토어들만 표시하도록 마커 업데이트
+ updateMarkersForCluster(stores: clusterData.cluster.stores)
- // 현재 뷰포트 내의 스토어 업데이트 - 마커만 업데이트
- 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 }
+ // 캐러셀 업데이트
+ carouselView.updateCards(clusterData.cluster.stores)
+ carouselView.isHidden = false
+ self.currentCarouselStores = clusterData.cluster.stores
- let effectiveViewport = self.getEffectiveViewport()
- let visibleRegion = self.mainView.mapView.projection.visibleRegion()
- let bounds = GMSCoordinateBounds(region: visibleRegion)
+ return true
+ }
- // 화면에 보이는 스토어만 필터링
- let visibleStores = stores.filter { store in
- bounds.contains(CLLocationCoordinate2D(
- latitude: store.latitude,
- longitude: store.longitude
- ))
- }
+ // 마이크로 클러스터 탭 처리
+ 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
+ }
- self.currentStores = visibleStores
+ isMovingToMarker = true
- // 개별 마커 레벨인지 확인
- let currentZoom = self.mainView.mapView.camera.zoom
- let level = MapZoomLevel.getLevel(from: currentZoom)
+ currentTooltipView?.removeFromSuperview()
+ currentTooltipView = nil
- if level == .detailed && !visibleStores.isEmpty {
- // 캐러셀에 모든 마커 정보 표시
- let effectiveViewport = self.getEffectiveViewport()
- let effectiveStores = visibleStores.filter { store in
- effectiveViewport.contains(CLLocationCoordinate2D(
- latitude: store.latitude,
- longitude: store.longitude
- ))
- }
+ if let previousMarker = currentMarker {
+ updateMarkerStyle(marker: previousMarker, selected: false, isCluster: false)
+ }
- self.currentCarouselStores = visibleStores
- self.carouselView.updateCards(visibleStores)
- self.carouselView.isHidden = false
- self.mainView.setStoreCardHidden(false, animated: true)
+ updateMarkerStyle(marker: marker, selected: true, isCluster: false, count: storeArray.count)
+ currentMarker = marker
- // 현재 선택된 마커가 있으면 해당 위치로 스크롤
- 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
+ currentCarouselStores = storeArray
+ carouselView.updateCards(storeArray)
+ carouselView.isHidden = false
+ carouselView.scrollToCard(index: 0)
- // 첫 번째 스토어의 마커를 선택 상태로 설정
- 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
- }
+ mainView.setStoreCardHidden(false, animated: true)
- 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)
+ // 지도 이동
+ let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position)
+ cameraUpdate.animation = .easeIn
+ cameraUpdate.animationDuration = 0.3
+ mainView.mapView.moveCamera(cameraUpdate)
- if level == .detailed && visibleStores.isEmpty {
- // 개별 마커 레벨인데 마커가 없는 경우 토스트 표시
- self.showNoMarkersToast()
- }
+ // 툴팁 생성
+ 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
}
+ }
- 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
- )
+ return true
}
- 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 showNoMarkersToast() {
+ // 디자인 예정이므로 임시 구현
+ Logger.log(message: "현재 지도 영역에 표시할 마커가 없습니다", category: .debug)
}
}
+ // MARK: - CLLocationManagerDelegate
+ extension MapViewController {
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ guard let location = locations.last else { return }
- 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()
+ currentMarker?.mapView = nil
+ currentMarker = nil
+ carouselView.isHidden = true
+ currentCarouselStores = []
- for marker in clusterMarkerDictionary.values {
- marker.map = nil
- }
- clusterMarkerDictionary.removeAll()
+ 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)
+ }
- for store in stores {
- addMarker(for: store)
+ locationManager.stopUpdatingLocation()
}
}
+ // MARK: - NMFMapViewTouchDelegate
+ extension MapViewController {
+ // 마커 탭 이벤트 처리
+ // 마커 탭 이벤트 처리
+ func mapView(_ mapView: NMFMapView, didTap marker: NMFMarker) -> Bool {
+ Logger.log(message: "didTapMarker 호출됨: \(marker.position), userInfo: \(marker.userInfo)", category: .debug)
+
+ // 클러스터 마커 확인
+ if let clusterData = marker.userInfo["clusterData"] as? ClusterMarkerData {
+ Logger.log(message: "클러스터 데이터 감지: \(clusterData.cluster.name), 스토어 수: \(clusterData.storeCount)", category: .debug)
+ return handleRegionalClusterTap(marker, clusterData: clusterData)
+ }
+ // 마이크로 클러스터 또는 단일 스토어 마커 확인
+ else if let storeArray = marker.userInfo["storeData"] as? [MapPopUpStore] {
+ if storeArray.count > 1 {
+ Logger.log(message: "마이크로 클러스터 감지: \(storeArray.count)개 스토어", category: .debug)
+ return handleMicroClusterTap(marker, storeArray: storeArray)
+ } else if let singleStore = storeArray.first {
+ Logger.log(message: "단일 스토어 감지: \(singleStore.name)", category: .debug)
+ return handleSingleStoreTap(marker, store: singleStore)
+ }
+ }
+ // 단일 스토어 마커 (배열이 아닌 경우) 확인
+ else if let singleStore = marker.userInfo["storeData"] as? MapPopUpStore {
+ Logger.log(message: "단일 스토어 감지: \(singleStore.name)", category: .debug)
+ return handleSingleStoreTap(marker, store: singleStore)
+ }
+ Logger.log(message: "인식할 수 없는 마커 타입", category: .error)
+ return false
+ }
-private func handleMarkerTap(_ marker: GMSMarker) -> Bool {
- isMovingToMarker = true
+ // 지도 탭 이벤트 처리
+ func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) {
+ guard !isMovingToMarker else { return }
- 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)
+ // 선택된 마커 초기화
+ if let currentMarker = currentMarker {
+ updateMarkerStyle(marker: currentMarker, selected: false, isCluster: false)
+ self.currentMarker = nil
+ }
- let camera = GMSCameraPosition(target: marker.position, zoom: newZoom)
- mainView.mapView.animate(to: camera)
+ // 툴팁 제거
+ currentTooltipView?.removeFromSuperview()
+ currentTooltipView = nil
+ currentTooltipStores = []
+ currentTooltipCoordinate = nil
- // 여러 스토어 캐러셀 업데이트
- let multiStores = clusterData.cluster.stores
- carouselView.updateCards(multiStores)
- carouselView.isHidden = multiStores.isEmpty
- currentCarouselStores = multiStores
- // 클러스터 마커 강조/해제 등 필요시 추가
+ // 캐러셀 초기화
+ carouselView.isHidden = true
+ carouselView.updateCards([])
+ self.currentCarouselStores = []
+ mainView.setStoreCardHidden(true, animated: true)
- return true
+ // 클러스터링 업데이트
+ updateMapWithClustering()
}
+ }
- // 2) 일반 마커일 때
- if let previousMarker = currentMarker {
- let markerView = MapMarker()
- markerView.injection(with: .init(isSelected: false, isCluster: false))
- previousMarker.iconView = markerView
+ // MARK: - NMFMapViewCameraDelegate
+ extension MapViewController {
+ // 카메라 이동 시작 시 호출
+ func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) {
+ if reason == NMFMapChangedByGesture && !isMovingToMarker {
+ resetSelectedMarker()
+ }
}
- // 새 마커 강조
- 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)
+ // 카메라 이동 중 호출
+ func mapView(_ mapView: NMFMapView, cameraIsChangingByReason reason: Int) {
+ if !isMovingToMarker {
+ currentTooltipView?.removeFromSuperview()
+ currentTooltipView = nil
+ currentTooltipStores = []
+ updateMapWithClustering()
+
+ // 캐러셀 초기화
+ carouselView.isHidden = true
+ carouselView.updateCards([])
+ currentCarouselStores = []
}
}
- 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
+ // 카메라 이동 완료 시 호출
+ 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()
}
- } 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)
+ self.isMovingToMarker = false
+
+ // 뷰포트 변경 이벤트 처리 - idleSubject 통해 알림
+ idleSubject.onNext(())
+
+ // 뷰포트 변경 이벤트 처리
+ if let reactor = self.reactor {
+ let bounds = self.getVisibleBounds()
+ reactor.action.onNext(.viewportChanged(
+ northEastLat: bounds.northEast.lat,
+ northEastLon: bounds.northEast.lng,
+ southWestLat: bounds.southWest.lat,
+ southWestLon: bounds.southWest.lng
+ ))
}
}
}
-}
-// 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
- }
+ // 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)
+ // 리스트뷰가 보일 때만 커스텀 탭 제스처 허용
+ 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
+ // 리스트뷰가 보이고 터치가 리스트뷰 위에 있으면 탭 처리하지 않음
+ if modalState != .bottom {
+ let listViewY = storeListViewController.view.frame.minY
+ if touchPoint.y > listViewY {
+ return false
+ }
}
- }
- return true
+ 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/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift
index 936b8848..a1c2bffe 100644
--- a/Poppool/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift
+++ b/Poppool/Poppool/Presentation/Map/MapView/MarkerTooltipView.swift
@@ -1,5 +1,5 @@
-import UIKit
import SnapKit
+import UIKit
final class MarkerTooltipView: UIView, UIGestureRecognizerDelegate {
diff --git a/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift b/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift
index f41f6569..f7d21f38 100644
--- a/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift
+++ b/Poppool/Poppool/Presentation/Map/MicroClusterMarkerView.swift
@@ -1,3 +1,2 @@
-
import Foundation
import UIKit
diff --git a/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift b/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift
index 842bc1de..5dd5201d 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListPanelLayout.swift
@@ -1,8 +1,8 @@
//
-//import FloatingPanel
-//import UIKit
+// import FloatingPanel
+// import UIKit
//
-//class StoreListPanelLayout: FloatingPanelLayout {
+// class StoreListPanelLayout: FloatingPanelLayout {
// let position: FloatingPanelPosition = .bottom
// let initialState: FloatingPanelState = .half
//
@@ -27,4 +27,4 @@
// func surfaceLayout(for size: CGSize) -> NSCollectionLayoutDimension {
// return .fractionalWidth(1.0)
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift
index e8e5e003..7b12cfb5 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListCell.swift
@@ -1,7 +1,7 @@
-import UIKit
-import SnapKit
-import RxSwift
import ReactorKit
+import RxSwift
+import SnapKit
+import UIKit
final class StoreListCell: UICollectionViewCell {
static let identifier = "StoreListCell"
@@ -48,7 +48,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/StoreListHeaderView.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift
index 7911caca..65f574b0 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListHeaderView.swift
@@ -1,10 +1,10 @@
//
-//import UIKit
-//import SnapKit
-//import RxSwift
+// import UIKit
+// import SnapKit
+// import RxSwift
//
//
-//class StoreListHeaderView: UICollectionReusableView {
+// class StoreListHeaderView: UICollectionReusableView {
// static let identifier = "StoreListHeaderView"
//
// let searchInput = MapSearchInput()
@@ -51,4 +51,4 @@
//
//
// }
-//}
+// }
diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift
index f5eee867..ab3b649d 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListPanelLayout.swift
@@ -20,9 +20,6 @@ class StoreListPanelLayout: FloatingPanelLayout {
]
}
-
-
-
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift
index 864d2426..21e54ec8 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListReactor.swift
@@ -1,8 +1,7 @@
-import ReactorKit
-import RxSwift
import Foundation
+import ReactorKit
import RxCocoa
-
+import RxSwift
final class StoreListReactor: Reactor {
// MARK: - Reactor
@@ -10,7 +9,6 @@ final class StoreListReactor: Reactor {
private let popUpAPIUseCase: PopUpAPIUseCaseImpl
private let bookmarkStateRelay = PublishRelay<(Int64, Bool)>()
-
// private var currentPage = 0
// private let pageSize = 10
// private var hasMorePages = true
@@ -25,7 +23,6 @@ final class StoreListReactor: Reactor {
case clearFilters(FilterType)
case updateStoreBookmark(id: Int64, isBookmarked: Bool) // 추가
-
}
enum Mutation {
@@ -59,7 +56,6 @@ final class StoreListReactor: Reactor {
self.initialState = State()
}
-
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -84,7 +80,7 @@ final class StoreListReactor: Reactor {
return .empty()
}
- let bookmarkRequest = popUpAPIUseCase.getPopUpDetail(
+ return popUpAPIUseCase.getPopUpDetail(
commentType: "NORMAL",
popUpStoredId: Int64(idInt32) // Int32 → Int64 변환
)
@@ -102,10 +98,6 @@ final class StoreListReactor: Reactor {
]))
}
- return bookmarkRequest
-
-
-
//
// case let .setStores(storeItems):
// return Observable.from(storeItems)
@@ -134,7 +126,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 +153,6 @@ final class StoreListReactor: Reactor {
}
return .empty()
-
case let .filterTapped(filterType):
return .just(.setActiveFilter(filterType))
@@ -182,7 +174,6 @@ final class StoreListReactor: Reactor {
}
}
-
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
@@ -206,10 +197,9 @@ final class StoreListReactor: Reactor {
)
}
-
case let .showBookmarkToast(isBookmarked):
if currentState.stores.isEmpty {
- break
+ break
}
newState.shouldShowBookmarkToast = isBookmarked
@@ -236,11 +226,8 @@ final class StoreListReactor: Reactor {
bookmarkStateRelay.accept((storeId, isBookmarked))
}
-
-
}
-
// MARK: - Model
struct StoreItem {
let id: Int64
diff --git a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift
index 63f88bc5..79f01619 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListView.swift
+++ b/Poppool/Poppool/Presentation/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/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift
index e8fb9c69..3a358ce8 100644
--- a/Poppool/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift
+++ b/Poppool/Poppool/Presentation/Map/StoreListView/StoreListViewController.swift
@@ -1,10 +1,10 @@
-import UIKit
-import SnapKit
-import RxCocoa
-import RxSwift
-import ReactorKit
import FloatingPanel
+import ReactorKit
+import RxCocoa
import RxDataSources
+import RxSwift
+import SnapKit
+import UIKit
final class StoreListViewController: UIViewController, View {
typealias Reactor = StoreListReactor
@@ -56,15 +56,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,
@@ -124,7 +125,6 @@ final class StoreListViewController: UIViewController, View {
})
.disposed(by: disposeBag)
-
// 4) viewWillAppear -> viewDidLoad
// rx.viewWillAppear
// .map { _ in Reactor.Action.viewDidLoad }
@@ -155,7 +155,7 @@ final class StoreListViewController: UIViewController, View {
// .compactMap { $0 }
// .bind(to: reactor.action)
// .disposed(by: disposeBag)
-
+
}
private func presentFilterBottomSheet(for filterType: FilterType) {
@@ -167,7 +167,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 +180,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/Map/TestViewController.swift b/Poppool/Poppool/Presentation/Map/TestViewController.swift
index 8a35c50d..82bd615b 100644
--- a/Poppool/Poppool/Presentation/Map/TestViewController.swift
+++ b/Poppool/Poppool/Presentation/Map/TestViewController.swift
@@ -7,87 +7,86 @@
import UIKit
-import SnapKit
-import RxSwift
-import RxGesture
import RxCocoa
+import RxGesture
+import RxSwift
+import SnapKit
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
+ return PPButton(style: .secondary, text: "리스트 버튼")
}()
-
+
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)
@@ -96,7 +95,7 @@ class TestViewController: UIViewController {
make.centerX.equalToSuperview()
}
}
-
+
func bind() {
listButton.rx.tap
.withUnretained(self)
@@ -109,11 +108,11 @@ class TestViewController: UIViewController {
}
}
.disposed(by: disposeBag)
-
+
gestureBar.rx.swipeGesture(.up)
.skip(1)
.withUnretained(self)
- .subscribe { (owner, gesture) in
+ .subscribe { (owner, _) in
print("swipe up")
UIView.animate(withDuration: 0.3) {
owner.bottomViewTopConstraints?.update(offset: 0)
@@ -123,11 +122,11 @@ class TestViewController: UIViewController {
}
}
.disposed(by: disposeBag)
-
+
gestureBar.rx.swipeGesture(.down)
.skip(1)
.withUnretained(self)
- .subscribe { (owner, gesture) in
+ .subscribe { (owner, _) in
print("swipe down")
switch owner.modalState {
case .top:
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift b/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift
index 3bee6c71..f1e20e9f 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+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 +48,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/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift
index 4afbec9d..9653a396 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckReactor.swift
+++ b/Poppool/Poppool/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/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift
index 51d6458e..c16fb508 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentCheck/CommentCheckView.swift
@@ -10,42 +10,39 @@ import UIKit
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 +50,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/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift
index e4fb63a0..49d47a25 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailController.swift
@@ -7,23 +7,23 @@
import UIKit
-import SnapKit
+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 +47,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)
@@ -72,7 +72,7 @@ extension CommentDetailController {
.map { Reactor.Action.viewWillAppear }
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
cellTapped
.withUnretained(self)
.map { (owner, indexPath) in
@@ -80,19 +80,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 +112,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 +138,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/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift
index 978cb6bf..8b7d28f3 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/CommentDetailReactor.swift
@@ -8,36 +8,36 @@
import UIKit
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()))
-
+
lazy var compositionalLayout: UICollectionViewCompositionalLayout = {
UICollectionViewCompositionalLayout { [weak self] section, env in
guard let self = self else {
@@ -51,17 +51,17 @@ 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) {
self.initialState = State(commentData: comment)
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -73,7 +73,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 +93,26 @@ final class CommentDetailReactor: Reactor {
if newState.commentData.isLike {
newState.commentData.likeCount += 1
userAPIUseCase.postCommentLike(commentId: newState.commentData.commentID)
- .subscribe(onDisposed: {
+ .subscribe(onDisposed: {
Logger.log(message: "CommentLike", category: .info)
})
.disposed(by: disposeBag)
} else {
newState.commentData.likeCount -= 1
userAPIUseCase.deleteCommentLike(commentId: newState.commentData.commentID)
- .subscribe(onDisposed: {
+ .subscribe(onDisposed: {
Logger.log(message: "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/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift
index 1bb2c50e..b127b716 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift
index 17650a73..43237524 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailContentSection/CommentDetailContentSectionCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class CommentDetailContentSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
private let contentLabel: PPLabel = {
@@ -19,15 +19,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 +47,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/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift
index a2644557..a3c61126 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailImageSection.swift
@@ -10,17 +10,17 @@ import UIKit
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/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift
index 6bf879f4..c75acd0f 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentDetail/View/CommentDetailView.swift
@@ -10,42 +10,40 @@ import UIKit
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 +51,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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift
index 3dcc05c4..088653fe 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+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 +50,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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift
index 25324ff7..caebe2ca 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuReactor.swift
+++ b/Poppool/Poppool/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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift
index 59563034..a6034782 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentMyMenu/CommentMyMenuView.swift
@@ -10,50 +10,50 @@ import UIKit
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 +61,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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift
index 8c0ffa1d..41bbe402 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+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 +50,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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift
index 9b12bdb6..76562c2b 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoReactor.swift
+++ b/Poppool/Poppool/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/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift
index 25356672..f3a6927e 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentInfoMenu/CommentUserInfo/CommentUserInfoView.swift
@@ -10,50 +10,50 @@ import UIKit
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 +61,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/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift
index 14a2f784..a11d2f53 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListController.swift
@@ -7,18 +7,18 @@
import UIKit
-import SnapKit
+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 +30,7 @@ extension CommentListController {
super.viewDidLoad()
setUp()
}
-
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
@@ -58,18 +58,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 +77,7 @@ extension CommentListController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
reactor.state
.withUnretained(self)
.subscribe { (owner, state) in
@@ -85,7 +85,7 @@ extension CommentListController {
if state.isReloadView { owner.mainView.contentCollectionView.reloadData()}
}
.disposed(by: disposeBag)
-
+
}
}
@@ -94,11 +94,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 +113,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 +121,7 @@ extension CommentListController: UICollectionViewDelegate, UICollectionViewDataS
}
.bind(to: reactor.action)
.disposed(by: cell.disposeBag)
-
+
cell.totalViewButton.rx.tap
.withUnretained(self)
.map { (owner, _) in
@@ -129,7 +129,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 +137,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/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift
index d62b4b1c..3823d183 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/CommentListReactor.swift
@@ -8,11 +8,11 @@
import UIKit
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
final class CommentListReactor: Reactor {
-
+
// MARK: - Reactor
enum Action {
case viewWillAppear
@@ -24,7 +24,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 +33,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()))
-
+
lazy var compositionalLayout: UICollectionViewCompositionalLayout = {
UICollectionViewCompositionalLayout { [weak self] section, env in
guard let self = self else {
@@ -66,10 +66,10 @@ 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
@@ -78,7 +78,7 @@ final class CommentListReactor: Reactor {
self.popUpID = popUpID
self.popUpName = popUpName
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -173,7 +173,7 @@ final class CommentListReactor: Reactor {
return Observable.just(.loadView)
}
}
-
+
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
newState.isReloadView = false
@@ -207,11 +207,11 @@ final class CommentListReactor: Reactor {
} else {
showOtherUserCommentMenu(controller: controller, comment: comment)
}
-
+
}
return newState
}
-
+
func getSection() -> [any Sectionable] {
return [
spacing24Section,
@@ -220,7 +220,7 @@ final class CommentListReactor: Reactor {
commentSection
]
}
-
+
func showOtherUserCommentMenu(controller: BaseViewController, comment: DetailCommentSection.CellType.Input) {
let nextController = CommentUserInfoController()
nextController.reactor = CommentUserInfoReactor(nickName: comment.nickName)
@@ -250,7 +250,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 +268,13 @@ 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,7 +287,7 @@ 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 {
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift
index 18057b37..74440198 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSection.swift
@@ -5,23 +5,22 @@
// Created by SeoJunYoung on 12/25/24.
//
-
import UIKit
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 +37,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/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift
index 708c4b59..da43fc9a 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListTitleSection/CommentListTitleSectionCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class CommentListTitleSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
private let countLabel: PPLabel = {
@@ -21,12 +21,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 +47,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/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift
index faa57203..66c5c957 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentList/View/CommentListView.swift
@@ -10,24 +10,22 @@ import UIKit
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 +33,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/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift
index 603c2dcd..0756496e 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+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 +50,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/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift
index 9ca6bd7a..271a84b6 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedReactor.swift
+++ b/Poppool/Poppool/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/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift
index 9ca4bc6e..e77d939b 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentSelected/CommentSelectedView.swift
@@ -10,50 +10,50 @@ import UIKit
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 +61,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/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift
index a62c93ce..75166c4d 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+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 +48,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/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift
index 7400fa39..72c9b9fa 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockReactor.swift
+++ b/Poppool/Poppool/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/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift
index 1906ff04..3ca9705b 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/CommentUserBlock/CommentUserBlockView.swift
@@ -10,43 +10,40 @@ import UIKit
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 +51,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/InstaComment/InstaCommentAddController.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift
index 7318d844..68c6327b 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddController.swift
@@ -7,19 +7,19 @@
import UIKit
-import SnapKit
+import ReactorKit
import RxCocoa
import RxSwift
-import ReactorKit
+import SnapKit
import SwiftSoup
final class InstaCommentAddController: BaseViewController, View {
-
+
typealias Reactor = InstaCommentAddReactor
-
+
// MARK: - Properties
var disposeBag = DisposeBag()
-
+
private var mainView = InstaCommentAddView()
private var sections: [any Sectionable] = []
}
@@ -52,7 +52,7 @@ private extension InstaCommentAddController {
// MARK: - Methods
extension InstaCommentAddController {
func bind(reactor: Reactor) {
-
+
SceneDelegate.appDidBecomeActive
.subscribe { _ in
if let url = UIPasteboard.general.string {
@@ -64,17 +64,17 @@ extension InstaCommentAddController {
}
}
.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
@@ -90,18 +90,18 @@ extension InstaCommentAddController: 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 }
-
+
return cell
}
}
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift
index 6546f5d2..c9d0e127 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/InstaCommentAddReactor.swift
@@ -8,31 +8,31 @@
import UIKit
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
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 {
@@ -46,7 +46,7 @@ final class InstaCommentAddReactor: Reactor {
return getSection()[section].getSection(section: section, env: env)
}
}()
-
+
private let guideSection = InstaGuideSection(inputDataList: [
.init(
imageList: [
@@ -59,7 +59,7 @@ final class InstaCommentAddReactor: Reactor {
{
let title = "아래 인스타그램 열기\n버튼을 터치해 앱 열기"
let attributedTitle = NSMutableAttributedString(string: title)
- let koreanFont = UIFont.KorFont(style: .bold, size: 20)!
+ 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()
@@ -70,7 +70,7 @@ final class InstaCommentAddReactor: Reactor {
{
let title = "원하는 피드의 이미지로 이동 후\n공유하기 > 링크복사 터치하기"
let attributedTitle = NSMutableAttributedString(string: title)
- let koreanFont = UIFont.KorFont(style: .bold, size: 20)!
+ 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()
@@ -81,7 +81,7 @@ final class InstaCommentAddReactor: Reactor {
{
let title = "아래 이미지 영역을 터치해\n팝풀 앱으로 돌아오기"
let attributedTitle = NSMutableAttributedString(string: title)
- let koreanFont = UIFont.KorFont(style: .bold, size: 20)!
+ 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()
@@ -92,7 +92,7 @@ final class InstaCommentAddReactor: Reactor {
{
let title = "복사된 인스타 피드 이미지와\n함께할 글을 입력 후 등록하기"
let attributedTitle = NSMutableAttributedString(string: title)
- let koreanFont = UIFont.KorFont(style: .bold, size: 20)!
+ 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()
@@ -103,12 +103,12 @@ final class InstaCommentAddReactor: Reactor {
]
)
])
-
+
// MARK: - init
init() {
self.initialState = State()
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -118,7 +118,7 @@ final class InstaCommentAddReactor: Reactor {
return Observable.just(.moveToInsta)
}
}
-
+
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
@@ -126,21 +126,21 @@ final class InstaCommentAddReactor: Reactor {
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)
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift
index daf48dfe..c7c9faa0 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaCommentAddView.swift
@@ -10,14 +10,14 @@ 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))
+ view.headerLabel.setLineHeightText(text: "코멘트 작성하기", font: .korFont(style: .regular, size: 15))
return view
}()
-
+
let instaButton: UIButton = {
let button = UIButton()
button.setTitleColor(.white, for: .normal)
@@ -26,36 +26,36 @@ final class InstaCommentAddView: UIView {
let title = "Instagram 열기"
let attributedTitle = NSMutableAttributedString(string: title)
-
- let englishFont = UIFont.EngFont(style: .medium, size: 15)!
+
+ 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)!
+ 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")
}
@@ -63,26 +63,26 @@ final class InstaCommentAddView: UIView {
// 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)
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
index 77095f97..cb88aa60 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSection.swift
@@ -10,17 +10,17 @@ 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),
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
index 46596746..5a4bff39 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideChildSection/InstaGuideChildSectionCell.swift
@@ -7,15 +7,15 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class InstaGuideChildSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
let disposeBag = DisposeBag()
-
+
private let indexTrailgView: UIView = {
let view = UIView()
view.backgroundColor = .g900
@@ -23,35 +23,35 @@ final class InstaGuideChildSectionCell: UICollectionViewCell {
view.layer.cornerRadius = 4
return view
}()
-
+
private let indexLabel: UILabel = {
let label = UILabel()
- label.font = .EngFont(style: .medium, size: 16)
+ 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()
}
@@ -67,12 +67,12 @@ private extension InstaGuideChildSectionCell {
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)
@@ -94,7 +94,7 @@ extension InstaGuideChildSectionCell: Inputable {
var title: NSMutableAttributedString?
var index: Int
}
-
+
func injection(with input: Input) {
indexLabel.text = "#\(input.index + 1)"
titleLabel.attributedText = input.title
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
index 98801b83..13bfca92 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSection.swift
@@ -10,17 +10,17 @@ 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),
@@ -35,8 +35,7 @@ struct InstaGuideSection: 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/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift
index aed527cb..d3c3371e 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/InstaComment/View/InstaGuideSection/InstaGuideSection/InstaGuideSectionCell.swift
@@ -7,24 +7,24 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
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
@@ -36,17 +36,17 @@ final class InstaGuideSectionCell: UICollectionViewCell {
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 {
@@ -60,25 +60,25 @@ final class InstaGuideSectionCell: UICollectionViewCell {
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()
}
@@ -89,7 +89,7 @@ private extension InstaGuideSectionCell {
func setUp() {
contentCollectionView.delegate = self
contentCollectionView.dataSource = self
-
+
contentCollectionView.register(
InstaGuideChildSectionCell.self,
forCellWithReuseIdentifier: InstaGuideChildSectionCell.identifiers
@@ -101,21 +101,21 @@ private extension InstaGuideSectionCell {
}
.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)
@@ -123,11 +123,11 @@ private extension InstaGuideSectionCell {
make.leading.equalTo(pageControl.snp.trailing).offset(-36)
}
}
-
+
func getSection() -> [any Sectionable] {
return [imageSection]
}
-
+
func startAutoScroll(interval: TimeInterval = 3.0) {
stopAutoScroll() // 기존 타이머를 중지
isAutoBannerPlay = true
@@ -157,7 +157,7 @@ private extension InstaGuideSectionCell {
contentCollectionView.scrollToItem(at: nextIndex, at: .centeredHorizontally, animated: true)
pageControl.currentPage = nextIndex.item
}
-
+
func bind() {
stopButton.rx.tap
.withUnretained(self)
@@ -177,7 +177,7 @@ extension InstaGuideSectionCell: Inputable {
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 }
@@ -189,19 +189,18 @@ extension InstaGuideSectionCell: Inputable {
// 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
+ return getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath)
}
}
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift
index b14cf4de..c2cf2ca4 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddController.swift
@@ -7,23 +7,23 @@
import UIKit
-import SnapKit
-import RxCocoa
-import RxSwift
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 +33,7 @@ extension NormalCommentAddController {
super.viewDidLoad()
setUp()
}
-
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
@@ -79,12 +79,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 +92,7 @@ extension NormalCommentAddController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
// RxKeyboard로 키보드 높이 감지
RxKeyboard.instance.visibleHeight
.skip(1)
@@ -103,7 +103,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 +111,7 @@ extension NormalCommentAddController {
}
})
.disposed(by: keyBoardDisposeBag)
-
+
mainView.saveButton.rx.tap
.withUnretained(self)
.map { (owner, _) in
@@ -120,7 +120,7 @@ extension NormalCommentAddController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
reactor.state
.withUnretained(self)
.subscribe { (owner, state) in
@@ -136,7 +136,7 @@ extension NormalCommentAddController {
}
owner.sections = state.sections
if state.isReloadView { owner.mainView.contentCollectionView.reloadData() }
-
+
}
.disposed(by: disposeBag)
}
@@ -147,11 +147,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 +164,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift
index 04bb9027..63b51884 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/NormalCommentAddReactor.swift
@@ -5,15 +5,15 @@
// Created by SeoJunYoung on 12/14/24.
//
-import UIKit
import PhotosUI
+import UIKit
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
final class NormalCommentAddReactor: Reactor {
-
+
// MARK: - Reactor
enum Action {
case viewWillAppear
@@ -24,7 +24,7 @@ final class NormalCommentAddReactor: Reactor {
case inputComment(text: String?)
case saveButtonTapped(controller: BaseViewController)
}
-
+
enum Mutation {
case loadView
case showImagePicker(controller: BaseViewController)
@@ -32,24 +32,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()
-
+
lazy var compositionalLayout: UICollectionViewCompositionalLayout = {
UICollectionViewCompositionalLayout { [weak self] section, env in
guard let self = self else {
@@ -79,7 +79,7 @@ final class NormalCommentAddReactor: Reactor {
self.popUpID = popUpID
self.popUpName = popUpName
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -102,7 +102,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,7 +154,7 @@ 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 }
@@ -170,11 +170,11 @@ final class NormalCommentAddReactor: Reactor {
})
.disposed(by: disposeBag)
}
-
+
}
return newState
}
-
+
func getSection() -> [any Sectionable] {
return [
spacing25Section,
@@ -199,19 +199,19 @@ 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 {
@@ -222,7 +222,7 @@ extension NormalCommentAddReactor: PHPickerViewControllerDelegate {
Logger.log(message: "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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift
index 3fb3c140..b7559602 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +36,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift
index e83fe204..dd8cd85f 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentDescriptionSection/AddCommentDescriptionSectionCell.swift
@@ -7,15 +7,15 @@
import UIKit
-import SnapKit
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 +23,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 +48,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift
index 09d0822d..f1df7177 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSection.swift
@@ -10,17 +10,17 @@ import UIKit
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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift
index d9b4d329..dc954aa0 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentImageSection/AddCommentImageSectionCell.swift
@@ -7,20 +7,19 @@
import UIKit
-import SnapKit
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 +27,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 +57,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 +85,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift
index 051d1370..9ef3468c 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSection.swift
@@ -10,17 +10,17 @@ import UIKit
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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift
index 8bc041f7..b81b98f3 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentSection/AddCommentSectionCell.swift
@@ -7,59 +7,59 @@
import UIKit
-import SnapKit
-import RxSwift
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 +70,7 @@ final class AddCommentSectionCell: UICollectionViewCell {
// MARK: - SetUp
private extension AddCommentSectionCell {
func bind() {
-
+
commentTextView.rx.didBeginEditing
.withUnretained(self)
.subscribe { (owner, _) in
@@ -78,7 +78,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 +86,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 +94,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 +109,7 @@ private extension AddCommentSectionCell {
}
.disposed(by: disposeBag)
}
-
+
func setUpConstraints() {
contentView.layer.cornerRadius = 4
contentView.clipsToBounds = true
@@ -136,13 +136,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 +158,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 +174,7 @@ enum CommentState {
case longLengthActive
case normal
case normalActive
-
+
var borderColor: UIColor? {
switch self {
case .shortLength, .longLength, .longLengthActive:
@@ -183,7 +183,7 @@ enum CommentState {
return .g100
}
}
-
+
var countLabelColor: UIColor? {
switch self {
case .shortLength, .longLength, .longLengthActive:
@@ -192,7 +192,7 @@ enum CommentState {
return .g500
}
}
-
+
var textColor: UIColor? {
switch self {
case .shortLength, .longLength, .longLengthActive:
@@ -201,7 +201,7 @@ enum CommentState {
return .g1000
}
}
-
+
var description: String? {
switch self {
case .longLength, .longLengthActive:
@@ -212,7 +212,7 @@ enum CommentState {
return nil
}
}
-
+
var isHiddenNoticeLabel: Bool {
switch self {
case .longLength, .longLengthActive, .shortLength:
@@ -221,7 +221,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift
index 3a50e804..d8e99796 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +36,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift
index 8c7ad96d..b5847ee0 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/AddCommentTitleSection/AddCommentTitleSectionCell.swift
@@ -7,26 +7,25 @@
import UIKit
-import SnapKit
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 +45,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/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift
index c5030a02..819e9200 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentAdd/View/NormalCommentAddView.swift
@@ -10,33 +10,32 @@ import UIKit
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 +43,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/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift
index 84732cc8..e451afc4 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditController.swift
@@ -7,23 +7,23 @@
import UIKit
-import SnapKit
-import RxCocoa
-import RxSwift
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 +33,7 @@ extension NormalCommentEditController {
super.viewDidLoad()
setUp()
}
-
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
@@ -79,12 +79,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 +92,7 @@ extension NormalCommentEditController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
// RxKeyboard로 키보드 높이 감지
RxKeyboard.instance.visibleHeight
.skip(1)
@@ -103,7 +103,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 +111,7 @@ extension NormalCommentEditController {
}
})
.disposed(by: keyBoardDisposeBag)
-
+
mainView.saveButton.rx.tap
.withUnretained(self)
.map { (owner, _) in
@@ -120,7 +120,7 @@ extension NormalCommentEditController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
reactor.state
.withUnretained(self)
.subscribe { (owner, state) in
@@ -136,17 +136,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 +157,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 +174,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/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift
index 437a7046..dee4c3cc 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditReactor.swift
@@ -5,15 +5,15 @@
// Created by SeoJunYoung on 2/1/25.
//
-import UIKit
import PhotosUI
+import UIKit
import ReactorKit
-import RxSwift
import RxCocoa
+import RxSwift
final class NormalCommentEditReactor: Reactor {
-
+
// MARK: - Reactor
enum Action {
case viewWillAppear
@@ -24,7 +24,7 @@ final class NormalCommentEditReactor: Reactor {
case inputComment(text: String?)
case saveButtonTapped(controller: BaseViewController)
}
-
+
enum Mutation {
case loadView
case showImagePicker(controller: BaseViewController)
@@ -32,25 +32,25 @@ 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
}
-
+
// 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()
-
+
lazy var compositionalLayout: UICollectionViewCompositionalLayout = {
UICollectionViewCompositionalLayout { [weak self] section, env in
guard let self = self else {
@@ -74,7 +74,7 @@ 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) {
self.initialState = State(text: comment.comment)
@@ -86,7 +86,7 @@ final class NormalCommentEditReactor: Reactor {
.init(image: nil, isFirstCell: false, isEditCase: true, imageURL: url, imageID: id)
}))
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -108,7 +108,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,26 +147,26 @@ 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")}
-
+
if !addImages.isEmpty {
imageService.tryUpload(datas: addImages.map { .init(filePath: pathList[$0.offset], image: $0.element)})
.subscribe { [weak self] _ in
@@ -208,7 +208,7 @@ final class NormalCommentEditReactor: Reactor {
}
return newState
}
-
+
func getSection() -> [any Sectionable] {
return [
spacing25Section,
@@ -233,19 +233,19 @@ 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 {
@@ -256,7 +256,7 @@ extension NormalCommentEditReactor: PHPickerViewControllerDelegate {
Logger.log(message: "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/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift
index 544e47aa..ee06753a 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/NormalCommentEdit/NormalCommentEditView.swift
@@ -10,33 +10,32 @@ import UIKit
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 +43,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/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift
index 2665a516..cbe375f7 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentController.swift
@@ -7,22 +7,22 @@
import UIKit
-import SnapKit
+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 +31,9 @@ extension OtherUserCommentController {
override func viewDidLoad() {
super.viewDidLoad()
setUp()
-
+
}
-
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
@@ -48,13 +48,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 +62,7 @@ private extension OtherUserCommentController {
mainView.contentCollectionView.register(
SpacingSectionCell.self,
forCellWithReuseIdentifier: SpacingSectionCell.identifiers
- )
+ )
mainView.contentCollectionView.register(
MyCommentedPopUpGridSectionCell.self,
forCellWithReuseIdentifier: MyCommentedPopUpGridSectionCell.identifiers
@@ -77,7 +77,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 +85,7 @@ extension OtherUserCommentController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
cellTapped
.withUnretained(self)
.map { (owner, row) in
@@ -93,7 +93,7 @@ extension OtherUserCommentController {
}
.bind(to: reactor.action)
.disposed(by: disposeBag)
-
+
reactor.state
.withUnretained(self)
.subscribe { (owner, state) in
@@ -109,19 +109,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/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift
index af4281b6..bc2de562 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/OtherUserCommentReactor.swift
@@ -8,38 +8,38 @@
import UIKit
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()))
-
+
lazy var compositionalLayout: UICollectionViewCompositionalLayout = {
UICollectionViewCompositionalLayout { [weak self] section, env in
guard let self = self else {
@@ -56,13 +56,13 @@ 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?) {
self.initialState = State()
self.commenterID = commenterID
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -88,7 +88,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
@@ -108,7 +108,7 @@ final class OtherUserCommentReactor: Reactor {
}
return newState
}
-
+
func getSection() -> [any Sectionable] {
return [
spacing16Section,
diff --git a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift
index 68cacc55..42954f36 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSection.swift
@@ -10,17 +10,17 @@ import UIKit
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/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift
index 2640bfa9..7f1a0770 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentSection/OtherUserCommentSectionCell.swift
@@ -7,56 +7,55 @@
import UIKit
-import SnapKit
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 +76,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 +118,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/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift
index e6d99298..0cd89426 100644
--- a/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Comment/OtherUserComment/View/OtherUserCommentView.swift
@@ -10,26 +10,26 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/DetailController.swift
index 75cfd69c..ee001146 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/DetailController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/DetailController.swift
@@ -1,16 +1,9 @@
-//
-// DetailController.swift
-// Poppool
-//
-// Created by SeoJunYoung on 12/9/24.
-//
-
import UIKit
-import SnapKit
+import ReactorKit
import RxCocoa
import RxSwift
-import ReactorKit
+import SnapKit
final class DetailController: BaseViewController, View {
@@ -52,7 +45,6 @@ extension DetailController {
tabBarController?.tabBar.isHidden = false
}
-
}
// MARK: - SetUp
@@ -205,12 +197,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 +224,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 +258,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/Poppool/Presentation/Scene/Detail/DetailReactor.swift
index 165a8b7a..964cdb5f 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/DetailReactor.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/DetailReactor.swift
@@ -1,19 +1,12 @@
-//
-// DetailReactor.swift
-// Poppool
-//
-// Created by SeoJunYoung on 12/9/24.
-//
-
import UIKit
+import LinkPresentation
import ReactorKit
-import RxSwift
import RxCocoa
-import LinkPresentation
+import RxSwift
final class DetailReactor: Reactor {
-
+
// MARK: - Reactor
enum Action {
case viewWillAppear
@@ -26,12 +19,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,26 +37,26 @@ 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()))
@@ -81,7 +74,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: [])
@@ -91,8 +84,7 @@ final class DetailReactor: Reactor {
private var commentEmptySection = DetailEmptyCommetSection(inputDataList: [.init()])
private var similarTitleSecion = SearchTitleSection(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)])
@@ -106,7 +98,7 @@ final class DetailReactor: Reactor {
self.popUpID = popUpID
self.initialState = State()
}
-
+
// MARK: - Reactor Methods
func mutate(action: Action) -> Observable {
switch action {
@@ -136,11 +128,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 {
@@ -176,9 +168,6 @@ final class DetailReactor: Reactor {
mapGuideController.modalTransitionStyle = .coverVertical
controller.present(mapGuideController, animated: true)
-
-
-
case .moveToCommentTotalScene(let controller):
if isLogin {
let nextController = CommentListController()
@@ -216,8 +205,8 @@ final class DetailReactor: Reactor {
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 +214,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 +300,8 @@ final class DetailReactor: Reactor {
]
}
}
-
}
-
+
func setContent() -> Observable {
return popUpAPIUseCase.getPopUpDetail(commentType: "NORMAL", popUpStoredId: popUpID, isViewCount: isFirstRequest)
.withUnretained(self)
@@ -325,7 +316,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 +353,7 @@ final class DetailReactor: Reactor {
return .loadView
}
}
-
+
func bookMark() -> Observable {
if let isBookMark = titleSection.inputDataList.first?.isBookMark {
titleSection.inputDataList[0].isBookMark.toggle()
@@ -378,32 +369,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 = KeyPath.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)
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)
return
}
-
+
guard let data = data, let image = UIImage(data: data) else {
Logger.log(message: "이미지 변환 실패", category: .error)
return
}
-
+
Logger.log(message: "이미지 다운로드 성공", category: .info)
-
+
let sharedText = "[팝풀] \(storeName) 팝업 어때요?\n지금 바로 팝풀에서 확인해보세요!"
// UI 업데이트는 메인 스레드에서 실행
DispatchQueue.main.async {
@@ -414,10 +405,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 +423,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)
@@ -462,7 +453,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 +471,13 @@ 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,7 +490,7 @@ 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 {
@@ -527,7 +518,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 +526,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/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift
index b94dfefb..514b9bf9 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentImageCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class DetailCommentImageCell: UICollectionViewCell {
-
+
// MARK: - Components
private let imageView: UIImageView = {
let view = UIImageView()
@@ -19,14 +19,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 +46,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/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift
index fdbada94..44ec4a67 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentProfileView.swift
@@ -10,7 +10,7 @@ import UIKit
import SnapKit
final class DetailCommentProfileView: UIStackView {
-
+
// MARK: - Components
let profileImageView: UIImageView = {
let view = UIImageView()
@@ -19,47 +19,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/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift
index ada32f6b..4f08bab2 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift
index 6df1feb5..4251a119 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentSection/DetailCommentSectionCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class DetailCommentSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
private let contentStackView: UIStackView = {
let view = UIStackView()
@@ -20,12 +20,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 +35,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 +92,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 +102,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 +140,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 +159,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 +174,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 +207,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 +216,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 +277,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 +304,7 @@ extension DetailCommentSectionCell: Inputable {
imageCollectionView.isHidden = false
imagePathList = input.imageList
}
-
+
imageCollectionView.reloadData()
if input.isLogin {
blurBackGroundView.isHidden = true
@@ -327,23 +323,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 +349,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 +363,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/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift
index d1a25558..d2979800 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift
index a285a972..f7c37325 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailCommentTitleSection/DetailCommentTitleSectionCell.swift
@@ -7,29 +7,28 @@
import UIKit
-import SnapKit
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 +37,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 +60,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 +80,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/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift
index 24a4e9d4..8d9c926c 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift
index aa7c0f73..02e98e1c 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailContentSection/DetailContentSectionCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class DetailContentSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
private let contentStackView: UIStackView = {
@@ -26,38 +26,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 +96,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/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift
index 45ae3913..3cda98e4 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift
index a5a1e031..76e43892 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailEmptyCommetSection/DetailEmptyCommetSectionCell.swift
@@ -7,46 +7,46 @@
import UIKit
-import SnapKit
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 +61,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 +74,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/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift
index d21f060a..f869c617 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift
index e509e3c4..2a81ca35 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailInfoSection/DetailInfoSectionCell.swift
@@ -7,79 +7,73 @@
import UIKit
-import SnapKit
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 +88,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 +125,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 +139,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 +158,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/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift
index 664f296e..c62203f8 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +39,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/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift
index d746d9ae..3ca56575 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailSimilarSection/DetailSimilarSectionCell.swift
@@ -7,11 +7,11 @@
import UIKit
-import SnapKit
import RxSwift
+import SnapKit
final class DetailSimilarSectionCell: UICollectionViewCell {
-
+
// MARK: - Components
private let imageView: UIImageView = {
let view = UIImageView()
@@ -19,28 +19,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 +50,11 @@ final class DetailSimilarSectionCell: UICollectionViewCell {
addHolesToCell()
}
-
+
required init?(coder: NSCoder) {
fatalError()
}
-
-
+
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
@@ -65,27 +62,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 +103,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 +138,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/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift
index f99f45b0..8b2b7ee2 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSection.swift
@@ -10,17 +10,17 @@ import UIKit
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 +37,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/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift
index b2a86cce..907df869 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailTitleSection/DetailTitleSectionCell.swift
@@ -7,41 +7,40 @@
import UIKit
-import SnapKit
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 +55,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 +78,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/Poppool/Presentation/Scene/Detail/View/DetailView.swift
index a79b9333..0e286f01 100644
--- a/Poppool/Poppool/Presentation/Scene/Detail/View/DetailView.swift
+++ b/Poppool/Poppool/Presentation/Scene/Detail/View/DetailView.swift
@@ -10,19 +10,18 @@ import UIKit
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 +38,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 +52,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/Home/List/HomeListController.swift b/Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift
index ce91a1f1..8fe382bf 100644
--- a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift
+++ b/Poppool/Poppool/Presentation/Scene/Home/List/HomeListController.swift
@@ -7,24 +7,24 @@
import UIKit
-import SnapKit
+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