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 = .init() } @@ -33,9 +33,9 @@ extension HomeListController { override func viewDidLoad() { super.viewDidLoad() setUp() - + } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -49,13 +49,13 @@ private extension HomeListController { mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) } - + if let layout = reactor?.compositionalLayout { mainView.contentCollectionView.collectionViewLayout = layout } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( HomeCardSectionCell.self, forCellWithReuseIdentifier: HomeCardSectionCell.identifiers @@ -74,7 +74,7 @@ extension HomeListController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -82,13 +82,13 @@ extension HomeListController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + pageChange .throttle(.milliseconds(1000), scheduler: MainScheduler.asyncInstance) .map { Reactor.Action.changePage } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -96,7 +96,7 @@ extension HomeListController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -113,11 +113,11 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -130,10 +130,10 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -142,7 +142,7 @@ extension HomeListController: UICollectionViewDelegate, UICollectionViewDataSour pageChange.onNext(()) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 1 { cellTapped.onNext(indexPath.row)} } diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift b/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift index 56480168..23903e0b 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/List/HomeListReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class HomeListReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -21,7 +21,7 @@ final class HomeListReactor: Reactor { case changePage case cellTapped(controller: BaseViewController, row: Int) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) case loadView @@ -30,28 +30,28 @@ final class HomeListReactor: Reactor { case appendData case moveToDetailScene(controller: BaseViewController, row: Int) } - + struct State { var popUpType: HomePopUpType var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var popUpType: HomePopUpType - + private let homeAPIUseCase = HomeAPIUseCaseImpl() private let userDefaultService = UserDefaultService() private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + private var isLoading: Bool = false private var totalPage: Int32 = 0 private var currentPage: Int32 = 0 private var size: Int32 = 10 - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -68,13 +68,13 @@ final class HomeListReactor: Reactor { private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) private var cardSections = HomeCardGridSection(inputDataList: []) - + // MARK: - init init(popUpType: HomePopUpType) { self.initialState = State(popUpType: popUpType) self.popUpType = popUpType } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -122,7 +122,7 @@ final class HomeListReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -149,11 +149,11 @@ final class HomeListReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { - return [spacing24Section,cardSections,spacing64Section] + return [spacing24Section, cardSections, spacing64Section] } - + func setSection(response: GetHomeInfoResponse) { let isLogin = response.loginYn switch popUpType { @@ -206,7 +206,7 @@ final class HomeListReactor: Reactor { totalPage = response.popularPopUpStoreTotalPages } } - + func appendSectionData(response: GetHomeInfoResponse) { let isLogin = response.loginYn switch popUpType { diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift b/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift index d257675a..2a2a6071 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/List/HomePopUpType.swift @@ -11,7 +11,7 @@ enum HomePopUpType { case curation case new case popular - + var title: String { switch self { case .curation: @@ -22,7 +22,7 @@ enum HomePopUpType { return "인기 팝업 전체보기" } } - + var path: String { switch self { case .curation: diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift b/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift index 185069a7..92406df4 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeCardGridSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct HomeCardGridSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 16) / 2), diff --git a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift b/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift index 7db0f713..e4769564 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/List/View/HomeListView.swift @@ -10,24 +10,22 @@ import UIKit import SnapKit final class HomeListView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { - let view = PPReturnHeaderView() - return view + return PPReturnHeaderView() }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,13 +33,13 @@ final class HomeListView: UIView { // MARK: - SetUp private extension HomeListView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift index 5a507a7d..fcf6d180 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/HomeController.swift @@ -1,35 +1,27 @@ -// -// HomeController.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class HomeController: BaseViewController, View { - + typealias Reactor = HomeReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = HomeView() - + let homeHeaderView: HomeHeaderView = { - let view = HomeHeaderView() - return view + return HomeHeaderView() }() - + private let headerBackgroundView: UIView = UIView() let backGroundblurEffect = UIBlurEffect(style: .regular) lazy var backGroundblurView = UIVisualEffectView(effect: backGroundblurEffect) - + private var sections: [any Sectionable] = [] } @@ -39,7 +31,7 @@ extension HomeController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = false @@ -55,58 +47,57 @@ private extension HomeController { } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( ImageBannerSectionCell.self, forCellWithReuseIdentifier: ImageBannerSectionCell.identifiers ) - + mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomeTitleSectionCell.self, forCellWithReuseIdentifier: HomeTitleSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomeCardSectionCell.self, forCellWithReuseIdentifier: HomeCardSectionCell.identifiers ) - + mainView.contentCollectionView.register( HomePopularCardSectionCell.self, forCellWithReuseIdentifier: HomePopularCardSectionCell.identifiers ) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.top.equalToSuperview() make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) } - + view.addSubview(homeHeaderView) homeHeaderView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).inset(7) make.leading.trailing.equalToSuperview() } - + headerBackgroundView.addSubview(backGroundblurView) backGroundblurView.snp.makeConstraints { make in make.edges.equalToSuperview() } backGroundblurView.isUserInteractionEnabled = false backGroundblurView.isHidden = true - + view.addSubview(headerBackgroundView) headerBackgroundView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(homeHeaderView.snp.bottom).offset(7) } - view.bringSubviewToFront(homeHeaderView) } } @@ -118,7 +109,7 @@ extension HomeController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + homeHeaderView.searchBarButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -126,7 +117,7 @@ extension HomeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -142,18 +133,18 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? ImageBannerSectionCell { cell.bannerTapped .withUnretained(self) @@ -162,7 +153,7 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { }) .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.imageSection.currentPage .distinctUntilChanged() .withUnretained(self) @@ -182,17 +173,17 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? HomeCardSectionCell { cell.bookmarkButton.rx.tap .map { Reactor.Action.bookMarkButtonTapped(indexPath: indexPath)} .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.contentOffset.y <= (307 - headerBackgroundView.frame.maxY) { backGroundblurView.isHidden = true @@ -201,7 +192,7 @@ extension HomeController: UICollectionViewDelegate, UICollectionViewDataSource { backGroundblurView.isHidden = false } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { reactor?.action.onNext(.collectionViewCellTapped(controller: self, indexPath: indexPath)) } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift index d7bdfb8a..aa54ee84 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/HomeReactor.swift @@ -1,18 +1,11 @@ -// -// HomeReactor.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class HomeReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -24,7 +17,7 @@ final class HomeReactor: Reactor { case bannerCellTapped(controller: BaseViewController, row: Int) case changeIndicatorColor(controller: BaseViewController, row: Int) } - + enum Mutation { case loadView case setHedaerState(isDarkMode: Bool) @@ -33,23 +26,23 @@ final class HomeReactor: Reactor { case moveToSearchScene(controller: BaseViewController) case skip } - + struct State { var sections: [any Sectionable] = [] var headerIsDarkMode: Bool = true var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State - + var disposeBag = DisposeBag() - + private let homeApiUseCase = HomeAPIUseCaseImpl() private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) private let userDefaultService = UserDefaultService() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -70,7 +63,16 @@ final class HomeReactor: Reactor { private var popularTitleSection = HomeTitleSection(inputDataList: [ .init(blueText: "팝풀이", topSubText: "들은 지금 이런", bottomText: "팝업에 가장 관심있어요", backgroundColor: .g700, textColor: .w100) ]) - private var popularSection = HomePopularCardSection(inputDataList: [], decorationItems: [SectionDecorationItem(elementKind: "BackgroundView", reusableView: SectionBackGroundDecorationView(), viewInput: .init(backgroundColor: .g700))]) + private var popularSection = HomePopularCardSection( + inputDataList: [], + decorationItems: [ + SectionDecorationItem( + elementKind: "BackgroundView", + reusableView: SectionBackGroundDecorationView(), + viewInput: .init(backgroundColor: .g700) + ) + ] + ) private var newTitleSection = HomeTitleSection(inputDataList: [.init(blueText: "제일 먼저", topSubText: "피드 올리는", bottomText: "신규 오픈 팝업")]) private var newSection = HomeCardSection(inputDataList: []) private var spaceClear48Section = SpacingSection(inputDataList: [.init(spacing: 48)]) @@ -80,12 +82,12 @@ final class HomeReactor: Reactor { private var spaceGray40Section = SpacingSection(inputDataList: [.init(spacing: 40, backgroundColor: .g700)]) private var spaceGray28Section = SpacingSection(inputDataList: [.init(spacing: 28, backgroundColor: .g700)]) private var spaceGray24Section = SpacingSection(inputDataList: [.init(spacing: 24, backgroundColor: .g700)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -125,7 +127,7 @@ final class HomeReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, indexPath: IndexPath(row: row, section: 0))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -162,9 +164,9 @@ final class HomeReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { - + if isLoign { return [ loginImageBannerSection, @@ -193,7 +195,7 @@ final class HomeReactor: Reactor { } } - + func getNewSection() -> [any Sectionable] { if newSection.isEmpty { return [] @@ -206,17 +208,17 @@ final class HomeReactor: Reactor { ] } } - + func setBannerSection(response: GetHomeInfoResponse) { let imagePaths = response.bannerPopUpStoreList.map { $0.mainImageUrl } let idList = response.bannerPopUpStoreList.map { $0.id } loginImageBannerSection.inputDataList = imagePaths.isEmpty ? [] : [.init(imagePaths: imagePaths, idList: idList)] } - + func setCurationTitleSection(response: GetHomeInfoResponse) { curationTitleSection.inputDataList = [.init(blueText: response.nickname, topSubText: "님을 위한", bottomText: "맞춤 팝업 큐레이션")] } - + func setCurationSection(response: GetHomeInfoResponse) { let islogin = response.loginYn curationSection.inputDataList = response.customPopUpStoreList.map({ response in @@ -233,7 +235,7 @@ final class HomeReactor: Reactor { ) }) } - + func setPopularSection(response: GetHomeInfoResponse) { popularSection.inputDataList = response.popularPopUpStoreList.map({ response in return .init( @@ -246,7 +248,7 @@ final class HomeReactor: Reactor { ) }) } - + func setNewSection(response: GetHomeInfoResponse) { let islogin = response.loginYn newSection.inputDataList = response.newPopUpStoreList.map({ response in @@ -263,7 +265,7 @@ final class HomeReactor: Reactor { ) }) } - + func getDetailController(indexPath: IndexPath, currentController: BaseViewController) { if isLoign { switch indexPath.section { @@ -334,7 +336,7 @@ final class HomeReactor: Reactor { } } } - + func getPopUpData(indexPath: IndexPath) -> HomeCardSectionCell.Input { if isLoign { switch indexPath.section { diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift index 7d2e3c8e..23e570ee 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct HomeCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(158), diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift index 5bc534e3..30bb15e1 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeCardSection/HomeCardSectionCell.swift @@ -1,41 +1,33 @@ -// -// HomeCardSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit import RxSwift -import Kingfisher +import SnapKit final class HomeCardSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let imageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + private let categoryLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 11) label.textColor = .blu500 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 14) label.numberOfLines = 2 label.lineBreakMode = .byTruncatingTail return label }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.numberOfLines = 1 @@ -43,19 +35,18 @@ final class HomeCardSectionCell: UICollectionViewCell { label.textColor = .g400 return label }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11) label.lineBreakMode = .byTruncatingTail label.textColor = .g400 return label }() - + let bookmarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let rankLabel: UILabel = { let label = UILabel() label.backgroundColor = .w10 @@ -65,19 +56,19 @@ final class HomeCardSectionCell: UICollectionViewCell { label.textColor = .w100 return label }() - + private let imageService = PreSignedService() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -89,7 +80,7 @@ private extension HomeCardSectionCell { func setUpConstraints() { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true - + contentView.addSubview(imageView) imageView.snp.makeConstraints { make in make.width.equalTo(contentView.bounds.width) @@ -99,42 +90,40 @@ private extension HomeCardSectionCell { } imageView.layer.cornerRadius = 4 imageView.clipsToBounds = true - + contentView.addSubview(categoryLabel) categoryLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.top.equalTo(imageView.snp.bottom).offset(12) make.height.equalTo(15) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(categoryLabel.snp.bottom).offset(4) make.leading.trailing.equalToSuperview() } - - contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.height.equalTo(15).priority(.high) make.bottom.equalToSuperview() } - + contentView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalTo(dateLabel.snp.top) make.height.equalTo(17).priority(.high) } - + contentView.addSubview(bookmarkButton) bookmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.top.trailing.equalToSuperview().inset(8) } - + imageView.addSubview(rankLabel) rankLabel.snp.makeConstraints { make in make.height.equalTo(24) @@ -158,21 +147,21 @@ extension HomeCardSectionCell: Inputable { var isPopular: Bool = false var row: Int? } - + func injection(with input: Input) { - categoryLabel.setLineHeightText(text: "#" + (input.category ?? ""), font: .KorFont(style: .bold, size: 11)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 14)) - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .medium, size: 11)) + categoryLabel.setLineHeightText(text: "#" + (input.category ?? ""), font: .korFont(style: .bold, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 14)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .medium, size: 11)) let date = input.startDate.toDate().toPPDateString() + " ~ " + input.endDate.toDate().toPPDateString() - dateLabel.setLineHeightText(text: date, font: .KorFont(style: .medium, size: 11)) + dateLabel.setLineHeightText(text: date, font: .korFont(style: .medium, size: 11)) let bookmarkImage = input.isBookmark ? UIImage(named: "icon_bookmark_fill") : UIImage(named: "icon_bookmark") bookmarkButton.setImage(bookmarkImage, for: .normal) imageView.setPPImage(path: input.imagePath) bookmarkButton.isHidden = !input.isLogin - + rankLabel.isHidden = !input.isPopular let rank = input.row ?? 0 - rankLabel.setLineHeightText(text: "\(rank + 1)위", font: .KorFont(style: .medium, size: 11), lineHeight: 1) + rankLabel.setLineHeightText(text: "\(rank + 1)위", font: .korFont(style: .medium, size: 11), lineHeight: 1) rankLabel.textAlignment = .center if rank > 2 { rankLabel.isHidden = true diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift index 1a3a4fee..ed9aadf7 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeHeaderView.swift @@ -10,37 +10,37 @@ import UIKit import SnapKit final class HomeHeaderView: UIView { - + // MARK: - Components - + let searchBarButton: UIButton = { let button = UIButton() button.layer.cornerRadius = 4 return button }() - + let blurEffect = UIBlurEffect(style: .regular) lazy var blurView = UIVisualEffectView(effect: blurEffect) - + private let searchIconImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_search_black") return view }() - + private let searchLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14, text: "팝업스토어명을 입력해보세요") label.textColor = .g1000 label.isUserInteractionEnabled = false return label }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -48,7 +48,7 @@ final class HomeHeaderView: UIView { // MARK: - SetUp private extension HomeHeaderView { - + func setUpConstraints() { blurView.isUserInteractionEnabled = false blurView.layer.cornerRadius = 4 @@ -57,21 +57,21 @@ private extension HomeHeaderView { blurView.snp.makeConstraints { make in make.edges.equalTo(searchBarButton) } - + self.addSubview(searchBarButton) searchBarButton.snp.makeConstraints { make in make.height.equalTo(37) make.leading.trailing.equalToSuperview().inset(20) make.top.bottom.equalToSuperview() } - + searchBarButton.addSubview(searchIconImageView) searchIconImageView.snp.makeConstraints { make in make.size.equalTo(20) make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(12) } - + searchBarButton.addSubview(searchLabel) searchLabel.snp.makeConstraints { make in make.centerY.equalTo(searchIconImageView) diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift index e790e333..4196db68 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct HomePopularCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomePopularCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(232), @@ -33,13 +33,13 @@ struct HomePopularCardSection: Sectionable { heightDimension: .absolute(332) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - + // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 16 section.orthogonalScrollingBehavior = .continuous - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift index aa395113..b4bd7862 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomePopularCardSection/HomePopularCardSectionCell.swift @@ -1,25 +1,17 @@ -// -// HomePopularCardSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/30/24. -// - import UIKit -import SnapKit import RxSwift -import Kingfisher +import SnapKit final class HomePopularCardSectionCell: UICollectionViewCell { - + // MARK: - Components private var backGroundImageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + private let blurView: UIView = { var view = UIView() view.frame = CGRect(x: 0, y: 0, width: 232, height: 332) @@ -36,43 +28,43 @@ final class HomePopularCardSectionCell: UICollectionViewCell { view.layer.addSublayer(gradientLayer) return view }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .w100 return label }() - + private let categoryLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .g1000 label.backgroundColor = .w100 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.numberOfLines = 2 label.textColor = .w100 return label }() - + private let locationLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 16) label.textColor = .w100 return label }() - + let disposeBag = DisposeBag() - + private let imageService = PreSignedService() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -83,36 +75,36 @@ private extension HomePopularCardSectionCell { func setUpConstraints() { contentView.layer.cornerRadius = 4 contentView.clipsToBounds = true - + contentView.addSubview(backGroundImageView) backGroundImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + backGroundImageView.addSubview(blurView) blurView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + contentView.addSubview(categoryLabel) categoryLabel.snp.makeConstraints { make in make.bottom.equalTo(titleLabel.snp.top).offset(-16) make.leading.equalToSuperview().inset(20) make.height.equalTo(24) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.bottom.equalTo(categoryLabel.snp.top).offset(-6) make.leading.trailing.equalToSuperview().inset(20) } - + contentView.addSubview(locationLabel) locationLabel.snp.makeConstraints { make in make.centerY.equalTo(categoryLabel) @@ -131,10 +123,10 @@ extension HomePopularCardSectionCell: Inputable { var id: Int64 var address: String? } - + func injection(with input: Input) { let date = "#\(input.endDate.toDate().toPPDateMonthString())까지 열리는" - dateLabel.setLineHeightText(text: date, font: .KorFont(style: .regular, size: 16)) + dateLabel.setLineHeightText(text: date, font: .korFont(style: .regular, size: 16)) let category = "#\(input.category ?? "")" if let addressArray = input.address?.components(separatedBy: " ") { if addressArray.count > 2 { @@ -142,9 +134,9 @@ extension HomePopularCardSectionCell: Inputable { locationLabel.text = "#\(address)" } } - + categoryLabel.text = category - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 16)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 16)) backGroundImageView.setPPImage(path: input.imagePath) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift index 32dc908a..3be6b834 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct HomeTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = HomeTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,9 +35,8 @@ struct HomeTitleSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) // section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 16) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift index e74a2fcd..cf0f58cb 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeTitleSection/HomeTitleSectionCell.swift @@ -7,47 +7,45 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class HomeTitleSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private var blueLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.textColor = .blu500 return label }() - + private let subLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + private let bottomLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + let detailButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_right_gray"), for: .normal) return button }() - + // MARK: - init override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -72,7 +70,7 @@ private extension HomeTitleSectionCell { make.leading.equalToSuperview().inset(20) make.bottom.equalToSuperview() } - + contentView.addSubview(detailButton) detailButton.snp.makeConstraints { make in make.size.equalTo(24) @@ -90,11 +88,11 @@ extension HomeTitleSectionCell: Inputable { var backgroundColor: UIColor? = .clear var textColor: UIColor? = .g1000 } - + func injection(with input: Input) { - blueLabel.setLineHeightText(text: input.blueText, font: .KorFont(style: .bold, size: 16)) - subLabel.setLineHeightText(text: input.topSubText, font: .KorFont(style: .bold, size: 16)) - bottomLabel.setLineHeightText(text: input.bottomText, font: .KorFont(style: .bold, size: 16)) + blueLabel.setLineHeightText(text: input.blueText, font: .korFont(style: .bold, size: 16)) + subLabel.setLineHeightText(text: input.topSubText, font: .korFont(style: .bold, size: 16)) + bottomLabel.setLineHeightText(text: input.bottomText, font: .korFont(style: .bold, size: 16)) contentView.backgroundColor = input.backgroundColor subLabel.textColor = input.textColor bottomLabel.textColor = input.textColor diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift index cab79f80..3710e7e0 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/HomeView.swift @@ -10,20 +10,20 @@ import UIKit import SnapKit final class HomeView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.contentInsetAdjustmentBehavior = .never return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -31,7 +31,7 @@ final class HomeView: UIView { // MARK: - SetUp private extension HomeView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift index 14ddb820..8d323718 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct ImageBannerChildSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ImageBannerChildSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift index 43dcc0e9..173f05ed 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerChildSection/ImageBannerChildSectionCell.swift @@ -7,28 +7,28 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class ImageBannerChildSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let imageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -49,7 +49,7 @@ extension ImageBannerChildSectionCell: Inputable { var imagePath: String? var id: Int64 } - + func injection(with input: Input) { imageView.setPPImage(path: input.imagePath) } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift index 8a03671c..e60a5a6d 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct ImageBannerSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ImageBannerSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +35,7 @@ struct ImageBannerSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift index 025ec4a9..c6d16cef 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/ImageBannerSection/ImageBannerSection/ImageBannerSectionCell.swift @@ -1,32 +1,24 @@ -// -// ImageBannerSectionCell.swift -// Poppool -// -// Created by SeoJunYoung on 11/28/24. -// - import UIKit -import SnapKit import RxSwift +import SnapKit final class ImageBannerSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private var autoScrollTimer: Timer? - + private lazy var contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) view.contentInsetAdjustmentBehavior = .never return view }() - + var pageControl: CustomPageControl = { - let controller = CustomPageControl() - return controller + return CustomPageControl() }() let stopButton: UIButton = { @@ -34,18 +26,18 @@ final class ImageBannerSectionCell: UICollectionViewCell { button.setImage(UIImage(named: "icon_banner_stopButton"), for: .normal) return button }() - + let playButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_banner_playButton"), for: .normal) return button }() - + private var isAutoBannerPlay: Bool = false private var isFirstResponseAutoScroll: Bool = true - + var imageSection = ImageBannerChildSection(inputDataList: []) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -59,30 +51,30 @@ final class ImageBannerSectionCell: UICollectionViewCell { return getSection()[section].getSection(section: section, env: env) } }() - + let bannerTapped: PublishSubject = .init() - + private var currentIndex: Int = 1 private var isHiddenPauseButton: Bool = true - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUp() setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() isFirstResponseAutoScroll = false } - + // 자동 스크롤 중지 함수 func stopAutoScroll() { stopButton.isHidden = true @@ -91,7 +83,9 @@ final class ImageBannerSectionCell: UICollectionViewCell { autoScrollTimer?.invalidate() autoScrollTimer = nil } - + + // FIXME: (홈 -> 상세) 이동 시 홈의 자동 스크롤이 계속 돌아감. + // FIXME: 또한 오토 스크롤을 한번만 실행하면 되는데, 사진이 넘어갈 때 마다 실행되는것으로 보임 func startAutoScroll(interval: TimeInterval = 3.0) { stopAutoScroll() // 기존 타이머를 중지 stopButton.isHidden = false @@ -112,13 +106,13 @@ private extension ImageBannerSectionCell { func setUp() { contentCollectionView.delegate = self contentCollectionView.dataSource = self - + contentCollectionView.register( ImageBannerChildSectionCell.self, forCellWithReuseIdentifier: ImageBannerChildSectionCell.identifiers ) } - + func setUpConstraints() { contentView.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in @@ -135,7 +129,7 @@ private extension ImageBannerSectionCell { make.centerY.equalTo(pageControl) make.leading.equalTo(pageControl.snp.trailing).offset(6) } - + contentView.addSubview(playButton) playButton.snp.makeConstraints { make in make.size.equalTo(6) @@ -143,11 +137,11 @@ private extension ImageBannerSectionCell { make.leading.equalTo(pageControl.snp.trailing).offset(6) } } - + func getSection() -> [any Sectionable] { return [imageSection] } - + private func findViewController() -> BaseViewController? { var nextResponder = self.next while nextResponder != nil { @@ -158,7 +152,7 @@ private extension ImageBannerSectionCell { } return nil } - + func bind() { stopButton.rx.tap .withUnretained(self) @@ -170,7 +164,7 @@ private extension ImageBannerSectionCell { } } .disposed(by: disposeBag) - + playButton.rx.tap .withUnretained(self) .subscribe { (owner, _) in @@ -181,7 +175,7 @@ private extension ImageBannerSectionCell { } } .disposed(by: disposeBag) - + imageSection.currentPage .distinctUntilChanged() .withUnretained(self) @@ -202,39 +196,43 @@ extension ImageBannerSectionCell: Inputable { var idList: [Int64] var isHiddenPauseButton: Bool = false } - + func injection(with input: Input) { if imageSection.isEmpty { pageControl.setNumberOfPages(input.imagePaths.count) let datas = zip(input.imagePaths, input.idList) - let backContents = datas.suffix(1) - let frontContents = datas.prefix(1) - imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } - imageSection.inputDataList.append(contentsOf: frontContents.map { .init(imagePath: $0.0, id: $0.1) }) - imageSection.inputDataList = backContents.map {.init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList - DispatchQueue.main.async { [weak self] in - self?.contentCollectionView.scrollToItem( - at: .init(row: 1, section: 0), - at: .centeredHorizontally, animated: false - ) + if input.imagePaths.count > 1 { + let backContents = datas.suffix(1) + let frontContents = datas.prefix(1) + imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList.append(contentsOf: frontContents.map { .init(imagePath: $0.0, id: $0.1) }) + imageSection.inputDataList = backContents.map { .init(imagePath: $0.0, id: $0.1) } + imageSection.inputDataList + DispatchQueue.main.async { [weak self] in + self?.contentCollectionView.scrollToItem( + at: .init(row: 1, section: 0), + at: .centeredHorizontally, animated: false + ) + } + } else { + imageSection.inputDataList = datas.map { .init(imagePath: $0.0, id: $0.1) } } } - + contentCollectionView.reloadData() isHiddenPauseButton = input.isHiddenPauseButton if isFirstResponseAutoScroll { startAutoScroll() isFirstResponseAutoScroll = false } - + if input.isHiddenPauseButton { stopAutoScroll() stopButton.isHidden = true playButton.isHidden = true } - + bind() - + if input.imagePaths.count == 1 { playButton.isHidden = true stopButton.isHidden = true @@ -245,27 +243,28 @@ extension ImageBannerSectionCell: Inputable { // MARK: - UICollectionViewDelegate, UICollectionViewDataSource extension ImageBannerSectionCell: UICollectionViewDelegate, UICollectionViewDataSource { - + func numberOfSections(in collectionView: UICollectionView) -> Int { return getSection().count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return getSection()[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return getSection()[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { bannerTapped.onNext(indexPath.row) } - + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard imageSection.dataCount > 1 else { return } + if currentIndex == 0 { contentCollectionView.scrollToItem( at: .init(row: imageSection.dataCount - 2, section: 0), @@ -281,7 +280,7 @@ extension ImageBannerSectionCell: UICollectionViewDelegate, UICollectionViewData if !isHiddenPauseButton { startAutoScroll() } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift index 6a2c2396..300f838a 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SectionBackGroundDecorationView.swift @@ -13,7 +13,7 @@ class SectionBackGroundDecorationView: UICollectionReusableView { super.init(frame: frame) self.backgroundColor = .g700 } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -23,7 +23,7 @@ extension SectionBackGroundDecorationView: Inputable { struct Input { var backgroundColor: UIColor } - + func injection(with input: Input) { self.backgroundColor = input.backgroundColor } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift index 02c2172b..0c7ae8ee 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct SpacingSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = SpacingSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +35,7 @@ struct SpacingSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift index db08e964..5cb66eb4 100644 --- a/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Home/Main/View/SpacingSection/SpacingSectionCell.swift @@ -7,24 +7,24 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class SpacingSectionCell: UICollectionViewCell { - + // MARK: - Components let disposeBag = DisposeBag() - + private let spaceView: UIView = UIView() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -42,7 +42,7 @@ extension SpacingSectionCell: Inputable { var spacing: Float var backgroundColor: UIColor? = .clear } - + func injection(with input: Input) { spaceView.snp.removeConstraints() spaceView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift index c2b104a5..49a2a231 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift +++ b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailController.swift @@ -7,20 +7,20 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class ImageDetailController: BaseViewController, View { - + typealias Reactor = ImageDetailReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = ImageDetailView() - + private let cancelButton: UIButton = { let view = UIButton() view.setImage(UIImage(named: "icon_xmark_white"), for: .normal) @@ -56,7 +56,7 @@ private extension ImageDetailController { // MARK: - Methods extension ImageDetailController { func bind(reactor: Reactor) { - + cancelButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -64,7 +64,7 @@ extension ImageDetailController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift index 826253be..f94bb7bc 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailReactor.swift @@ -6,34 +6,34 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class ImageDetailReactor: Reactor { - + // MARK: - Reactor enum Action { case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) } - + struct State { var imagePath: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(imagePath: String?) { self.initialState = State(imagePath: imagePath) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -41,7 +41,7 @@ final class ImageDetailReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToRecentScene(let controller): diff --git a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift index 512c88d1..6092969d 100644 --- a/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift +++ b/Poppool/Poppool/Presentation/Scene/ImageDetail/ImageDetailView.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class ImageDetailView: UIView { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -18,13 +18,13 @@ final class ImageDetailView: UIView { view.contentMode = .scaleAspectFit return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -32,7 +32,7 @@ final class ImageDetailView: UIView { // MARK: - SetUp private extension ImageDetailView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift b/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift index 28c0ff4b..eeebbd5a 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/LastLoginView.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class LastLoginView: UIView { - + /// 방향에 따라 툴팁을 다르게 표시합니다 enum TipDirection { case pointUp case pointDown } - + /// 툴팁의 색상을 지정하였습니다 /// 텍스트 컬러 또한 TipColor에 따라 수정됩니다 enum TipColor { case blu500 case w100 - + var color: UIColor { switch self { case .blu500: return UIColor.blu500 case .w100: return UIColor.w100 } } - + var textColor: UIColor { switch self { case .blu500: return UIColor.w100 @@ -37,34 +37,33 @@ final class LastLoginView: UIView { } } } - + // MARK: - Properties - + private let bgView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let notificationLabel: UILabel = { let label = UILabel() - label.font = .KorFont(style: .medium, size: 13) + label.font = .korFont(style: .medium, size: 13) return label }() - + private var colorType: TipColor { didSet { self.setNeedsDisplay() } } - + private var midX: CGFloat { return self.bounds.midX } - + private var direction: TipDirection - + // MARK: - init - + /// 툴팁 뷰를 생성합니다 /// - Parameters: /// - colorType: 툴팁의 색상(UIColor)을 인자로 받습니다 - w100, blu500 @@ -77,29 +76,29 @@ final class LastLoginView: UIView { notificationLabel.textColor = colorType.textColor notificationLabel.text = text } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func draw(_ rect: CGRect) { drawToolTip() } } extension LastLoginView { - + // MARK: - Methods - + private func setupLayer(color: TipColor) { self.backgroundColor = .clear addSubview(bgView) - + bgView.snp.makeConstraints { make in make.height.equalTo(45) make.edges.equalToSuperview() } - + bgView.addSubview(notificationLabel) switch direction { case .pointUp: @@ -107,7 +106,7 @@ extension LastLoginView { make.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview().inset(11) } - + case .pointDown: notificationLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) @@ -115,20 +114,20 @@ extension LastLoginView { } } } - + /// 툴팁을 방향에 맞춰 그리고 섀도우를 더하는 메서드 private func drawToolTip() { switch direction { case .pointUp: drawUpPointingToolTip() addShadow() - + case .pointDown: drawDownPointingTip() addShadow() } } - + /// 위를 가리키는 툴팁을 만듭니다 private func drawUpPointingToolTip() { let textLength = notificationLabel.frame.width @@ -137,10 +136,10 @@ extension LastLoginView { tip.addLine(to: CGPoint(x: midX, y: 0)) tip.addLine(to: CGPoint(x: midX + 8, y: 10)) tip.close() - + colorType.color.setFill() tip.fill() - + let message = UIBezierPath( roundedRect: CGRect( x: 0, y: 10, @@ -149,25 +148,25 @@ extension LastLoginView { ), cornerRadius: 6 ) - + colorType.color.setFill() message.fill() message.close() } - + /// 아래를 가리키는 툴팁을 만듭니다 private func drawDownPointingTip() { let textLength = notificationLabel.frame.width - + let tip = UIBezierPath() tip.move(to: CGPoint(x: midX - 8, y: 35)) tip.addLine(to: CGPoint(x: midX, y: 45)) tip.addLine(to: CGPoint(x: midX + 8, y: 35)) tip.close() - + colorType.color.setFill() tip.fill() - + let message = UIBezierPath( roundedRect: CGRect( x: 0, y: 0, @@ -176,19 +175,19 @@ extension LastLoginView { ), cornerRadius: 6 ) - + colorType.color.setFill() message.fill() message.close() } - + /// 툴팁의 섀도우를 더합니다 private func addShadow() { layer.shadowOffset = CGSize(width: 0, height: 5) layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.2 layer.shadowRadius = 5 - + // 섀도우를 그릴 때 드는 리소스를 줄이기 위해 캐시를 적용하는 방식 // layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath layer.shouldRasterize = true @@ -200,9 +199,9 @@ extension UIView { func showToolTip(color: LastLoginView.TipColor, direction: LastLoginView.TipDirection, text: String? = "최근에 이 방법으로 로그인했어요") { // 호출하는 컴포넌트 위 또는 아래에 생성되기 위해 superview를 구합니다 guard let superview = self.superview else { return } - + let toolTip = LastLoginView(colorType: color, direction: direction, text: text) - + superview.addSubview(toolTip) toolTip.snp.makeConstraints { make in if direction == .pointDown { diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift index f0fd9f61..47912023 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class LoginController: BaseViewController, View { - + typealias Reactor = LoginReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = LoginView() } @@ -28,7 +28,7 @@ extension LoginController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let lastLogin = reactor?.userDefaultService.fetch(key: "lastLogin") { @@ -57,12 +57,12 @@ private extension LoginController { // MARK: - Methods extension LoginController { func bind(reactor: Reactor) { - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.guestButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -70,7 +70,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.kakaoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,7 +78,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.inquiryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -86,7 +86,7 @@ extension LoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.appleButton.rx.tap .withUnretained(self) .map { (owner, _) in diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift index 74ef7572..1d3425a0 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class LoginReactor: Reactor { - + // MARK: - Reactor enum Action { case kakaoButtonTapped(controller: BaseViewController) @@ -19,7 +19,7 @@ final class LoginReactor: Reactor { case viewWillAppear case inquiryButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToSignUpScene(controller: BaseViewController) case moveToHomeScene(controller: BaseViewController) @@ -27,28 +27,28 @@ final class LoginReactor: Reactor { case resetService case moveToInquiryScene(controller: BaseViewController) } - + struct State { } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - + private let kakaoLoginService = KakaoLoginService() private var appleLoginService = AppleLoginService() private let authApiUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) private let keyChainService = KeyChainService() let userDefaultService = UserDefaultService() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,8 +57,8 @@ final class LoginReactor: Reactor { case .appleButtonTapped(let controller): return loginWithApple(controller: controller) case .guestButtonTapped(let controller): - let _ = keyChainService.deleteToken(type: .accessToken) - let _ = keyChainService.deleteToken(type: .refreshToken) + _ = keyChainService.deleteToken(type: .accessToken) + _ = keyChainService.deleteToken(type: .refreshToken) return Observable.just(.moveToHomeScene(controller: controller)) case .viewWillAppear: return Observable.just(.resetService) @@ -66,7 +66,7 @@ final class LoginReactor: Reactor { return Observable.just(.moveToInquiryScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToSignUpScene(let controller): @@ -88,7 +88,7 @@ final class LoginReactor: Reactor { } return state } - + func loginWithKakao(controller: BaseViewController) -> Observable { return kakaoLoginService.fetchUserCredential() .withUnretained(self) @@ -115,7 +115,7 @@ final class LoginReactor: Reactor { } } } - + func loginWithApple(controller: BaseViewController) -> Observable { return appleLoginService.fetchUserCredential() .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift index ed3830da..437232c9 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Main/LoginView.swift @@ -10,66 +10,64 @@ import UIKit import SnapKit final class LoginView: UIView { - + // MARK: - Components let guestButton: UIButton = { let button = UIButton(type: .system) button.setTitle("둘러보기", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 14) + button.titleLabel?.font = .korFont(style: .regular, size: 14) button.setTitleColor(.g1000, for: .normal) return button }() - + private let logoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_login_logo") view.contentMode = .scaleAspectFit return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16, text: "간편하게 SNS 로그인하고\n팝풀 서비스를 이용해보세요") label.numberOfLines = 0 label.textAlignment = .center return label }() - + let kakaoButton: PPButton = { - let button = PPButton(style: .kakao, text: "카카오톡으로 로그인") - return button + return PPButton(style: .kakao, text: "카카오톡으로 로그인") }() - + private let kakaoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_kakao") return view }() - + let appleButton: PPButton = { - let button = PPButton(style: .apple, text: "Apple로 로그인") - return button + return PPButton(style: .apple, text: "Apple로 로그인") }() - + private let appleImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_apple") return view }() - + let inquiryButton: UIButton = { let button = UIButton(type: .system) button.setTitle("로그인이 어려우신가요?", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 12) + button.titleLabel?.font = .korFont(style: .regular, size: 12) button.setTitleColor(.g1000, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -77,14 +75,14 @@ final class LoginView: UIView { // MARK: - SetUp private extension LoginView { - + func setUpConstraints() { self.addSubview(guestButton) guestButton.snp.makeConstraints { make in make.top.equalToSuperview().inset(11) make.trailing.equalToSuperview().inset(20) } - + self.addSubview(logoImageView) logoImageView.snp.makeConstraints { make in make.height.equalTo(90) @@ -92,41 +90,41 @@ private extension LoginView { make.top.equalTo(guestButton.snp.bottom).offset(75) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(logoImageView.snp.bottom).offset(28) } - + self.addSubview(kakaoButton) kakaoButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(156) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + kakaoButton.addSubview(kakaoImageView) kakaoImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(appleButton) appleButton.snp.makeConstraints { make in make.top.equalTo(kakaoButton.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + appleButton.addSubview(appleImageView) appleImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(inquiryButton) inquiryButton.snp.makeConstraints { make in make.bottom.equalToSuperview().inset(56) diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift index b35da1bb..a377e56b 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SubLoginController: BaseViewController, View { - + typealias Reactor = SubLoginReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SubLoginView() } @@ -28,7 +28,7 @@ extension SubLoginController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let lastLogin = reactor?.userDefaultService.fetch(key: "lastLogin") { @@ -62,7 +62,7 @@ extension SubLoginController { .map { Reactor.Action.viewWillAppear} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -70,7 +70,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.kakaoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,7 +78,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.inquiryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -86,7 +86,7 @@ extension SubLoginController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.appleButton.rx.tap .withUnretained(self) .map { (owner, _) in diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift index c9254e87..24b6d961 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SubLoginReactor: Reactor { - + // MARK: - Reactor enum Action { case kakaoButtonTapped(controller: BaseViewController) @@ -19,7 +19,7 @@ final class SubLoginReactor: Reactor { case viewWillAppear case inquiryButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToSignUpScene(controller: BaseViewController) case dismissScene(controller: BaseViewController) @@ -27,28 +27,28 @@ final class SubLoginReactor: Reactor { case resetService case moveToInquiryScene(controller: BaseViewController) } - + struct State { } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - + private let kakaoLoginService = KakaoLoginService() private var appleLoginService = AppleLoginService() private let authApiUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) private let keyChainService = KeyChainService() let userDefaultService = UserDefaultService() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -64,7 +64,7 @@ final class SubLoginReactor: Reactor { return Observable.just(.moveToInquiryScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToSignUpScene(let controller): @@ -85,7 +85,7 @@ final class SubLoginReactor: Reactor { } return state } - + func loginWithKakao(controller: BaseViewController) -> Observable { return kakaoLoginService.fetchUserCredential() .withUnretained(self) @@ -112,7 +112,7 @@ final class SubLoginReactor: Reactor { } } } - + func loginWithApple(controller: BaseViewController) -> Observable { return appleLoginService.fetchUserCredential() .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift index bcbe83c5..c1a34e01 100644 --- a/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift +++ b/Poppool/Poppool/Presentation/Scene/Login/Sub/SubLoginView.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class SubLoginView: UIView { - + // MARK: - Components let xmarkButton: UIButton = { let button = UIButton(type: .system) @@ -18,58 +18,56 @@ final class SubLoginView: UIView { button.tintColor = .g1000 return button }() - + private let logoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_login_logo") view.contentMode = .scaleAspectFit return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16, text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?") - label.setLineHeightText(text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?", font: .KorFont(style: .bold, size: 16), lineHeight: 1.3) + label.setLineHeightText(text: "간편하게 SNS 로그인하고\n공감가는 코멘트에 반응해볼까요?\n다른 코멘트를 확인해볼까요?", font: .korFont(style: .bold, size: 16), lineHeight: 1.3) label.numberOfLines = 0 label.textAlignment = .center return label }() - + let kakaoButton: PPButton = { - let button = PPButton(style: .kakao, text: "카카오톡으로 로그인") - return button + return PPButton(style: .kakao, text: "카카오톡으로 로그인") }() - + private let kakaoImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_kakao") return view }() - + let appleButton: PPButton = { - let button = PPButton(style: .apple, text: "Apple로 로그인") - return button + return PPButton(style: .apple, text: "Apple로 로그인") }() - + private let appleImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_login_apple") return view }() - + let inquiryButton: UIButton = { let button = UIButton(type: .system) button.setTitle("로그인이 어려우신가요?", for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 12) + button.titleLabel?.font = .korFont(style: .regular, size: 12) button.setTitleColor(.g1000, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -77,7 +75,7 @@ final class SubLoginView: UIView { // MARK: - SetUp private extension SubLoginView { - + func setUpConstraints() { self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in @@ -85,7 +83,7 @@ private extension SubLoginView { make.trailing.equalToSuperview().inset(20) make.size.equalTo(32) } - + self.addSubview(logoImageView) logoImageView.snp.makeConstraints { make in make.height.equalTo(90) @@ -93,41 +91,41 @@ private extension SubLoginView { make.top.equalTo(xmarkButton.snp.bottom).offset(75) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(logoImageView.snp.bottom).offset(28) } - + self.addSubview(kakaoButton) kakaoButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(156) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + kakaoButton.addSubview(kakaoImageView) kakaoImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(appleButton) appleButton.snp.makeConstraints { make in make.top.equalTo(kakaoButton.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + appleButton.addSubview(appleImageView) appleImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) make.size.equalTo(22) } - + self.addSubview(inquiryButton) inquiryButton.snp.makeConstraints { make in make.bottom.equalToSuperview().inset(56) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift index 9e52be99..ac172875 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageController.swift @@ -7,20 +7,20 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class BlockUserManageController: BaseViewController, View { - + typealias Reactor = BlockUserManageReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = BlockUserManageView() - + private var sections: [any Sectionable] = [] } @@ -30,7 +30,7 @@ extension BlockUserManageController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -52,12 +52,12 @@ private extension BlockUserManageController { mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( BlockUserListSectionCell.self, forCellWithReuseIdentifier: BlockUserListSectionCell.identifiers ) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) @@ -75,12 +75,12 @@ extension BlockUserManageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -98,11 +98,11 @@ extension BlockUserManageController: UICollectionViewDelegate, UICollectionViewD func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift index f714226b..99c16ac2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Block/BlockUserManageReactor.swift @@ -8,35 +8,35 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class BlockUserManageReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case blockButtonTapped(row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isEmptyList: Bool = true } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -50,16 +50,16 @@ final class BlockUserManageReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var countSection = CommentListTitleSection(inputDataList: []) private var listSection = BlockUserListSection(inputDataList: []) private var spcing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -87,7 +87,7 @@ final class BlockUserManageReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -99,7 +99,7 @@ final class BlockUserManageReactor: Reactor { newState.isEmptyList = listSection.isEmpty return newState } - + func getSection() -> [any Sectionable] { return [ spcing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift index 28e01604..43dd9f31 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct BlockUserListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = BlockUserListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift index f784f85b..dc08a239 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserListSection/BlockUserListSectionCell.swift @@ -7,15 +7,15 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class BlockUserListSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 18 @@ -23,29 +23,28 @@ final class BlockUserListSectionCell: UICollectionViewCell { view.contentMode = .scaleAspectFill return view }() - + private let nickNameLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let blockButton: UIButton = { let button = UIButton() button.layer.cornerRadius = 4 return button }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -60,18 +59,18 @@ private extension BlockUserListSectionCell { make.size.equalTo(36) make.leading.centerY.equalToSuperview() } - + contentView.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(profileImageView.snp.trailing).offset(12) } - + contentView.addSubview(blockButton) blockButton.snp.makeConstraints { make in make.width.equalTo(75) make.height.equalTo(32) - + make.centerY.trailing.equalToSuperview() } } @@ -84,19 +83,19 @@ extension BlockUserListSectionCell: Inputable { var userID: String? var isBlocked: Bool } - + func injection(with input: Input) { profileImageView.setPPImage(path: input.profileImagePath) - nickNameLabel.setLineHeightText(text: input.nickName, font: .KorFont(style: .bold, size: 14)) + nickNameLabel.setLineHeightText(text: input.nickName, font: .korFont(style: .bold, size: 14)) if input.isBlocked { blockButton.setTitle("차단완료", for: .normal) - blockButton.titleLabel?.font = .KorFont(style: .medium, size: 13) + blockButton.titleLabel?.font = .korFont(style: .medium, size: 13) blockButton.backgroundColor = .re600 blockButton.setTitleColor(.w100, for: .normal) blockButton.layer.borderWidth = 0 } else { blockButton.setTitle("차단해제", for: .normal) - blockButton.titleLabel?.font = .KorFont(style: .medium, size: 13) + blockButton.titleLabel?.font = .korFont(style: .medium, size: 13) blockButton.backgroundColor = .w100 blockButton.setTitleColor(.g300, for: .normal) blockButton.layer.borderWidth = 1 diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift index f217bc90..4fb424f1 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Block/View/BlockUserManageView.swift @@ -10,19 +10,18 @@ import UIKit import SnapKit final class BlockUserManageView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "차단한 사용자 관리", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "차단한 사용자 관리", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + let emptyLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "차단한 사용자가 없어요") label.textColor = .g400 @@ -33,7 +32,7 @@ final class BlockUserManageView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -41,19 +40,19 @@ final class BlockUserManageView: UIView { // MARK: - SetUp private extension BlockUserManageView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + self.addSubview(emptyLabel) emptyLabel.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(137) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift index 413df27f..55e98c2b 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkController.swift @@ -7,28 +7,28 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageBookmarkController: BaseViewController, View { - + typealias Reactor = MyPageBookmarkReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageBookmarkView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() - + private var currentPageIndex: Int = 0 - + private var maxPageIndex: Int = 0 - + private var viewType: String? } @@ -38,7 +38,7 @@ extension MyPageBookmarkController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -64,7 +64,7 @@ private extension MyPageBookmarkController { mainView.contentCollectionView.register( ListCountButtonSectionCell.self, forCellWithReuseIdentifier: ListCountButtonSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( PopUpCardSectionCell.self, forCellWithReuseIdentifier: PopUpCardSectionCell.identifiers @@ -84,7 +84,7 @@ extension MyPageBookmarkController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -92,7 +92,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -100,7 +100,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.emptyButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -108,7 +108,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.countButtonView.dropdownButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -116,7 +116,7 @@ extension MyPageBookmarkController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.contentCollectionView.rx.gesture(.swipe(direction: .up)) .skip(1) .withUnretained(self) @@ -139,12 +139,12 @@ extension MyPageBookmarkController { } owner.currentPageIndex += 1 owner.mainView.contentCollectionView.scrollToItem(at: .init(row: owner.currentPageIndex, section: 1), at: .top, animated: true) - + } } } .disposed(by: disposeBag) - + mainView.contentCollectionView.rx.gesture(.swipe(direction: .down)) .skip(1) .withUnretained(self) @@ -157,7 +157,7 @@ extension MyPageBookmarkController { } } .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -166,13 +166,13 @@ extension MyPageBookmarkController { owner.mainView.contentCollectionView.isHidden = state.isEmptyCase owner.mainView.emptyLabel.isHidden = !state.isEmptyCase owner.mainView.emptyButton.isHidden = !state.isEmptyCase - owner.mainView.countButtonView.buttonTitleLabel.setLineHeightText(text: state.buttonTitle, font: .KorFont(style: .regular, size: 13)) - owner.mainView.countButtonView.countLabel.setLineHeightText(text: "총 \(state.count)개", font: .KorFont(style: .regular, size: 13)) - + owner.mainView.countButtonView.buttonTitleLabel.setLineHeightText(text: state.buttonTitle, font: .korFont(style: .regular, size: 13)) + owner.mainView.countButtonView.countLabel.setLineHeightText(text: "총 \(state.count)개", font: .korFont(style: .regular, size: 13)) + if state.buttonTitle != owner.viewType { owner.mainView.contentCollectionView.scrollsToTop = true } - + if state.buttonTitle == "크게보기" { owner.mainView.contentCollectionView.isScrollEnabled = false if owner.viewType == "모아서보기" { @@ -182,7 +182,7 @@ extension MyPageBookmarkController { } else { owner.mainView.contentCollectionView.isScrollEnabled = true } - + owner.maxPageIndex = Int(state.count) owner.viewType = state.buttonTitle } @@ -195,11 +195,11 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -212,7 +212,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? DetailSimilarSectionCell { cell.bookMarkButton.rx.tap .map { Reactor.Action.bookMarkButtonTapped(row: indexPath.row) } @@ -221,7 +221,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -230,7 +230,7 @@ extension MyPageBookmarkController: UICollectionViewDelegate, UICollectionViewDa reactor?.action.onNext(.changePage) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 1 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift index a9ffdfee..077b4506 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageBookmarkReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -23,7 +23,7 @@ final class MyPageBookmarkReactor: Reactor { case emptyButtonTapped(controller: BaseViewController) case bookMarkButtonTapped(row: Int) } - + enum Mutation { case loadView case skip @@ -32,7 +32,7 @@ final class MyPageBookmarkReactor: Reactor { case presentModal(controller: BaseViewController) case moveToSuggestScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false @@ -40,9 +40,9 @@ final class MyPageBookmarkReactor: Reactor { var count: Int32 = 0 var buttonTitle: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var isLoading: Bool = false @@ -51,9 +51,9 @@ final class MyPageBookmarkReactor: Reactor { private var currentPage: Int32 = 0 private var size: Int32 = 10 private var viewType: String = "크게보기" - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -67,17 +67,17 @@ final class MyPageBookmarkReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var listSection = RecentPopUpSection(inputDataList: []) private var cardListSection = PopUpCardSection(inputDataList: []) private var spacing12Section = SpacingSection(inputDataList: [.init(spacing: 12)]) private var spacing150Section = SpacingSection(inputDataList: [.init(spacing: 150)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -165,7 +165,7 @@ final class MyPageBookmarkReactor: Reactor { } } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -204,7 +204,7 @@ final class MyPageBookmarkReactor: Reactor { newState.buttonTitle = viewType return newState } - + func getSection() -> [any Sectionable] { return [ spacing12Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift index e6cb99f7..1bf108c0 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/MyPageBookmarkView.swift @@ -10,14 +10,14 @@ import UIKit import SnapKit final class MyPageBookmarkView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "찜한 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "찜한 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -25,39 +25,38 @@ final class MyPageBookmarkView: UIView { view.isPrefetchingEnabled = true return view }() - + let emptyLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "앗! 아직 찜해둔 팝업이 없어요") label.textColor = .g400 label.isHidden = true return label }() - + let countButtonView: CountButtonView = { - let view = CountButtonView() - return view + return CountButtonView() }() - + let emptyButton: UIButton = { let button = UIButton() let buttonTitle = NSAttributedString( string: "추천 팝업 보러가기", attributes: [ - .font : UIFont.KorFont(style: .regular, size: 13)!, - .underlineStyle : NSUnderlineStyle.single.rawValue, - .foregroundColor : UIColor.g1000 + .font: UIFont.korFont(style: .regular, size: 13)!, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .foregroundColor: UIColor.g1000 ] ) button.setAttributedTitle(buttonTitle, for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -65,7 +64,7 @@ final class MyPageBookmarkView: UIView { // MARK: - SetUp private extension MyPageBookmarkView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in @@ -82,13 +81,13 @@ private extension MyPageBookmarkView { make.top.equalTo(countButtonView.snp.bottom).offset(16) make.leading.trailing.bottom.equalToSuperview() } - + self.addSubview(emptyLabel) emptyLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalToSuperview().inset(245) } - + self.addSubview(emptyButton) emptyButton.snp.makeConstraints { make in make.top.equalTo(emptyLabel.snp.bottom).offset(26) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift index b36c7257..7d2168e8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/CountButtonView.swift @@ -10,35 +10,33 @@ import UIKit import SnapKit final class CountButtonView: UIView { - + // MARK: - Components let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g400 return label }() - + private let dropDownImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + let buttonTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropdownButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,27 +44,27 @@ final class CountButtonView: UIView { // MARK: - SetUp private extension CountButtonView { - + func setUpConstraints() { self.addSubview(countLabel) countLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + dropdownButton.addSubview(dropDownImageView) dropDownImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.trailing.bottom.equalToSuperview() } - + dropdownButton.addSubview(buttonTitleLabel) buttonTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalTo(dropDownImageView.snp.leading).offset(-6) make.centerY.equalToSuperview() } - + self.addSubview(dropdownButton) dropdownButton.snp.makeConstraints { make in make.trailing.equalToSuperview() diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift index d71ea811..bb6347c8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct PopUpCardSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = PopUpCardSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift index 8812d5b0..33ac884c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardSectionCell.swift @@ -7,12 +7,12 @@ import UIKit -import SnapKit -import RxSwift import RxCocoa +import RxSwift +import SnapKit final class PopUpCardSectionCell: UICollectionViewCell { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -20,35 +20,34 @@ final class PopUpCardSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) - label.font = .EngFont(style: .regular, size: 11) + label.font = .engFont(style: .regular, size: 11) label.textColor = .g1000 return label }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.numberOfLines = 2 return label }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.textColor = .g400 return label }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let trailingView: UIView = UIView() - + var disposeBag = DisposeBag() - + // MARK: - init override init(frame: CGRect) { super.init(frame: frame) @@ -59,40 +58,40 @@ final class PopUpCardSectionCell: UICollectionViewCell { addHolesToCell() setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(roundedRect: contentView.bounds, cornerRadius: 4) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let leftHoleCenter = CGPoint(x: contentView.bounds.minX, y: 423) let rightHoleCenter = CGPoint(x: contentView.bounds.maxX, y: 423) - + // 구멍을 만드는 경로 생성 (반지름 6) let leftHolePath = UIBezierPath(arcCenter: leftHoleCenter, radius: 12, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: true) let rightHolePath = UIBezierPath(arcCenter: rightHoleCenter, radius: 12, startAngle: .pi / 2, endAngle: -.pi / 2, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(leftHolePath) fullPath.append(rightHolePath) fullPath.usesEvenOddFillRule = true - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd holeLayer.fillColor = UIColor.black.cgColor trailingView.layer.mask = holeLayer - + // 그림자 Layer let shadowLayer = CAShapeLayer() shadowLayer.path = fullPath.cgPath @@ -109,7 +108,7 @@ final class PopUpCardSectionCell: UICollectionViewCell { // MARK: - SetUp private extension PopUpCardSectionCell { func setUpConstraints() { - + contentView.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -119,26 +118,26 @@ private extension PopUpCardSectionCell { make.top.leading.trailing.equalToSuperview() make.height.equalTo(423) } - + trailingView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(36) make.top.trailing.equalToSuperview().inset(20) } - + trailingView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(24) make.centerX.equalToSuperview() } - + trailingView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(dateLabel.snp.bottom).offset(20) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(44) } - + trailingView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) @@ -156,16 +155,16 @@ extension PopUpCardSectionCell: Inputable { var address: String? var isBookMark: Bool } - + func injection(with input: Input) { let date = input.date ?? "" imageView.setPPImage(path: input.imagePath) - dateLabel.setLineHeightText(text: date, font: .EngFont(style: .regular, size: 13)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) + dateLabel.setLineHeightText(text: date, font: .engFont(style: .regular, size: 13)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) titleLabel.textAlignment = .center - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .regular, size: 14)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .regular, size: 14)) addressLabel.textAlignment = .center - + if input.isBookMark { bookMarkButton.setImage(UIImage(named: "icon_bookmark_fill"), for: .normal) } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift index 7b4378b6..c24d05f2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/Main/View/PopUpCardSection/PopUpCardView.swift @@ -7,12 +7,12 @@ import UIKit -import SnapKit -import RxSwift import RxCocoa +import RxSwift +import SnapKit final class PopUpCardView: UIView { - + // MARK: - Components let imageView: UIImageView = { let view = UIImageView() @@ -20,36 +20,34 @@ final class PopUpCardView: UIView { view.clipsToBounds = true return view }() - + let contentView: UIView = UIView() - + private let dateLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 11) - label.font = .EngFont(style: .regular, size: 11) + label.font = .engFont(style: .regular, size: 11) label.textColor = .g1000 return label }() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 12) - return label + return PPLabel(style: .bold, fontSize: 12) }() - + private let addressLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 12) label.textColor = .g400 return label }() - + let bookMarkButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let trailingView: UIView = UIView() - + var disposeBag = DisposeBag() - + // MARK: - init init() { super.init(frame: .zero) @@ -59,40 +57,40 @@ final class PopUpCardView: UIView { trailingView.layer.cornerRadius = 4 setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func layoutSubviews() { super.layoutSubviews() addHolesToCell() } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(roundedRect: bounds, cornerRadius: 4) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let leftHoleCenter = CGPoint(x: bounds.minX, y: 423) let rightHoleCenter = CGPoint(x: bounds.maxX, y: 423) - + // 구멍을 만드는 경로 생성 (반지름 6) let leftHolePath = UIBezierPath(arcCenter: leftHoleCenter, radius: 12, startAngle: -.pi / 2, endAngle: .pi / 2, clockwise: true) let rightHolePath = UIBezierPath(arcCenter: rightHoleCenter, radius: 12, startAngle: .pi / 2, endAngle: -.pi / 2, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(leftHolePath) fullPath.append(rightHolePath) fullPath.usesEvenOddFillRule = true - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd holeLayer.fillColor = UIColor.black.cgColor trailingView.layer.mask = holeLayer - + // 그림자 Layer let shadowLayer = CAShapeLayer() shadowLayer.path = fullPath.cgPath @@ -109,7 +107,7 @@ final class PopUpCardView: UIView { // MARK: - SetUp private extension PopUpCardView { func setUpConstraints() { - + self.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -119,25 +117,25 @@ private extension PopUpCardView { make.top.leading.trailing.equalToSuperview() make.height.equalTo(423) } - + trailingView.addSubview(bookMarkButton) bookMarkButton.snp.makeConstraints { make in make.size.equalTo(36) make.top.trailing.equalToSuperview().inset(20) } - + trailingView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(24) make.centerX.equalToSuperview() } - + trailingView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(dateLabel.snp.bottom).offset(20) } - + trailingView.addSubview(addressLabel) addressLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) @@ -156,14 +154,14 @@ extension PopUpCardView: Inputable { var address: String? var isBookMark: Bool } - + func injection(with input: Input) { let date = input.date ?? "" imageView.setPPImage(path: input.imagePath) - dateLabel.setLineHeightText(text: date, font: .EngFont(style: .regular, size: 13)) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) - addressLabel.setLineHeightText(text: input.address, font: .KorFont(style: .regular, size: 14)) - + dateLabel.setLineHeightText(text: date, font: .engFont(style: .regular, size: 13)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) + addressLabel.setLineHeightText(text: input.address, font: .korFont(style: .regular, size: 14)) + if input.isBookMark { bookMarkButton.setImage(UIImage(named: "icon_bookmark_fill"), for: .normal) } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift index 9616ae96..67a61a01 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalController.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 BookMarkPopUpViewTypeModalController: BaseViewController, View { - + typealias Reactor = BookMarkPopUpViewTypeModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = BookMarkPopUpViewTypeModalView() } @@ -48,13 +48,13 @@ extension BookMarkPopUpViewTypeModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.sortedSegmentControl.rx.selectedSegmentIndex .skip(1) .map { Reactor.Action.selectedSegmentControl(row: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -62,7 +62,7 @@ extension BookMarkPopUpViewTypeModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -70,7 +70,7 @@ extension BookMarkPopUpViewTypeModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +92,7 @@ extension BookMarkPopUpViewTypeModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(250) } @@ -106,4 +106,3 @@ extension BookMarkPopUpViewTypeModalController: PanModalPresentable { return 20 } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift index 8bd33888..8d879bdd 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class BookMarkPopUpViewTypeModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -18,13 +18,13 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { case saveButtonTapped(controller: BaseViewController) case xmarkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setSelectedIndex(row: Int) case dismissScene(controller: BaseViewController, isSave: Bool) } - + struct State { var isSetView: Bool = false var originSortedCode: String @@ -32,18 +32,17 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { var saveButtonIsEnabled: Bool = false var isSave: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - + // MARK: - init init(sortedCode: String) { self.initialState = State(originSortedCode: sortedCode) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,7 +56,7 @@ final class BookMarkPopUpViewTypeModalReactor: Reactor { return Observable.just(.dismissScene(controller: controller, isSave: false)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isSetView = false diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift index 8604d089..f57b0988 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Bookmark/ViewTypeModal/BookMarkPopUpViewTypeModalView.swift @@ -10,35 +10,32 @@ import UIKit import SnapKit final class BookMarkPopUpViewTypeModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["크게보기","모아서보기"]) - return control + return PPSegmentedControl(type: .base, segments: ["크게보기", "모아서보기"]) }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,33 +43,33 @@ final class BookMarkPopUpViewTypeModalView: UIView { // MARK: - SetUp private extension BookMarkPopUpViewTypeModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(sortedSegmentControl) sortedSegmentControl.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift index dbdb455d..3057e6d8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class FAQController: BaseViewController, View { - + typealias Reactor = FAQReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = FAQView() private var sections: [any Sectionable] = [] } @@ -29,7 +29,7 @@ extension FAQController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -51,11 +51,11 @@ private extension FAQController { mainView.contentCollectionView.register( MyPageMyCommentTitleSectionCell.self, forCellWithReuseIdentifier: MyPageMyCommentTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageListSectionCell.self, forCellWithReuseIdentifier: MyPageListSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( FAQDropdownSectionCell.self, forCellWithReuseIdentifier: FAQDropdownSectionCell.identifiers @@ -78,12 +78,12 @@ extension FAQController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -99,11 +99,11 @@ extension FAQController: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -118,7 +118,7 @@ extension FAQController: UICollectionViewDelegate, UICollectionViewDataSource { } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let cell = collectionView.cellForItem(at: indexPath) as? MyPageListSectionCell { reactor?.action.onNext(.mailInquiryCellTapped(controller: self)) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift index 1f32d7e2..ba9947a4 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/FAQReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class FAQReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,22 +20,22 @@ final class FAQReactor: Reactor { case backButtonTapped(controller: BaseViewController) case mailInquiryCellTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) case moveToMailApp(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -80,7 +80,7 @@ final class FAQReactor: Reactor { title: "고객센터 상담은 어디서 할 수 있나요?", content: "[마이페이지 > 고객문의 > 메일로 문의]에서 할 수 있으며, 주말, 공휴일을 제외한 평일 오전 9시부터 오후 6시까지 운영해요.", isOpen: false - ), + ) ]) private let qnaTitleSection = MyPageMyCommentTitleSection(inputDataList: [.init(title: "직접 문의하기")]) @@ -94,7 +94,7 @@ final class FAQReactor: Reactor { init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -109,7 +109,7 @@ final class FAQReactor: Reactor { return Observable.just(.moveToMailApp(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -128,27 +128,27 @@ final class FAQReactor: Reactor { } return newState } - + func showMailAppRecoveryAlert(controller: BaseViewController) { - + let alert = UIAlertController( title: "'Mail' 앱을 복원하겠습니까?", message: "계속하려면 App Store에서 'Mail' 앱을\n다운로드하십시오", preferredStyle: .alert ) - + alert.addAction(UIAlertAction(title: "App Store로 이동", style: .default, handler: { _ in // 📌 App Store의 메일 앱 복구 페이지 열기 if let mailAppURL = URL(string: "itms-apps://itunes.apple.com/app/id1108187098") { UIApplication.shared.open(mailAppURL, options: [:], completionHandler: nil) } })) - + alert.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil)) - + controller.present(alert, animated: true, completion: nil) } - + func getSection() -> [any Sectionable] { return [ spacing24Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift index 715c815e..2ba04ea5 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct FAQDropdownSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = FAQDropdownSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct FAQDropdownSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift index b8910b60..36493d0c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQDropdownSection/FAQDropdownSectionCell.swift @@ -7,43 +7,41 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class FAQDropdownSectionCell: UICollectionViewCell { - + // MARK: - Components let contentStackView: UIStackView = { let view = UIStackView() view.axis = .vertical return view }() - + let listContentButton = UIButton() - + let qLabel: UILabel = { let label = UILabel() - label.setLineHeightText(text: "Q", font: .EngFont(style: .bold, size: 16), lineHeight: 1) + label.setLineHeightText(text: "Q", font: .engFont(style: .bold, size: 16), lineHeight: 1) label.textColor = .blu500 return label }() - + let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropDownImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + let dropContentView: UIView = { let view = UIView() view.backgroundColor = .pb4 return view }() - + let dropContentLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 @@ -51,16 +49,16 @@ final class FAQDropdownSectionCell: UICollectionViewCell { }() var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -74,7 +72,7 @@ private extension FAQDropdownSectionCell { contentStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + listContentButton.snp.makeConstraints { make in make.height.equalTo(59).priority(.high) } @@ -94,7 +92,7 @@ private extension FAQDropdownSectionCell { make.centerY.equalToSuperview() make.trailing.equalToSuperview().inset(20) } - + dropContentView.addSubview(dropContentLabel) dropContentLabel.snp.makeConstraints { make in make.top.bottom.equalToSuperview().inset(16) @@ -112,10 +110,10 @@ extension FAQDropdownSectionCell: Inputable { var content: String? var isOpen: Bool } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 14)) - dropContentLabel.setLineHeightText(text: input.content, font: .KorFont(style: .regular, size: 14), lineHeight: 1.5) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .medium, size: 14)) + dropContentLabel.setLineHeightText(text: input.content, font: .korFont(style: .regular, size: 14), lineHeight: 1.5) dropContentLabel.lineBreakStrategy = .hangulWordPriority dropContentLabel.textColor = .g600 if input.isOpen { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift index 0ecc6ab2..77680e11 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/FAQ/View/FAQView.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class FAQView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "고객문의", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "고객문의", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,13 +37,13 @@ final class FAQView: UIView { // MARK: - SetUp private extension FAQView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift index 14b45244..e699c2d3 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageController.swift @@ -7,42 +7,42 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageController: BaseViewController, View { - + typealias Reactor = MyPageReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageView() - + private let headerView: UIView = { let view = UIView() view.backgroundColor = .w100 view.alpha = 0 return view }() - + private let settingButton: UIButton = { let button = UIButton(type: .system) button.setImage(UIImage(named: "icon_gear_white"), for: .normal) - button.tintColor = .g1000 + button.tintColor = .g1000 return button }() - + private var sections: [any Sectionable] = [] private var commentCellTapped: PublishSubject = .init() private var listCellTapped: PublishSubject = .init() - + private var isBrightImage: Bool = false - + private var scrollAlpha: CGFloat = 0 - + } // MARK: - Life Cycle @@ -51,7 +51,7 @@ extension MyPageController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = false @@ -61,7 +61,7 @@ extension MyPageController { // MARK: - SetUp private extension MyPageController { func setUp() { - + if let layout = reactor?.compositionalLayout { mainView.contentCollectionView.collectionViewLayout = layout } @@ -74,19 +74,19 @@ private extension MyPageController { mainView.contentCollectionView.register( MyPageProfileSectionCell.self, forCellWithReuseIdentifier: MyPageProfileSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageMyCommentTitleSectionCell.self, forCellWithReuseIdentifier: MyPageMyCommentTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageCommentSectionCell.self, forCellWithReuseIdentifier: MyPageCommentSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageListSectionCell.self, forCellWithReuseIdentifier: MyPageListSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( MyPageLogoutSectionCell.self, forCellWithReuseIdentifier: MyPageLogoutSectionCell.identifiers @@ -95,13 +95,13 @@ private extension MyPageController { mainView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + view.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(44) } - + view.addSubview(settingButton) settingButton.snp.makeConstraints { make in make.trailing.equalTo(headerView.snp.trailing).inset(16) @@ -118,7 +118,7 @@ extension MyPageController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + commentCellTapped .withUnretained(self) .map { (owner, index) in @@ -126,7 +126,7 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + listCellTapped .withUnretained(self) .map { (owner, title) in @@ -134,7 +134,7 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + settingButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -142,18 +142,17 @@ extension MyPageController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.settingButton.isHidden = !state.isLogin owner.sections = state.sections owner.mainView.contentCollectionView.reloadData() - } .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -171,7 +170,7 @@ extension MyPageController { owner.settingButton.tintColor = .w100 owner.systemStatusBarIsDark.accept(false) } - + } } } @@ -185,18 +184,18 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? MyPageProfileSectionCell { let originHeight = 162 + 49 + 44 + view.safeAreaInsets.top cell.updateHeight(height: originHeight) @@ -209,7 +208,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? MyPageMyCommentTitleSectionCell { cell.button.rx.tap .withUnretained(self) @@ -219,7 +218,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? MyPageLogoutSectionCell { cell.logoutButton.rx.tap .map { Reactor.Action.logoutButtonTapped } @@ -228,7 +227,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let cell = collectionView.cellForItem(at: indexPath) as? MyPageCommentSectionCell { commentCellTapped.onNext(indexPath.row) @@ -238,7 +237,7 @@ extension MyPageController: UICollectionViewDelegate, UICollectionViewDataSource listCellTapped.onNext(title) } } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { if let cell = mainView.contentCollectionView.cellForItem(at: IndexPath(row: 0, section: 0)) as? MyPageProfileSectionCell { let contentOffsetY = scrollView.contentOffset.y diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift index b15851c9..7ac24f0b 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/MyPageReactor.swift @@ -5,10 +5,10 @@ // Created by SeoJunYoung on 12/30/24. // -import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift +import UIKit final class MyPageReactor: Reactor { @@ -129,7 +129,7 @@ final class MyPageReactor: Reactor { ) ] // 내가 댓글 단 팝업 리스트 - owner.commentSection.inputDataList = response.myCommentedPopUpList.map { + owner.commentSection.inputDataList = response.myCommentedPopUpList.map { .init(popUpImagePath: $0.mainImageUrl, title: $0.popUpStoreName, popUpID: $0.popUpStoreId) } if !owner.commentSection.inputDataList.isEmpty { @@ -185,8 +185,8 @@ final class MyPageReactor: Reactor { case .logout: let service = KeyChainService() - let _ = service.deleteToken(type: .accessToken) - let _ = service.deleteToken(type: .refreshToken) + _ = service.deleteToken(type: .accessToken) + _ = service.deleteToken(type: .refreshToken) ToastMaker.createToast(message: "로그아웃 되었어요") DispatchQueue.main.async { [weak self] in self?.action.onNext(.viewWillAppear) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift index 274f90fb..f92f51fc 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyPageCommentSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageCommentSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(68), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift index 2f17bf4c..1415b4f1 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageCommentSection/MyPageCommentSectionCell.swift @@ -7,11 +7,11 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyPageCommentSectionCell: UICollectionViewCell { - + // MARK: - Components private let firstBackgroundView: UIView = { let view = UIView() @@ -20,20 +20,20 @@ final class MyPageCommentSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let imageBackgroundView: UIView = { let view = UIView() view.backgroundColor = .w100 view.layer.cornerRadius = 31 return view }() - + private let gradientView: AnimatedGradientView = { let view = AnimatedGradientView() view.isHidden = true return view }() - + private let imageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 28 @@ -41,20 +41,19 @@ final class MyPageCommentSectionCell: UICollectionViewCell { view.contentMode = .scaleAspectFill return view }() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 11) - return label + return PPLabel(style: .regular, fontSize: 11) }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -68,24 +67,24 @@ private extension MyPageCommentSectionCell { make.leading.trailing.top.equalToSuperview() make.height.equalTo(68) } - + firstBackgroundView.addSubview(gradientView) gradientView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + firstBackgroundView.addSubview(imageBackgroundView) imageBackgroundView.snp.makeConstraints { make in make.size.equalTo(62) make.center.equalToSuperview() } - + imageBackgroundView.addSubview(imageView) imageView.snp.makeConstraints { make in make.size.equalTo(56) make.center.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() @@ -100,12 +99,12 @@ extension MyPageCommentSectionCell: Inputable { var popUpID: Int64 var isFirstCell: Bool = false } - + func injection(with input: Input) { imageView.setPPImage(path: input.popUpImagePath) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 11)) titleLabel.textAlignment = .center - + if input.isFirstCell { gradientView.isHidden = false } else { @@ -115,39 +114,39 @@ extension MyPageCommentSectionCell: Inputable { } class AnimatedGradientView: UIView { - + private let gradientLayer = CAGradientLayer() - + override init(frame: CGRect) { super.init(frame: frame) setupGradient() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupGradient() } - + private func setupGradient() { // 초기 그라디언트 색상 설정 gradientLayer.colors = [ UIColor.init(hexCode: "#1570FC").cgColor, UIColor.init(hexCode: "#00E6BD").cgColor ] - + gradientLayer.startPoint = CGPoint(x: 0, y: 0) gradientLayer.endPoint = CGPoint(x: 1, y: 1) gradientLayer.frame = bounds layer.insertSublayer(gradientLayer, at: 0) - + animateGradient() } - + override func layoutSubviews() { super.layoutSubviews() gradientLayer.frame = bounds // 레이아웃 변경 시 반영 } - + private func animateGradient() { let animation = CABasicAnimation(keyPath: "colors") animation.fromValue = gradientLayer.colors @@ -158,7 +157,7 @@ class AnimatedGradientView: UIView { animation.duration = 1 // 색이 부드럽게 바뀌는 시간 animation.autoreverses = true // 원래 색으로 돌아가게 함 animation.repeatCount = .infinity // 무한 반복 - + gradientLayer.add(animation, forKey: "colorChange") } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift index b31885e8..441344c6 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyPageListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct MyPageListSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift index faa94ae6..0cd90311 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageListSection/MyPageListSectionCell.swift @@ -7,33 +7,32 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyPageListSectionCell: UICollectionViewCell { - + // MARK: - Components let titleLabel = UILabel() - + private let rightImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") return view }() - + private let subTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -47,13 +46,13 @@ private extension MyPageListSectionCell { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + contentView.addSubview(rightImageView) rightImageView.snp.makeConstraints { make in make.trailing.centerY.equalToSuperview() make.size.equalTo(22) } - + contentView.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.centerY.trailing.equalToSuperview() @@ -66,17 +65,17 @@ extension MyPageListSectionCell: Inputable { var title: String? var subTitle: String? } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .regular, size: 15)) - + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .regular, size: 15)) + if input.subTitle == nil { rightImageView.isHidden = false subTitleLabel.isHidden = true } else { rightImageView.isHidden = true subTitleLabel.isHidden = false - subTitleLabel.setLineHeightText(text: input.subTitle, font: .KorFont(style: .regular, size: 13)) + subTitleLabel.setLineHeightText(text: input.subTitle, font: .korFont(style: .regular, size: 13)) subTitleLabel.textColor = . blu500 } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift index be5246e8..89795be7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyPageLogoutSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageLogoutSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct MyPageLogoutSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift index dc46e212..3c737be7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageLogoutSection/MyPageLogoutSectionCell.swift @@ -7,30 +7,29 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyPageLogoutSectionCell: UICollectionViewCell { - + // MARK: - Components let logoutButton: PPButton = { - let button = PPButton(style: .secondary, text: "로그아웃") - return button + return PPButton(style: .secondary, text: "로그아웃") }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -51,7 +50,7 @@ extension MyPageLogoutSectionCell: Inputable { struct Input { } - + func injection(with input: Input) { } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift index e87b6626..20d2a2b2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyPageMyCommentTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageMyCommentTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct MyPageMyCommentTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift index f9820f1c..db582281 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageMyCommentTitleSection/MyPageMyCommentTitleSectionCell.swift @@ -7,34 +7,32 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyPageMyCommentTitleSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16) - return label + return PPLabel(style: .bold, fontSize: 16) }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -48,7 +46,7 @@ private extension MyPageMyCommentTitleSectionCell { titleLabel.snp.makeConstraints { make in make.centerY.leading.equalToSuperview() } - + contentView.addSubview(button) button.snp.makeConstraints { make in make.centerY.trailing.equalToSuperview() @@ -61,17 +59,17 @@ extension MyPageMyCommentTitleSectionCell: Inputable { var title: String? var buttonTitle: String? } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 16)) - + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 16)) + if input.buttonTitle != nil { let buttonTitle = NSAttributedString( string: input.buttonTitle ?? "", attributes: [ - .font : UIFont.KorFont(style: .regular, size: 13)!, - .underlineStyle : NSUnderlineStyle.single.rawValue, - .foregroundColor : UIColor.g600 + .font: UIFont.korFont(style: .regular, size: 13)!, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .foregroundColor: UIColor.g600 ] ) button.setAttributedTitle(buttonTitle, for: .normal) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift index 8276831c..430fae01 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyPageProfileSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyPageProfileSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -35,8 +35,7 @@ struct MyPageProfileSection: Sectionable { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 섹션 생성 - let section = NSCollectionLayoutSection(group: group) - - return section + + return NSCollectionLayoutSection(group: group) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift index c7edecae..639252c2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageProfileSection/MyPageProfileSectionCell.swift @@ -7,34 +7,30 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyPageProfileSectionCell: UICollectionViewCell { - + // MARK: - Components private let backGroundTrailingView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let backGroundImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + lazy var blurView: UIVisualEffectView = { let blurEffect = UIBlurEffect(style: .regular) - let view = UIVisualEffectView(effect: blurEffect) - return view + return UIVisualEffectView(effect: blurEffect) }() - + private let profileView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 32 @@ -42,73 +38,70 @@ final class MyPageProfileSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let bottomView: UIView = { let view = UIView() view.backgroundColor = .w100 return view }() - + let nickNameLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let descriptionLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let bottomHoleView: UIView = { let view = UIView() view.backgroundColor = .w100 view.alpha = 0 return view }() - + private let loginView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let loginLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 18, text: "나에게 딱 맞는\n팝업스토어 만나러 가기") label.textColor = .w100 label.numberOfLines = 2 return label }() - + let loginButton: UIButton = { let button = UIButton() button.setTitle("로그인/회원가입", for: .normal) button.backgroundColor = .w10 - button.titleLabel?.font = .KorFont(style: .medium, size: 13) + button.titleLabel?.font = .korFont(style: .medium, size: 13) button.setTitleColor(.w100, for: .normal) button.layer.cornerRadius = 4 return button }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() addHolesToCell() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() } - + var cellHeight: Constraint? var containerTopInset: Constraint? - + var isBright: PublishSubject = .init() } @@ -121,62 +114,62 @@ private extension MyPageProfileSectionCell { make.edges.equalToSuperview() cellHeight = make.height.equalTo(162 + 49 + 44).priority(.high).constraint } - + backGroundTrailingView.addSubview(backGroundImageView) backGroundImageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.size.equalTo(UIScreen.main.bounds.width) } - + backGroundTrailingView.addSubview(blurView) blurView.snp.makeConstraints { make in make.edges.equalToSuperview() } - + backGroundTrailingView.addSubview(profileView) profileView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(162) containerTopInset = make.top.equalToSuperview().constraint } - + backGroundTrailingView.addSubview(bottomView) bottomView.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview() make.height.equalTo(49) } - + backGroundTrailingView.addSubview(loginView) loginView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(162) make.bottom.equalTo(bottomView.snp.top) } - + profileView.addSubview(profileImageView) profileImageView.snp.makeConstraints { make in make.size.equalTo(64) make.top.equalToSuperview().inset(17) make.leading.equalToSuperview().inset(20) } - + profileView.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in make.centerY.equalTo(profileImageView) make.leading.equalTo(profileImageView.snp.trailing).offset(10) } - + profileView.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(profileImageView.snp.bottom).offset(25) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(bottomHoleView) bottomHoleView.snp.makeConstraints { make in make.edges.equalTo(bottomView) } - + contentView.addSubview(loginButton) loginButton.snp.makeConstraints { make in make.width.equalTo(120) @@ -184,32 +177,32 @@ private extension MyPageProfileSectionCell { make.bottom.equalToSuperview().inset(97) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(loginLabel) loginLabel.snp.makeConstraints { make in make.bottom.equalTo(loginButton.snp.top).offset(-16) make.leading.equalToSuperview().inset(20) } } - + private func addHolesToCell() { // 전체 영역 경로 let fullPath = UIBezierPath(rect: bounds) - + // 왼쪽 아래와 오른쪽 아래 구멍을 뚫을 위치 설정 (이미지뷰의 frame 위치 고려) let holeCenter = CGPoint(x: bounds.maxX / 2, y: bounds.minY) - + // 구멍을 만드는 경로 생성 (반지름 6) let holePath = UIBezierPath(arcCenter: holeCenter, radius: 12, startAngle: 0, endAngle: .pi, clockwise: true) - + // 구멍 경로를 전체 경로에서 빼기 fullPath.append(holePath) - + // 기존에 구멍을 뚫을 경로를 추가하는 레이어 let holeLayer = CAShapeLayer() holeLayer.path = fullPath.cgPath holeLayer.fillRule = .evenOdd - + // 구멍을 추가하는 서브 레이어로 삽입 bottomView.layer.mask = holeLayer } @@ -222,7 +215,7 @@ extension MyPageProfileSectionCell: Inputable { var nickName: String? var description: String? } - + func injection(with input: Input) { if input.isLogin { profileView.isHidden = false @@ -230,8 +223,8 @@ extension MyPageProfileSectionCell: Inputable { loginLabel.isHidden = true loginButton.isHidden = true blurView.isHidden = false - nickNameLabel.setLineHeightText(text: input.nickName, font: .KorFont(style: .bold, size: 16)) - descriptionLabel.setLineHeightText(text: input.description ?? "", font: .KorFont(style: .light, size: 11)) + nickNameLabel.setLineHeightText(text: input.nickName, font: .korFont(style: .bold, size: 16)) + descriptionLabel.setLineHeightText(text: input.description ?? "", font: .korFont(style: .light, size: 11)) backGroundImageView.image = nil backGroundImageView.setPPImage(path: input.profileImagePath) profileImageView.setPPImage(path: input.profileImagePath) { [weak self] in @@ -255,19 +248,19 @@ extension MyPageProfileSectionCell: Inputable { loginLabel.isHidden = false loginButton.isHidden = false blurView.isHidden = true - + } } - + func updateHeight(height: CGFloat) { cellHeight?.update(offset: height) layoutIfNeeded() } - + func updateAlpha(alpha: CGFloat) { bottomHoleView.alpha = alpha } - + func updateContentTopInset(inset: CGFloat) { containerTopInset?.update(offset: inset) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift index 967bc557..832979d9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Main/View/MyPageView.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class MyPageView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) @@ -22,7 +22,7 @@ final class MyPageView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -30,7 +30,7 @@ final class MyPageView: UIView { // MARK: - SetUp private extension MyPageView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift index d886c21b..1673c99e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentController.swift @@ -7,22 +7,22 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyCommentController: BaseViewController, View { - + typealias Reactor = MyCommentReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyCommentView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -46,7 +46,7 @@ private extension MyCommentController { } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers @@ -59,7 +59,7 @@ private extension MyCommentController { MyCommentedPopUpGridSectionCell.self, forCellWithReuseIdentifier: MyCommentedPopUpGridSectionCell.identifiers ) - + view.backgroundColor = .g50 view.addSubview(mainView) mainView.snp.makeConstraints { make in @@ -75,7 +75,7 @@ extension MyCommentController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -83,7 +83,7 @@ extension MyCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -91,7 +91,7 @@ extension MyCommentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -107,19 +107,18 @@ extension MyCommentController: UICollectionViewDelegate, UICollectionViewDataSou func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift index bc297806..83445aac 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/MyCommentReactor.swift @@ -8,37 +8,37 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyCommentReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case listTapped(controller: BaseViewController, row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case skip case moveToDetailScene(controller: BaseViewController, row: Int) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -52,17 +52,17 @@ final class MyCommentReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var listCountSection = CommentListTitleSection(inputDataList: []) private var listSection = MyCommentedPopUpGridSection(inputDataList: []) private var spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private var spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -93,7 +93,7 @@ final class MyCommentReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -113,7 +113,7 @@ final class MyCommentReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift index 2b287e57..bbe9f9af 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct ListCountButtonSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = ListCountButtonSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct ListCountButtonSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift index de479782..35d9658e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/ListCountButtonSection/ListCountButtonSectionCell.swift @@ -7,47 +7,45 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class ListCountButtonSectionCell: UICollectionViewCell { - + // MARK: - Components - + private let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g400 return label }() - + private let dropDownImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + private let buttonTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let dropdownButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -62,20 +60,20 @@ private extension ListCountButtonSectionCell { make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + dropdownButton.addSubview(dropDownImageView) dropDownImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.trailing.bottom.equalToSuperview() } - + dropdownButton.addSubview(buttonTitleLabel) buttonTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalTo(dropDownImageView.snp.leading).offset(-6) make.centerY.equalToSuperview() } - + self.addSubview(dropdownButton) dropdownButton.snp.makeConstraints { make in make.trailing.equalToSuperview() @@ -89,9 +87,9 @@ extension ListCountButtonSectionCell: Inputable { var count: Int64 var buttonTitle: String? } - + func injection(with input: Input) { - countLabel.setLineHeightText(text: "총 \(input.count)개", font: .KorFont(style: .regular, size: 13)) - buttonTitleLabel.setLineHeightText(text: input.buttonTitle, font: .KorFont(style: .regular, size: 13)) + countLabel.setLineHeightText(text: "총 \(input.count)개", font: .korFont(style: .regular, size: 13)) + buttonTitleLabel.setLineHeightText(text: input.buttonTitle, font: .korFont(style: .regular, size: 13)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift index 25bc49e9..f1cf8955 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentView.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class MyCommentView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "내가 코멘트한 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "내가 코멘트한 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,13 +37,13 @@ final class MyCommentView: UIView { // MARK: - SetUp private extension MyCommentView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift index 3b41bf85..96612580 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct MyCommentedPopUpGridSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = MyCommentedPopUpGridSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 8) / 2), @@ -34,7 +34,7 @@ struct MyCommentedPopUpGridSection: Sectionable { ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(8) - + // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift index 29a04bdf..f8dfa86a 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/Main/View/MyCommentedPopUpGridSection/MyCommentedPopUpGridSectionCell.swift @@ -7,11 +7,11 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class MyCommentedPopUpGridSectionCell: UICollectionViewCell { - + // MARK: - Components private let contentImageView: UIImageView = { let view = UIImageView() @@ -19,32 +19,31 @@ final class MyCommentedPopUpGridSectionCell: UICollectionViewCell { view.clipsToBounds = true return view }() - + private let titleLabel: UILabel = { let label = UILabel() label.textColor = .blu500 return label }() - + private let contentLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + private let dateLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -60,25 +59,25 @@ private extension MyCommentedPopUpGridSectionCell { contentView.layer.shadowOpacity = 1 contentView.layer.shadowRadius = 8 contentView.layer.shadowOffset = CGSize(width: 0, height: 2) - + contentView.addSubview(contentImageView) contentImageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(contentView.bounds.width) } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(contentImageView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(contentLabel) contentLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(12) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.top.equalTo(contentLabel.snp.bottom).offset(8) @@ -97,12 +96,12 @@ extension MyCommentedPopUpGridSectionCell: Inputable { var startDate: String? var endDate: String? } - + func injection(with input: Input) { contentImageView.setPPImage(path: input.imageURL) - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 11)) - contentLabel.setLineHeightText(text: input.content, font: .KorFont(style: .medium, size: 11)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 11)) + contentLabel.setLineHeightText(text: input.content, font: .korFont(style: .medium, size: 11)) contentLabel.numberOfLines = 2 - dateLabel.setLineHeightText(text: "\(input.startDate ?? "") ~ \(input.endDate ?? "")", font: .EngFont(style: .regular, size: 11)) + dateLabel.setLineHeightText(text: "\(input.startDate ?? "") ~ \(input.endDate ?? "")", font: .engFont(style: .regular, size: 11)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift index b107a650..04030d12 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalController.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 MyCommentSortedModalController: BaseViewController, View { - + typealias Reactor = MyCommentSortedModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyCommentSortedModalView() } @@ -48,13 +48,13 @@ extension MyCommentSortedModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.sortedSegmentControl.rx.selectedSegmentIndex .skip(1) .map { Reactor.Action.selectedSegmentControl(row: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -62,7 +62,7 @@ extension MyCommentSortedModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .compactMap { (owner, _) in @@ -70,7 +70,7 @@ extension MyCommentSortedModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +92,7 @@ extension MyCommentSortedModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(250) } @@ -106,4 +106,3 @@ extension MyCommentSortedModalController: PanModalPresentable { return 20 } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift index ec940c78..60106273 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyCommentSortedModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -18,13 +18,13 @@ final class MyCommentSortedModalReactor: Reactor { case saveButtonTapped(controller: BaseViewController) case xmarkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setSelectedIndex(row: Int) case dismissScene(controller: BaseViewController, isSave: Bool) } - + struct State { var isSetView: Bool = false var originSortedCode: String @@ -32,18 +32,17 @@ final class MyCommentSortedModalReactor: Reactor { var saveButtonIsEnabled: Bool = false var isSave: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - - + // MARK: - init init(sortedCode: String) { self.initialState = State(originSortedCode: sortedCode) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,7 +56,7 @@ final class MyCommentSortedModalReactor: Reactor { return Observable.just(.dismissScene(controller: controller, isSave: false)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isSetView = false diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift index 625953d9..f5bf99ee 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/MyComment/SortedModal/MyCommentSortedModalView.swift @@ -10,35 +10,32 @@ import UIKit import SnapKit final class MyCommentSortedModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "보기 옵션을 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["최신순","반응순"]) - return control + return PPSegmentedControl(type: .base, segments: ["최신순", "반응순"]) }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -46,33 +43,33 @@ final class MyCommentSortedModalView: UIView { // MARK: - SetUp private extension MyCommentSortedModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(sortedSegmentControl) sortedSegmentControl.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(48) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift index abcfb3d0..678153d9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageNoticeDetailController: BaseViewController, View { - + typealias Reactor = MyPageNoticeDetailReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageNoticeDetailView() } @@ -48,7 +48,7 @@ extension MyPageNoticeDetailController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -56,13 +56,13 @@ extension MyPageNoticeDetailController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - owner.mainView.titleLabel.setLineHeightText(text: state.title, font: .KorFont(style: .bold, size: 18)) - owner.mainView.dateLabel.setLineHeightText(text: state.date, font: .EngFont(style: .regular, size: 14)) - owner.mainView.contentLabel.setLineHeightText(text: state.content, font: .KorFont(style: .regular, size: 14)) + owner.mainView.titleLabel.setLineHeightText(text: state.title, font: .korFont(style: .bold, size: 18)) + owner.mainView.dateLabel.setLineHeightText(text: state.date, font: .engFont(style: .regular, size: 14)) + owner.mainView.contentLabel.setLineHeightText(text: state.content, font: .korFont(style: .regular, size: 14)) } .disposed(by: disposeBag) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift index 7fc3d1c1..02bb5176 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailReactor.swift @@ -6,44 +6,44 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageNoticeDetailReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) } - + struct State { var title: String? var date: String? var content: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var title: String? var date: String? var content: String? var noticeID: Int64 - + let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) // MARK: - init init(noticeID: Int64) { self.noticeID = noticeID self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -60,7 +60,7 @@ final class MyPageNoticeDetailReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift index 2ae6a1d2..64125f43 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/Detail/MyPageNoticeDetailView.swift @@ -10,17 +10,16 @@ import UIKit import SnapKit final class MyPageNoticeDetailView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "공지사항", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "공지사항", font: .korFont(style: .regular, size: 15)) return view }() - + let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() let dateLabel: UILabel = { let label = UILabel() @@ -32,16 +31,16 @@ final class MyPageNoticeDetailView: UIView { label.numberOfLines = 0 return label }() - + private let scrollView: UIScrollView = UIScrollView() private let contentView: UIView = UIView() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -49,26 +48,26 @@ final class MyPageNoticeDetailView: UIView { // MARK: - SetUp private extension MyPageNoticeDetailView { - + func setUpConstraints() { self.backgroundColor = .g50 self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(scrollView) scrollView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + scrollView.addSubview(contentView) contentView.snp.makeConstraints { make in make.edges.equalToSuperview() make.width.equalToSuperview() } - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift index f09947a0..10a2ffd7 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeController.swift @@ -7,22 +7,22 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageNoticeController: BaseViewController, View { - + typealias Reactor = MyPageNoticeReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageNoticeView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -32,7 +32,7 @@ extension MyPageNoticeController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,11 +50,11 @@ private extension MyPageNoticeController { mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( NoticeListSectionCell.self, forCellWithReuseIdentifier: NoticeListSectionCell.identifiers @@ -70,7 +70,7 @@ private extension MyPageNoticeController { // MARK: - Methods extension MyPageNoticeController { func bind(reactor: Reactor) { - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -78,12 +78,12 @@ extension MyPageNoticeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -91,7 +91,7 @@ extension MyPageNoticeController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -107,11 +107,11 @@ extension MyPageNoticeController: UICollectionViewDelegate, UICollectionViewData func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -120,7 +120,7 @@ extension MyPageNoticeController: UICollectionViewDelegate, UICollectionViewData guard let reactor = reactor else { return cell } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift index 5b24879d..ab4da90c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/MyPageNoticeReactor.swift @@ -8,34 +8,34 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageNoticeReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case listCellTapped(controller: BaseViewController, row: Int) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToDetailScene(controller: BaseViewController, row: Int) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -53,12 +53,11 @@ final class MyPageNoticeReactor: Reactor { private var listSection = NoticeListSection(inputDataList: []) private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -78,7 +77,7 @@ final class MyPageNoticeReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -93,7 +92,7 @@ final class MyPageNoticeReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift index cb08ae4f..874e228c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/MyPageNoticeView.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class MyPageNoticeView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "공지사항", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "공지사항", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,18 +37,18 @@ final class MyPageNoticeView: UIView { // MARK: - SetUp private extension MyPageNoticeView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } - + } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift index c95268f7..c1c6f4a9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct NoticeListSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = NoticeListSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -37,7 +37,7 @@ struct NoticeListSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift index 7f6eede7..6708b6a8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Notice/List/View/NoticeListSection/NoticeListSectionCell.swift @@ -7,23 +7,22 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class NoticeListSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 14) - return label + return PPLabel(style: .medium, fontSize: 14) }() - + private let dateLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + private let arrowImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") @@ -31,12 +30,12 @@ final class NoticeListSectionCell: UICollectionViewCell { }() let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -50,14 +49,13 @@ private extension NoticeListSectionCell { make.leading.equalToSuperview() make.top.equalToSuperview().inset(20) } - + contentView.addSubview(dateLabel) dateLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.bottom.equalToSuperview().inset(20) } - - + contentView.addSubview(arrowImageView) arrowImageView.snp.makeConstraints { make in make.size.equalTo(22) @@ -73,9 +71,9 @@ extension NoticeListSectionCell: Inputable { var date: String? var noticeID: Int64 } - + func injection(with input: Input) { - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 14)) - dateLabel.setLineHeightText(text: input.date, font: .EngFont(style: .regular, size: 12)) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .medium, size: 14)) + dateLabel.setLineHeightText(text: input.date, font: .engFont(style: .regular, size: 12)) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift index a17e4108..1ecd1e02 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalController.swift @@ -7,21 +7,21 @@ import UIKit -import SnapKit +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class CategoryEditModalController: BaseViewController, View { - + typealias Reactor = CategoryEditModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = CategoryEditModalView() - + private var sections: [any Sectionable] = [] } @@ -42,7 +42,7 @@ private extension CategoryEditModalController { mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self mainView.contentCollectionView.register(TagSectionCell.self, forCellWithReuseIdentifier: TagSectionCell.identifiers) - + view.addSubview(mainView) mainView.snp.makeConstraints { make in make.edges.equalTo(view.safeAreaLayoutGuide) @@ -57,7 +57,7 @@ extension CategoryEditModalController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.xmarkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -65,7 +65,7 @@ extension CategoryEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -73,7 +73,7 @@ extension CategoryEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -90,19 +90,18 @@ extension CategoryEditModalController: UICollectionViewDelegate, UICollectionVie func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { reactor?.action.onNext(.cellTapped(row: indexPath.row)) } @@ -113,7 +112,7 @@ extension CategoryEditModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(360) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift index f4b209f7..2fb1bac2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class CategoryEditModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,24 +20,24 @@ final class CategoryEditModalReactor: Reactor { case cellTapped(row: Int) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController, isEdit: Bool) } - + struct State { var sections: [any Sectionable] = [] var originSelectedID: [Int64] var saveButtonIsEnable: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let originSelectedID: [Int64] - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -51,17 +51,17 @@ final class CategoryEditModalReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var signUpUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) private var userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) private var tagSection = TagSection(inputDataList: []) - + // MARK: - init init(selectedID: [Int64]) { self.originSelectedID = selectedID self.initialState = State(originSelectedID: selectedID) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -90,11 +90,11 @@ final class CategoryEditModalReactor: Reactor { var keepList: [Int64] = [] var deleteList: [Int64] = [] let currentArray = tagSection.inputDataList.filter { $0.isSelected == true }.compactMap { $0.id } - for i in currentArray { - if originSelectedID.contains(i) { - keepList.append(i) + for index in currentArray { + if originSelectedID.contains(index) { + keepList.append(index) } else { - addList.append(i) + addList.append(index) } } deleteList = originSelectedID.filter { !currentArray.contains($0) } @@ -106,7 +106,7 @@ final class CategoryEditModalReactor: Reactor { .andThen(Observable.just(.moveToRecentScene(controller: controller, isEdit: true))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state let originArray = originSelectedID.sorted(by: <) @@ -118,7 +118,7 @@ final class CategoryEditModalReactor: Reactor { if isEdit { ToastMaker.createToast(message: "수정사항을 반영했어요")} controller.dismiss(animated: true) } - + if currentArray.isEmpty { newState.saveButtonIsEnable = false } else { @@ -126,7 +126,7 @@ final class CategoryEditModalReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [tagSection] } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift index 13be5401..537ab835 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/CategoryEditModal/CategoryEditModalView.swift @@ -10,36 +10,34 @@ import UIKit import SnapKit final class CategoryEditModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "관심 카테고리를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "관심 카테고리를 선택해주세요") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.isScrollEnabled = false return view }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -47,28 +45,28 @@ final class CategoryEditModalView: UIView { // MARK: - SetUp private extension CategoryEditModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(32) make.leading.trailing.equalToSuperview() make.height.equalTo(195) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift index e4467054..92b2c880 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalController.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 InfoEditModalController: BaseViewController, View { - + typealias Reactor = InfoEditModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = InfoEditModalView() } @@ -51,12 +51,12 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.genderSegmentControl.rx.selectedSegmentIndex .map { Reactor.Action.changeGender(index: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.ageButton.button.rx.tap .withUnretained(self) .map { (owner, _) in @@ -64,7 +64,7 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -72,7 +72,7 @@ extension InfoEditModalController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -92,7 +92,7 @@ extension InfoEditModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(390) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift index 252ca754..df0ae566 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class InfoEditModalReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -22,7 +22,7 @@ final class InfoEditModalReactor: Reactor { case changeAge(age: Int32) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case setGender(index: Int) @@ -30,24 +30,24 @@ final class InfoEditModalReactor: Reactor { case moveToAgeSelectedScene(controller: BaseViewController) case moveToRecentScene(controller: BaseViewController, isEdit: Bool) } - + struct State { var age: Int32 var gender: String? var isLoadView: Bool = true var saveButtonEnable: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + var originAge: Int32 var originGender: String? var currentAge: Int32 = 0 var currentGender: String? - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) // MARK: - init init(age: Int32, gender: String?) { @@ -55,7 +55,7 @@ final class InfoEditModalReactor: Reactor { self.originGender = gender self.initialState = State(age: age, gender: gender) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -74,7 +74,7 @@ final class InfoEditModalReactor: Reactor { .andThen(Observable.just(.moveToRecentScene(controller: controller, isEdit: true))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isLoadView = false @@ -96,7 +96,6 @@ final class InfoEditModalReactor: Reactor { .disposed(by: nextController.disposeBag) } - case .moveToRecentScene(let controller, let isEdit): if isEdit { ToastMaker.createToast(message: "수정사항을 반영했어요") } controller.dismiss(animated: true) @@ -106,7 +105,7 @@ final class InfoEditModalReactor: Reactor { case .setAge(let age): newState.age = age } - + if newState.gender == originGender && newState.age == originAge { newState.saveButtonEnable = false } else { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift index 0d48161e..71d05ee9 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/InfoEditModal/InfoEditModalView.swift @@ -10,50 +10,44 @@ import UIKit import SnapKit final class InfoEditModalView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "사용자 정보를 설정해주세요.") - return label + return PPLabel(style: .bold, fontSize: 18, text: "사용자 정보를 설정해주세요.") }() - + let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + private let genderTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "성별") - return label + return PPLabel(style: .regular, fontSize: 13, text: "성별") }() - + let genderSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["남성", "여성", "선택안함"]) - return control + return PPSegmentedControl(type: .base, segments: ["남성", "여성", "선택안함"]) }() - + private let ageTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "나이") - return label + return PPLabel(style: .regular, fontSize: 13, text: "나이") }() - + let ageButton: AgeSelectedButton = { - let label = AgeSelectedButton() - return label + return AgeSelectedButton() }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,46 +55,46 @@ final class InfoEditModalView: UIView { // MARK: - SetUp private extension InfoEditModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(genderTitleLabel) genderTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) } - + self.addSubview(genderSegmentControl) genderSegmentControl.snp.makeConstraints { make in make.top.equalTo(genderTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(ageTitleLabel) ageTitleLabel.snp.makeConstraints { make in make.top.equalTo(genderSegmentControl.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) } - + self.addSubview(ageButton) ageButton.snp.makeConstraints { make in make.top.equalTo(ageTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(72) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift index 3b71886f..95801cc2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditController.swift @@ -5,24 +5,24 @@ // Created by SeoJunYoung on 1/4/25. // -import UIKit import PhotosUI +import UIKit -import SnapKit -import RxCocoa -import RxSwift import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class ProfileEditController: BaseViewController, View { - + typealias Reactor = ProfileEditReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = ProfileEditView() - + var isFirstResponse: Bool = true } @@ -32,7 +32,7 @@ extension ProfileEditController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -53,60 +53,60 @@ private extension ProfileEditController { // MARK: - Methods extension ProfileEditController { func bind(reactor: Reactor) { - + mainView.rx.tapGesture() .withUnretained(self) .subscribe { (owner, _) in owner.mainView.endEditing(true) } .disposed(by: disposeBag) - + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.text .skip(1) .debounce(.milliseconds(300), scheduler: MainScheduler.asyncInstance) .map { Reactor.Action.changeNickName(nickName: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.controlEvent(.editingDidBegin) .map { Reactor.Action.beginNickName } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.nickNameTextField.rx.controlEvent(.editingDidEnd) .map { Reactor.Action.endNickName } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.text .distinctUntilChanged() .skip(1) .map { Reactor.Action.changeIntro(intro: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.didBeginEditing .map { Reactor.Action.beginIntro } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.introTextView.rx.didEndEditing .map { Reactor.Action.endIntro } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.profileImageButton.rx.tap .withUnretained(self) .subscribe(onNext: { (owner, _) in owner.showActionSheet() }) .disposed(by: disposeBag) - + mainView.categoryButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -114,7 +114,7 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.infoButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -122,18 +122,17 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .map { Reactor.Action.saveButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - - + mainView.nickNameDuplicatedCheckButton.rx.tap .map { Reactor.Action.nickNameCheckButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -141,18 +140,18 @@ extension ProfileEditController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in guard let originProfileData = state.originProfileData else { return } - + if owner.isFirstResponse { owner.mainView.profileImageView.setPPImage(path: originProfileData.profileImageUrl) owner.mainView.nickNameTextField.text = originProfileData.nickname owner.mainView.introTextView.text = originProfileData.intro } - + let categoryTitleList = originProfileData.interestCategoryList.map { $0.category } let categoryTitle: String if let firstTitle = categoryTitleList.first { @@ -163,12 +162,11 @@ extension ProfileEditController { categoryTitle = "" } owner.mainView.categoryButton.subTitleLabel - .setLineHeightText(text: categoryTitle, font: .KorFont(style: .regular, size: 13), lineHeight: 1) + .setLineHeightText(text: categoryTitle, font: .korFont(style: .regular, size: 13), lineHeight: 1) let userInfoTitle = "\(originProfileData.gender ?? "")・\(originProfileData.age)세" owner.mainView.infoButton.subTitleLabel - .setLineHeightText(text: userInfoTitle, font: .KorFont(style: .regular, size: 13) ,lineHeight: 1) - - + .setLineHeightText(text: userInfoTitle, font: .korFont(style: .regular, size: 13), lineHeight: 1) + // NickName TextField 설정 owner.mainView.nickNameTextFieldTrailingView.layer.borderColor = state.nickNameState.borderColor?.cgColor owner.mainView.nickNameClearButton.isHidden = state.nickNameState.isHiddenClearButton @@ -178,7 +176,7 @@ extension ProfileEditController { owner.mainView.nickNameTextField.textColor = state.nickNameState.textFieldTextColor owner.mainView.nickNameTextCountLabel.text = "\(owner.mainView.nickNameTextField.text?.count ?? 0) / 10자" owner.mainView.nickNameDuplicatedCheckButton.isEnabled = state.nickNameState.duplicatedCheckButtonIsEnabled - + // Intro TextView 설정 owner.mainView.introTextCountLabel.text = "\(owner.mainView.introTextView.text?.count ?? 0) / 30자" owner.mainView.introTextTrailingView.layer.borderColor = state.introState.borderColor?.cgColor @@ -187,7 +185,7 @@ extension ProfileEditController { owner.mainView.introTextCountLabel.textColor = state.introState.textColor owner.mainView.introTextView.textColor = state.introState.textFieldTextColor owner.mainView.introPlaceHolderLabel.isHidden = state.introState.placeHolderIsHidden - + owner.mainView.saveButton.isEnabled = state.saveButtonIsEnable owner.isFirstResponse = false } @@ -199,7 +197,7 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo func showActionSheet() { // ActionSheet 생성 let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - + // 버튼 추가 let takePhotoAction = UIAlertAction(title: "촬영하기", style: .default) { [weak self] _ in self?.showCamera() @@ -212,32 +210,32 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo self?.reactor?.action.onNext(.changeDefaultImage) } let cancelAction = UIAlertAction(title: "취소", style: .cancel) - + // 버튼 스타일 변경 (기본 이미지는 빨간색으로 표시) changeToDefaultImageAction.setValue(UIColor.red, forKey: "titleTextColor") - + // 버튼 추가 actionSheet.addAction(takePhotoAction) actionSheet.addAction(selectFromAlbumAction) actionSheet.addAction(changeToDefaultImageAction) actionSheet.addAction(cancelAction) - + present(actionSheet, animated: true) } - + func showCamera() { guard UIImagePickerController.isSourceTypeAvailable(.camera) else { Logger.log(message: "카메라를 사용할 수 없습니다.", category: .error) return } - + let imagePicker = UIImagePickerController() imagePicker.sourceType = .camera - + imagePicker.delegate = self present(imagePicker, animated: true) } - + // MARK: - PHPicker 실행 func showPHPicker() { var configuration = PHPickerConfiguration() @@ -247,26 +245,26 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo picker.delegate = self present(picker, animated: true) } - + // MARK: - UIImagePickerControllerDelegate - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let selectedImage = info[.originalImage] as? UIImage { handleSelectedImage(selectedImage) } picker.dismiss(animated: true) } - + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } - + // MARK: - PHPickerViewControllerDelegate func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) - + for result in results { if result.itemProvider.canLoadObject(ofClass: UIImage.self) { - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, _) in if let selectedImage = image as? UIImage { DispatchQueue.main.async { self?.handleSelectedImage(selectedImage) @@ -276,7 +274,7 @@ extension ProfileEditController: PHPickerViewControllerDelegate, UIImagePickerCo } } } - + // MARK: - 이미지 처리 func handleSelectedImage(_ image: UIImage) { // 선택한 이미지 처리 diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift index 80f0740e..5719a881 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/ProfileEditReactor.swift @@ -5,15 +5,15 @@ // Created by SeoJunYoung on 1/4/25. // -import UIKit import PhotosUI +import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class ProfileEditReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -31,7 +31,7 @@ final class ProfileEditReactor: Reactor { case saveButtonTapped case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToInfoEditScene(controller: BaseViewController) @@ -40,39 +40,39 @@ final class ProfileEditReactor: Reactor { case moveToRecentScene(controller: BaseViewController) case changeNickNameState } - + struct State { var originProfileData: GetMyProfileResponse? var saveButtonIsEnable: Bool = false var nickNameState: NickNameState = .myNickName var introState: IntroState = .validate } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var originProfileData: GetMyProfileResponse? - + var currentImage: UIImage? var isChangeImage: Bool = false var currentImagePath: String? - + var currentNickName: String? var nickNameIsActive: Bool = false - + var currentIntro: String? var introIsActive: Bool = false - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) private let imageService = PreSignedService() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -124,7 +124,7 @@ final class ProfileEditReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -150,12 +150,12 @@ final class ProfileEditReactor: Reactor { case .changeNickNameState: newState.nickNameState = checkNickNameState(text: currentNickName, isActive: nickNameIsActive) } - + let originNickName = originProfileData?.nickname ?? "" let currentNickName = currentNickName ?? "" let originIntro = originProfileData?.intro ?? "" let currentIntro = currentIntro ?? "" - + if isChangeImage || originNickName != currentNickName || originIntro != currentIntro { if newState.nickNameState == .validate || newState.nickNameState == .validateActive || newState.nickNameState == .myNickName || newState.nickNameState == .myNickNameActive { if newState.introState == .validate || newState.introState == .validateActive || newState.introState == .empty || newState.introState == .emptyActive { @@ -169,10 +169,10 @@ final class ProfileEditReactor: Reactor { } else { newState.saveButtonIsEnable = false } - + return newState } - + func uploadS3() -> Observable { if let changeImage = currentImage { let newPath = "ProfileImage/\(UUID().uuidString).jpg" @@ -206,7 +206,7 @@ final class ProfileEditReactor: Reactor { } } } - + func editProfile() -> Observable { isChangeImage = false ToastMaker.createToast(message: "내용을 저장했어요") @@ -219,7 +219,7 @@ final class ProfileEditReactor: Reactor { ) .andThen(Observable.just(.loadView)) } - + func loadProfileData() -> Observable { return userAPIUseCase.getMyProfile() .withUnretained(self) @@ -231,27 +231,27 @@ final class ProfileEditReactor: Reactor { return .loadView } } - + func checkNickNameState(text: String?, isActive: Bool) -> NickNameState { guard let text = text, let originNickName = originProfileData?.nickname else { return isActive ? .emptyActive : .empty } if originNickName == text { return isActive ? .myNickNameActive : .myNickName } - + // textEmpty Check if text.isEmpty { return isActive ? .emptyActive : .empty } - + // kor and end Check let pattern = "^[가-힣a-zA-Z\\s]+$" // 허용하는 문자만 검사 - let regex = try! NSRegularExpression(pattern: pattern) + guard let regex = try? NSRegularExpression(pattern: pattern) else { return .empty } let range = NSRange(location: 0, length: text.utf16.count) if regex.firstMatch(in: text, options: [], range: range) == nil { return isActive ? .korAndEngActive : .korAndEng } - + // textLength Check - + if text.count < 2 { return isActive ? .shortLengthActive : .shortLength } if text.count > 10 { return isActive ? .longLengthActive : .longLength } return isActive ? .checkActive : .check } - + func checkIntroState(text: String?, isActive: Bool) -> IntroState { guard let text = text else { return isActive ? .emptyActive : .empty } if text.isEmpty { return isActive ? .emptyActive : .empty } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift index aa83db1a..844b3d7c 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditListButton.swift @@ -10,31 +10,30 @@ import UIKit import SnapKit final class ProfileEditListButton: UIButton { - + // MARK: - Components let mainTitleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let subTitleLabel: UILabel = { let label = UILabel() label.textColor = .g400 return label }() - + let iconImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_right_gray") return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -42,19 +41,19 @@ final class ProfileEditListButton: UIButton { // MARK: - SetUp private extension ProfileEditListButton { - + func setUpConstraints() { self.addSubview(mainTitleLabel) mainTitleLabel.snp.makeConstraints { make in make.leading.centerY.equalToSuperview() } - + self.addSubview(iconImageView) iconImageView.snp.makeConstraints { make in make.size.equalTo(22) make.top.bottom.trailing.equalToSuperview() } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift index c1faa431..ea14a361 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/ProfileEdit/Main/View/ProfileEditView.swift @@ -10,21 +10,20 @@ import UIKit import SnapKit final class ProfileEditView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "프로필 설정", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "프로필 설정", font: .korFont(style: .regular, size: 15)) return view }() let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + private let scrollView: UIScrollView = UIScrollView() private let contentView: UIView = UIView() - + let profileImageView: UIImageView = { let view = UIImageView() view.layer.cornerRadius = 48 @@ -51,10 +50,9 @@ final class ProfileEditView: UIView { view.contentMode = .scaleAspectFit return view }() - + private let nickNameTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "별명") - return label + return PPLabel(style: .regular, fontSize: 13, text: "별명") }() let nickNameTextFieldTrailingView: UIStackView = { let view = UIStackView() @@ -71,7 +69,7 @@ final class ProfileEditView: UIView { let nickNameTextField: UITextField = { let textField = UITextField() textField.placeholder = "별명을 입력해주세요" - textField.font = .KorFont(style: .medium, size: 14) + textField.font = .korFont(style: .medium, size: 14) return textField }() let nickNameClearButton: UIButton = { @@ -98,7 +96,7 @@ final class ProfileEditView: UIView { let attributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13)!, // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g1000 // 텍스트 색상 ] @@ -106,7 +104,7 @@ final class ProfileEditView: UIView { let disabledAttributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13)!, // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g300 // 텍스트 색상 ] @@ -116,10 +114,9 @@ final class ProfileEditView: UIView { button.setAttributedTitle(disabledAttributedTitle, for: .disabled) return button }() - + private let introTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "자기소개") - return label + return PPLabel(style: .regular, fontSize: 13, text: "자기소개") }() let introTextTrailingView: UIView = { let view = UIView() @@ -133,7 +130,7 @@ final class ProfileEditView: UIView { let view = UITextView() view.textContainerInset = .zero view.contentInset = .zero - view.font = .KorFont(style: .medium, size: 14) + view.font = .korFont(style: .medium, size: 14) return view }() let introTextCountLabel: PPLabel = { @@ -143,38 +140,36 @@ final class ProfileEditView: UIView { return label }() let introDescriptionLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 12) - return label + return PPLabel(style: .medium, fontSize: 12) }() let introPlaceHolderLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "자기소개를 입력해주세요") label.textColor = .g200 return label }() - + private let customInfoTitlelabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 16, text: "맞춤정보") - return label + return PPLabel(style: .bold, fontSize: 16, text: "맞춤정보") }() - + let categoryButton: ProfileEditListButton = { let button = ProfileEditListButton() - button.mainTitleLabel.setLineHeightText(text: "관심 카테고리", font: .KorFont(style: .regular, size: 15)) + button.mainTitleLabel.setLineHeightText(text: "관심 카테고리", font: .korFont(style: .regular, size: 15)) return button }() - + let infoButton: ProfileEditListButton = { let button = ProfileEditListButton() - button.mainTitleLabel.setLineHeightText(text: "사용자 정보", font: .KorFont(style: .regular, size: 15)) + button.mainTitleLabel.setLineHeightText(text: "사용자 정보", font: .korFont(style: .regular, size: 15)) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -182,7 +177,7 @@ final class ProfileEditView: UIView { // MARK: - SetUp private extension ProfileEditView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in @@ -209,7 +204,7 @@ private extension ProfileEditView { setUpIntroView() setUpCustomInfoView() } - + func setUpProfileImageView() { contentView.addSubview(profileImageView) profileImageView.snp.makeConstraints { make in @@ -228,7 +223,7 @@ private extension ProfileEditView { make.center.equalToSuperview() } } - + func setUpNickNameView() { contentView.addSubview(nickNameTitleLabel) nickNameTitleLabel.snp.makeConstraints { make in @@ -261,7 +256,7 @@ private extension ProfileEditView { make.trailing.equalToSuperview().inset(24) } } - + func setUpIntroView() { contentView.addSubview(introTitleLabel) introTitleLabel.snp.makeConstraints { make in @@ -294,21 +289,21 @@ private extension ProfileEditView { make.leading.equalToSuperview().inset(20) } } - + func setUpCustomInfoView() { contentView.addSubview(customInfoTitlelabel) customInfoTitlelabel.snp.makeConstraints { make in make.top.equalTo(introTextTrailingView.snp.bottom).offset(27) make.leading.equalToSuperview().inset(20) } - + contentView.addSubview(categoryButton) categoryButton.snp.makeConstraints { make in make.top.equalTo(customInfoTitlelabel.snp.bottom).offset(32) make.leading.equalToSuperview().inset(22) make.trailing.equalToSuperview().inset(20) } - + contentView.addSubview(infoButton) infoButton.snp.makeConstraints { make in make.top.equalTo(categoryButton.snp.bottom).offset(32) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift index a21e7549..12bb9237 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentController.swift @@ -7,22 +7,22 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageRecentController: BaseViewController, View { - + typealias Reactor = MyPageRecentReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = MyPageRecentView() - + private var sections: [any Sectionable] = [] - + private var cellTapped: PublishSubject = .init() } @@ -32,7 +32,7 @@ extension MyPageRecentController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,11 +50,11 @@ private extension MyPageRecentController { mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( DetailSimilarSectionCell.self, forCellWithReuseIdentifier: DetailSimilarSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers @@ -74,7 +74,7 @@ extension MyPageRecentController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -82,7 +82,7 @@ extension MyPageRecentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, row) in @@ -90,13 +90,13 @@ extension MyPageRecentController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in owner.sections = state.sections if state.isReloadView { owner.mainView.contentCollectionView.reloadData() } - + } .disposed(by: disposeBag) } @@ -107,11 +107,11 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -120,7 +120,7 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData guard let reactor = reactor else { return cell } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentHeight = scrollView.contentSize.height let scrollViewHeight = scrollView.frame.size.height @@ -129,7 +129,7 @@ extension MyPageRecentController: UICollectionViewDelegate, UICollectionViewData reactor?.action.onNext(.changePage) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.section == 3 { cellTapped.onNext(indexPath.row) } } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift index d7d85365..300b6bcb 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/MyPageRecentReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageRecentReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -20,30 +20,30 @@ final class MyPageRecentReactor: Reactor { case backButtonTapped(controller: BaseViewController) case cellTapped(controller: BaseViewController, row: Int) } - + enum Mutation { case loadView case skip case moveToRecentScene(controller: BaseViewController) case moveToDetailScene(controller: BaseViewController, row: Int) } - + struct State { var sections: [any Sectionable] = [] var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var isLoading: Bool = false private var totalPage: Int32 = 0 private var currentPage: Int32 = 0 private var size: Int32 = 100 - + private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -57,16 +57,16 @@ final class MyPageRecentReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var countSection = CommentListTitleSection(inputDataList: []) private var listSection = RecentPopUpSection(inputDataList: []) private var spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -111,7 +111,7 @@ final class MyPageRecentReactor: Reactor { return Observable.just(.moveToDetailScene(controller: controller, row: row)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -130,7 +130,7 @@ final class MyPageRecentReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ spacing16Section, diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift index 0f585389..d9f8fe3b 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/MyPageRecentView.swift @@ -10,14 +10,14 @@ import UIKit import SnapKit final class MyPageRecentView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "최근 본 팝업", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "최근 본 팝업", font: .korFont(style: .regular, size: 15)) return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -28,7 +28,7 @@ final class MyPageRecentView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -36,13 +36,13 @@ final class MyPageRecentView: UIView { // MARK: - SetUp private extension MyPageRecentView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift index 70afc695..a744e56e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Recent/View/RecentPopUpSection/RecentPopUpSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct RecentPopUpSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = DetailSimilarSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .absolute((UIScreen.main.bounds.width - 40 - 8) / 2), @@ -38,8 +38,7 @@ struct RecentPopUpSection: Sectionable { let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 12 - + return section } } - diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift index fbea50cf..2cd74d2e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsController.swift @@ -7,24 +7,24 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class MyPageTermsController: BaseViewController, View { - + typealias Reactor = MyPageTermsReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "약관", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "약관", font: .korFont(style: .regular, size: 15)) return view }() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -38,15 +38,15 @@ final class MyPageTermsController: BaseViewController, View { return sections[section].getSection(section: section, env: env) } }() - + private lazy var contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: self.compositionalLayout) view.backgroundColor = .g50 return view }() - + private var sections: [any Sectionable] = [] - + private let cellTapped: PublishSubject = .init() } @@ -56,7 +56,7 @@ extension MyPageTermsController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -76,7 +76,7 @@ private extension MyPageTermsController { make.top.equalTo(headerView.snp.bottom) make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) } - + contentCollectionView.delegate = self contentCollectionView.dataSource = self contentCollectionView.register(CommentListTitleSectionCell.self, forCellWithReuseIdentifier: CommentListTitleSectionCell.identifiers) @@ -92,7 +92,7 @@ extension MyPageTermsController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -100,7 +100,7 @@ extension MyPageTermsController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map { (owner, indexPath) in @@ -108,7 +108,7 @@ extension MyPageTermsController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -124,19 +124,18 @@ extension MyPageTermsController: UICollectionViewDelegate, UICollectionViewDataS func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { cellTapped.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift index bc4fffc3..18bdf23e 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Terms/MyPageTermsReactor.swift @@ -7,47 +7,47 @@ import Foundation import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class MyPageTermsReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear - case cellTapped(indexPath: IndexPath, controller : BaseViewController) + case cellTapped(indexPath: IndexPath, controller: BaseViewController) case backButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) case moveToRecentScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private lazy var countSection = CommentListTitleSection(inputDataList: [.init(count: termsSection.dataCount)]) private var termsSection = MyPageListSection(inputDataList: [ .init(title: "서비스이용약관"), .init(title: "개인정보처리방침"), .init(title: "위치정보 이용약관") ]) - + private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -57,10 +57,10 @@ final class MyPageTermsReactor: Reactor { return Observable.just(.moveToRecentScene(controller: controller)) case .cellTapped(let indexPath, let controller): return Observable.just(.moveToDetailScene(controller: controller, indexPath: indexPath)) - + } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -75,7 +75,7 @@ final class MyPageTermsReactor: Reactor { } return newState } - + func getSections() -> [any Sectionable] { return [ spacing16Section, @@ -84,7 +84,7 @@ final class MyPageTermsReactor: Reactor { termsSection ] } - + func getContent(index: Int) -> String { if let path = Bundle.main.path(forResource: "Terms", ofType: "plist"), let dict = NSDictionary(contentsOfFile: path) as? [String: String], diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift index cf7f2d28..c0d6bdd2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalController.swift @@ -6,28 +6,28 @@ // import UIKit -import SnapKit +import PanModal +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import PanModal +import SnapKit final class WithdrawlCheckModalController: BaseViewController, View { - + typealias Reactor = WithdrawlCheckModalReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = WithdrawlCheckModalView() - + init(nickName: String?) { super.init() let title = "\(nickName ?? "")님, 팝풀 서비스를\n정말 탈퇴하시겠어요?" - mainView.titleLabel.setLineHeightText(text: title, font: .KorFont(style: .bold, size: 18), lineHeight: 1.312) + mainView.titleLabel.setLineHeightText(text: title, font: .korFont(style: .bold, size: 18), lineHeight: 1.312) mainView.titleLabel.numberOfLines = 2 } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -58,7 +58,7 @@ extension WithdrawlCheckModalController { .map { Reactor.Action.cancelButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.agreeButton.rx.tap .map { Reactor.Action.appleyButtonTapped } .bind(to: reactor.action) @@ -71,7 +71,7 @@ extension WithdrawlCheckModalController: PanModalPresentable { var panScrollable: UIScrollView? { return nil } - + var longFormHeight: PanModalHeight { return .contentHeight(370) } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift index 81094df7..51b0c6ad 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalReactor.swift @@ -6,41 +6,41 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class WithdrawlCheckModalReactor: Reactor { - + enum ModalState { case none case cancel case apply } - + // MARK: - Reactor enum Action { case cancelButtonTapped case appleyButtonTapped } - + enum Mutation { case setModalState(state: ModalState) } - + struct State { var state: ModalState = .none } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +50,7 @@ final class WithdrawlCheckModalReactor: Reactor { return Observable.just(.setModalState(state: .cancel)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift index 860c1739..bf7e88f2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/CheckModal/WithdrawlCheckModalView.swift @@ -10,48 +10,45 @@ import UIKit import SnapKit final class WithdrawlCheckModalView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18) - return label + return PPLabel(style: .bold, fontSize: 18) }() - + private let trailingView: UIView = { let view = UIView() view.backgroundColor = .g50 view.layer.cornerRadius = 4 return view }() - + let cancelButton: PPButton = { - let button = PPButton(style: .secondary, text: "취소") - return button + return PPButton(style: .secondary, text: "취소") }() - + let agreeButton: PPButton = { - let button = PPButton(style: .primary, text: "동의 후 탈퇴하기") - return button + return PPButton(style: .primary, text: "동의 후 탈퇴하기") }() - + let firstLabel: UILabel = { let text = "서비스 탈퇴 시 회원 전용 서비스 이용이 불가하며 회원 데이터는 일괄 삭제 처리돼요." let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 13), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 13), lineHeight: 1.4) label.numberOfLines = 2 label.textColor = .g600 return label }() - + let secondLabel: UILabel = { let text = "탈퇴 후에는 계정을 다시 살리거나 복구할 수 없어요." let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 13), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 13), lineHeight: 1.4) label.numberOfLines = 2 label.textColor = .g600 return label }() - + let thirdLabel: PPLabel = { let text = "작성하신 코멘트나 댓글 등의 일부 정보는 계속 남아있을 수 있어요. " let label = PPLabel(style: .regular, fontSize: 13, text: text) @@ -59,20 +56,20 @@ final class WithdrawlCheckModalView: UIView { label.textColor = .g600 return label }() - + let textStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -80,20 +77,20 @@ final class WithdrawlCheckModalView: UIView { // MARK: - SetUp private extension WithdrawlCheckModalView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(32) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(trailingView) trailingView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) @@ -101,7 +98,7 @@ private extension WithdrawlCheckModalView { make.height.equalTo(50) make.width.equalTo(108) } - + self.addSubview(agreeButton) agreeButton.snp.makeConstraints { make in make.bottom.equalToSuperview() @@ -109,13 +106,13 @@ private extension WithdrawlCheckModalView { make.trailing.equalToSuperview().inset(20) make.height.equalTo(50) } - + trailingView.addSubview(textStackView) textStackView.snp.makeConstraints { make in // make.top.leading.trailing.equalToSuperview().inset(20) make.edges.equalToSuperview().inset(20) } - + textStackView.addArrangedSubview(firstLabel) textStackView.addArrangedSubview(secondLabel) textStackView.addArrangedSubview(thirdLabel) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift index 75be91ad..8bc6c8c8 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteController.swift @@ -7,16 +7,16 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class WithdrawlCompleteController: BaseViewController { - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = WithdrawlCompleteView() } diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift index 3538fc0a..0fd5844a 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/Complete/WithdrawlCompleteView.swift @@ -10,14 +10,14 @@ import UIKit import SnapKit final class WithdrawlCompleteView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_check_fill_blue") return view }() - + private let titleLabel: PPLabel = { let text = "탈퇴 완료\n다음에 또 만나요" let label = PPLabel(style: .bold, fontSize: 20, text: text) @@ -25,28 +25,27 @@ final class WithdrawlCompleteView: UIView { label.textAlignment = .center return label }() - + private let descriptionLabel: PPLabel = { let text = "고객님이 만족하실 수 있는\n팝풀이 되도록 앞으로도 노력할게요 :)" let label = PPLabel(style: .regular, fontSize: 15, text: text) - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 15), lineHeight: 1.5) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 15), lineHeight: 1.5) label.numberOfLines = 2 label.textAlignment = .center label.textColor = .g600 return label }() - + let checkButton: PPButton = { - let button = PPButton(style: .primary, text: "확인") - return button + return PPButton(style: .primary, text: "확인") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -54,7 +53,7 @@ final class WithdrawlCompleteView: UIView { // MARK: - SetUp private extension WithdrawlCompleteView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -62,19 +61,19 @@ private extension WithdrawlCompleteView { make.top.equalToSuperview().inset(84) make.centerX.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom).offset(64) make.centerX.equalToSuperview() } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.centerX.equalToSuperview() } - + self.addSubview(checkButton) checkButton.snp.makeConstraints { make in make.leading.bottom.trailing.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift index 862397ec..de4f14c0 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct WithdrawlCheckSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = WithdrawlCheckSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift index 9d308a9d..8ef20cb3 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlCheckSectionCell.swift @@ -7,39 +7,36 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class WithdrawlCheckSectionCell: UICollectionViewCell { - + // MARK: - Components private let checkImageView: UIImageView = { - let view = UIImageView() - return view + return UIImageView() }() - + private let titleLabel: UILabel = { - let label = UILabel() - return label + return UILabel() }() - + let textView: UITextView = { let view = UITextView() view.textContainerInset = .zero view.contentInset = .zero - view.font = .KorFont(style: .medium, size: 14) + view.font = .korFont(style: .medium, size: 14) view.backgroundColor = .clear return view }() - + let cellButton: UIButton = UIButton() - + private let trailingView: UIView = { - let view = UIView() - return view + return UIView() }() - + private let textTrailingView: UIView = { let view = UIView() view.layer.cornerRadius = 4 @@ -47,36 +44,36 @@ final class WithdrawlCheckSectionCell: UICollectionViewCell { view.layer.borderColor = UIColor.g100.cgColor view.backgroundColor = .w100 view.clipsToBounds = true - + return view }() - + private let contentStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 10 return view }() - + private let placeHolderLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "탈퇴 이유를 입력해주세요") label.textColor = .g200 return label }() - + var disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() bind() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -92,7 +89,7 @@ private extension WithdrawlCheckSectionCell { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + cellButton.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalTo(checkImageView.snp.trailing).offset(8) @@ -100,12 +97,12 @@ private extension WithdrawlCheckSectionCell { make.top.bottom.equalToSuperview() make.trailing.equalToSuperview() } - + contentView.addSubview(cellButton) cellButton.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + contentView.addSubview(contentStackView) contentStackView.addArrangedSubview(cellButton) contentStackView.addArrangedSubview(trailingView) @@ -113,31 +110,30 @@ private extension WithdrawlCheckSectionCell { make.edges.equalToSuperview() } - trailingView.snp.makeConstraints { make in make.height.equalTo(120) } - + trailingView.addSubview(textTrailingView) textTrailingView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.trailing.equalToSuperview() make.top.bottom.equalToSuperview() } - + textTrailingView.addSubview(textView) textView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(70) } - + textTrailingView.addSubview(placeHolderLabel) placeHolderLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(16) make.leading.equalToSuperview().inset(20) } } - + func bind() { textView.rx.text .withUnretained(self) @@ -156,12 +152,12 @@ extension WithdrawlCheckSectionCell: Inputable { var id: Int64 var text: String? } - + func injection(with input: Input) { let image = input.isSelected ? UIImage(named: "icon_check_fill") : UIImage(named: "icon_check") checkImageView.image = image let title = input.title ?? "" - titleLabel.setLineHeightText(text: title, font: .KorFont(style: .regular, size: 14)) + titleLabel.setLineHeightText(text: title, font: .korFont(style: .regular, size: 14)) bind() if input.isSelected { if title == "기타" { diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift index 7692c604..250a933b 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/View/WithdrawlReasonView.swift @@ -10,49 +10,48 @@ import UIKit import SnapKit final class WithdrawlReasonView: UIView { - + // MARK: - Components let headerView: PPReturnHeaderView = { let view = PPReturnHeaderView() - view.headerLabel.setLineHeightText(text: "회원 탈퇴", font: .KorFont(style: .regular, size: 15)) + view.headerLabel.setLineHeightText(text: "회원 탈퇴", font: .korFont(style: .regular, size: 15)) return view }() - + private let titleLabel: UILabel = { let text = "탈퇴하려는 이유가\n무엇인가요?" let label = UILabel() - label.setLineHeightText(text: text, font: .KorFont(style: .bold, size: 20), lineHeight: 1.312) + label.setLineHeightText(text: text, font: .korFont(style: .bold, size: 20), lineHeight: 1.312) label.numberOfLines = 2 return label }() - + private let descriptionLabel: PPLabel = { let text = "알려주시는 내용을 참고해 더 나은 팝풀을\n만들어볼게요." let label = PPLabel(style: .regular, fontSize: 15, text: text) label.textColor = .g600 label.numberOfLines = 2 - label.setLineHeightText(text: text, font: .KorFont(style: .regular, size: 15), lineHeight: 1.4) + label.setLineHeightText(text: text, font: .korFont(style: .regular, size: 15), lineHeight: 1.4) return label }() - + let skipButton: PPButton = { let button = PPButton(style: .secondary, text: "건너뛰기") button.setBackgroundColor(.w100, for: .normal) return button }() - + let checkButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.backgroundColor = .g50 @@ -63,7 +62,7 @@ final class WithdrawlReasonView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -71,34 +70,34 @@ final class WithdrawlReasonView: UIView { // MARK: - SetUp private extension WithdrawlReasonView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(64) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) make.height.equalTo(50) } - + buttonStackView.addArrangedSubview(skipButton) buttonStackView.addArrangedSubview(checkButton) - + self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalTo(descriptionLabel.snp.bottom).offset(48) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift index b584ab6b..c3ecb0f2 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonController.swift @@ -7,22 +7,22 @@ import UIKit -import SnapKit -import RxCocoa -import RxSwift import ReactorKit -import RxKeyboard +import RxCocoa import RxGesture +import RxKeyboard +import RxSwift +import SnapKit final class WithdrawlReasonController: BaseViewController, View { - + typealias Reactor = WithdrawlReasonReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = WithdrawlReasonView() - + private var sections: [any Sectionable] = [] } @@ -32,7 +32,7 @@ extension WithdrawlReasonController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -50,12 +50,12 @@ private extension WithdrawlReasonController { mainView.contentCollectionView.register( WithdrawlCheckSectionCell.self, forCellWithReuseIdentifier: WithdrawlCheckSectionCell.identifiers - ) + ) mainView.contentCollectionView.register( SpacingSectionCell.self, forCellWithReuseIdentifier: SpacingSectionCell.identifiers ) - + view.backgroundColor = .g50 view.addSubview(mainView) mainView.snp.makeConstraints { make in @@ -73,7 +73,7 @@ extension WithdrawlReasonController { owner.view.endEditing(true) } .disposed(by: disposeBag) - + RxKeyboard.instance.visibleHeight .drive { [weak self] height in if height > 0 { @@ -93,7 +93,7 @@ extension WithdrawlReasonController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.backButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -101,7 +101,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.checkButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -109,7 +109,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.skipButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -117,7 +117,7 @@ extension WithdrawlReasonController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -136,24 +136,24 @@ extension WithdrawlReasonController: UICollectionViewDelegate, UICollectionViewD func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) guard let reactor = reactor else { return cell } - + if let cell = cell as? WithdrawlCheckSectionCell { cell.cellButton.rx.tap .map { Reactor.Action.cellTapped(row: indexPath.row) } .bind(to: reactor.action) .disposed(by: cell.disposeBag) - + cell.textView.rx.text .map { Reactor.Action.etcTextInput(text: $0)} .bind(to: reactor.action) diff --git a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift index 29943ce2..ca08cbf5 100644 --- a/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/MyPage/Withdrawl/SelectedReason/WithdrawlReasonReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class WithdrawlReasonReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -22,25 +22,25 @@ final class WithdrawlReasonReactor: Reactor { case skipButtonTapped(controller: BaseViewController) case checkButtonTapped(controller: BaseViewController) } - + enum Mutation { case loadView case moveToRecentScene(controller: BaseViewController) case none case moveToCompleteScene(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var buttonIsEnabled: Bool = false var isReloadView: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -54,7 +54,7 @@ final class WithdrawlReasonReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var reasonSection = WithdrawlCheckSection(inputDataList: []) private var spacing156Section = SpacingSection(inputDataList: [.init(spacing: 156)]) private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) @@ -64,7 +64,7 @@ final class WithdrawlReasonReactor: Reactor { init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -96,7 +96,7 @@ final class WithdrawlReasonReactor: Reactor { .andThen(Observable.just(.moveToCompleteScene(controller: controller))) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state newState.isReloadView = false @@ -125,9 +125,9 @@ final class WithdrawlReasonReactor: Reactor { .disposed(by: disposeBag) controller.navigationController?.pushViewController(nextController, animated: true) } - + let isEmpty = reasonSection.inputDataList.filter { $0.isSelected == true }.isEmpty - + if let etc = reasonSection.inputDataList.filter({ $0.title == "기타" }).first { if etc.isSelected { if etc.text?.isEmpty ?? true { @@ -143,7 +143,7 @@ final class WithdrawlReasonReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ reasonSection, diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift index 6db3bb92..92b01182 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SearchResultController: BaseViewController, View { - + typealias Reactor = SearchResultReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SearchResultView() private var sections: [any Sectionable] = [] private let cellTapped: PublishSubject = .init() @@ -30,7 +30,7 @@ extension SearchResultController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -45,7 +45,7 @@ private extension SearchResultController { } mainView.contentCollectionView.delegate = self mainView.contentCollectionView.dataSource = self - + mainView.contentCollectionView.register( SearchTitleSectionCell.self, forCellWithReuseIdentifier: SearchTitleSectionCell.identifiers @@ -72,7 +72,7 @@ private extension SearchResultController { // MARK: - Methods extension SearchResultController { func bind(reactor: Reactor) { - + cellTapped .withUnretained(self) .map({ (owner, indexPath) in @@ -80,7 +80,7 @@ extension SearchResultController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -103,11 +103,11 @@ extension SearchResultController: UICollectionViewDelegate, UICollectionViewData func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -122,7 +122,7 @@ extension SearchResultController: UICollectionViewDelegate, UICollectionViewData } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { cellTapped.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift index 4dcde029..f4601985 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/SearchResultReactor.swift @@ -8,31 +8,31 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SearchResultReactor: Reactor { - + // MARK: - Reactor enum Action { case returnSearch(text: String) case bookmarkButtonTapped(indexPath: IndexPath) case cellTapped(controller: BaseViewController, indexPath: IndexPath) } - + enum Mutation { case loadView case emptyView case moveToDetailScene(controller: BaseViewController, indexPath: IndexPath) } - + struct State { var sections: [any Sectionable] = [] var isEmptyResult: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private var popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) @@ -50,19 +50,19 @@ final class SearchResultReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var titleSection = SearchTitleSection(inputDataList: [.init(title: "포함된 팝업", buttonTitle: nil)]) private var searchCountSection = SearchResultCountSection(inputDataList: [.init(count: 65)]) private var searchListSection = HomeCardGridSection(inputDataList: []) private let spacing24Section = SpacingSection(inputDataList: [.init(spacing: 24)]) private let spacing16Section = SpacingSection(inputDataList: [.init(spacing: 16)]) private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -112,7 +112,7 @@ final class SearchResultReactor: Reactor { } } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -128,8 +128,7 @@ final class SearchResultReactor: Reactor { } return newState } - - + func getSection() -> [any Sectionable] { return [ spacing24Section, @@ -142,14 +141,14 @@ final class SearchResultReactor: Reactor { } func hasFinalConsonant(_ text: String) -> Bool { guard let lastCharacter = text.last else { return false } - + let unicodeValue = Int(lastCharacter.unicodeScalars.first!.value) - + // 한글 유니코드 범위 체크 let base = 0xAC00 let last = 0xD7A3 guard base...last ~= unicodeValue else { return false } - + // 종성 인덱스 계산 (받침이 있으면 1 이상) let finalConsonantIndex = (unicodeValue - base) % 28 return finalConsonantIndex != 0 diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift index 24ac0177..e3555ab5 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct SearchResultCountSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = SearchResultCountSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -38,7 +38,7 @@ struct SearchResultCountSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 5, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift index ffcf0156..d7c5d211 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultCountSection/SearchResultCountSectionCell.swift @@ -7,32 +7,32 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class SearchResultCountSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g600 return label }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -54,7 +54,7 @@ extension SearchResultCountSectionCell: Inputable { struct Input { var count: Int } - + func injection(with input: Input) { countLabel.text = "총 \(input.count)개를 찾았어요." } diff --git a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift index 74352be4..43bbd685 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/AfterSearch/View/SearchResultView.swift @@ -10,13 +10,12 @@ import UIKit import SnapKit final class SearchResultView: UIView { - + // MARK: - Components let contentCollectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) - return view + return UICollectionView(frame: .zero, collectionViewLayout: .init()) }() - + let emptyLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "검색 결과가 없어요:(\n다른 키워드로 검색해주세요") label.textAlignment = .center @@ -24,13 +23,13 @@ final class SearchResultView: UIView { label.textColor = .g400 return label }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -38,14 +37,14 @@ final class SearchResultView: UIView { // MARK: - SetUp private extension SearchResultView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in make.top.equalToSuperview().offset(56) make.leading.trailing.bottom.equalToSuperview() } - + self.addSubview(emptyLabel) emptyLabel.snp.makeConstraints { make in make.top.equalTo(contentCollectionView.snp.top).inset(193) diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift index 7b00a9b1..9cac2f2b 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchController.swift @@ -7,19 +7,19 @@ import UIKit -import SnapKit -import RxCocoa -import RxSwift import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class SearchController: BaseViewController, View { - + typealias Reactor = SearchReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SearchView() private var sections: [any Sectionable] = [] private let cellTapped: PublishSubject = .init() @@ -32,7 +32,7 @@ extension SearchController { super.viewDidLoad() setUp() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -81,7 +81,7 @@ extension SearchController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .withUnretained(self) .map({ (owner, indexPath) in @@ -89,13 +89,13 @@ extension SearchController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + pageChange .throttle(.milliseconds(1000), scheduler: MainScheduler.asyncInstance) .map { Reactor.Action.changePage } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -111,11 +111,11 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -130,7 +130,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? CancelableTagSectionCell { if searchList.isEmpty { cell.cancelButton.rx.tap @@ -151,7 +151,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource } } } - + if let cell = cell as? SearchCountTitleSectionCell { cell.sortedButton.rx.tap .withUnretained(self) @@ -161,7 +161,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource .bind(to: reactor.action) .disposed(by: cell.disposeBag) } - + if let cell = cell as? HomeCardSectionCell { cell.bookmarkButton.rx.tap .map { Reactor.Action.bookmarkButtonTapped(indexPath: indexPath)} @@ -170,7 +170,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource } return cell } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { mainView.endEditing(true) let contentHeight = scrollView.contentSize.height @@ -180,7 +180,7 @@ extension SearchController: UICollectionViewDelegate, UICollectionViewDataSource pageChange.onNext(()) } } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { cellTapped.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift index 396090d3..a8fa3122 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/SearchReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SearchReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -29,7 +29,7 @@ final class SearchReactor: Reactor { case bookmarkButtonTapped(indexPath: IndexPath) case resetSearchKeyWord } - + enum Mutation { case loadView case moveToCategoryScene(controller: BaseViewController) @@ -38,29 +38,29 @@ final class SearchReactor: Reactor { case setSearchKeyWord(text: String?) case resetSearchKeyWord } - + struct State { var sections: [any Sectionable] = [] var searchKeyWord: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var sortedIndex: Int = 1 private var filterIndex: Int = 0 - + private var currentPage: Int32 = 0 private var lastAppendPage: Int32 = 0 private var lastPage: Int32 = 0 private var isLoading: Bool = false - + let userDefaultService = UserDefaultService() private let popUpAPIUseCase = PopUpAPIUseCaseImpl(repository: PopUpAPIRepositoryImpl(provider: ProviderImpl())) private let userAPIUseCase = UserAPIUseCaseImpl(repository: UserAPIRepositoryImpl(provider: ProviderImpl())) - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -74,10 +74,10 @@ final class SearchReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private let recentKeywordTitleSection = SearchTitleSection(inputDataList: [.init(title: "최근 검색어", buttonTitle: "모두삭제")]) private var recentKeywordSection = CancelableTagSection(inputDataList: []) - + private let searchTitleSection = SearchTitleSection(inputDataList: [.init(title: "팝업스토어 찾기")]) private var searchCategorySection = CancelableTagSection(inputDataList: [ .init(title: "카테고리", isSelected: false, isCancelAble: false) @@ -89,12 +89,12 @@ final class SearchReactor: Reactor { private let spacing18Section = SpacingSection(inputDataList: [.init(spacing: 18)]) private let spacing48Section = SpacingSection(inputDataList: [.init(spacing: 48)]) private let spacing64Section = SpacingSection(inputDataList: [.init(spacing: 64)]) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { let sort = sortedIndex == 0 ? "NEWEST" : "MOST_VIEWED,MOST_COMMENTED,MOST_BOOKMARKED" @@ -179,7 +179,7 @@ final class SearchReactor: Reactor { } } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -199,7 +199,7 @@ final class SearchReactor: Reactor { } else { owner.action.onNext(.changeCategory(categoryList: state.categoryIDList, categoryTitleList: state.categoryTitleList)) } - + } if state.isReset { owner.action.onNext(.resetCategory)} }) @@ -229,7 +229,7 @@ final class SearchReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { let searchList = userDefaultService.fetchArray(key: "searchList") ?? [] if searchList.isEmpty { @@ -262,19 +262,19 @@ final class SearchReactor: Reactor { ] } } - + func setSearchList() { let searchList = userDefaultService.fetchArray(key: "searchList") ?? [] recentKeywordSection.inputDataList = searchList.map { return .init(title: $0) } } - + func appendSearchList(text: String?) { if let text = text { if !text.isEmpty { var searchList = userDefaultService.fetchArray(key: "searchList") ?? [] if searchList.contains(text) { let targetIndex = searchList.firstIndex(of: text)! - searchList.remove(at: targetIndex) + searchList.remove(at: targetIndex) } searchList = [text] + searchList userDefaultService.save(key: "searchList", value: searchList) @@ -282,19 +282,19 @@ final class SearchReactor: Reactor { } } } - + func removeSearchList(indexPath: IndexPath) { var searchList = userDefaultService.fetchArray(key: "searchList") ?? [] searchList.remove(at: indexPath.row) userDefaultService.save(key: "searchList", value: searchList) recentKeywordSection.inputDataList = searchList.map { return .init(title: $0) } } - + func resetSearchList() { userDefaultService.save(key: "searchList", value: []) recentKeywordSection.inputDataList = [] } - + func setBottomSearchList(sort: String?) -> Observable { let isOpen = filterIndex == 0 ? true : false let categorys = searchCategorySection.inputDataList.compactMap { $0.id } diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift index fb80227f..7a26b80f 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct CancelableTagSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = CancelableTagSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(100), diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift index 7a3afb15..c7773c88 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/CancelableTagSection/CancelableTagSectionCell.swift @@ -7,43 +7,41 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class CancelableTagSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 11) - return label + return PPLabel(style: .medium, fontSize: 11) }() - + let cancelButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + private let contentStackView: UIStackView = { let view = UIStackView() view.alignment = .center view.spacing = 2 return view }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -56,7 +54,7 @@ private extension CancelableTagSectionCell { contentView.layer.cornerRadius = 15.5 contentView.clipsToBounds = true contentView.layer.borderWidth = 1 - + contentView.addSubview(contentStackView) contentStackView.snp.makeConstraints { make in make.top.bottom.equalToSuperview() @@ -65,7 +63,7 @@ private extension CancelableTagSectionCell { } contentStackView.addArrangedSubview(titleLabel) contentStackView.addArrangedSubview(cancelButton) - + titleLabel.snp.makeConstraints { make in make.height.equalTo(18) } @@ -82,23 +80,23 @@ extension CancelableTagSectionCell: Inputable { var isSelected: Bool = false var isCancelAble: Bool = true } - + func injection(with input: Input) { let xmarkImage = input.isSelected ? UIImage(named: "icon_xmark_white") : UIImage(named: "icon_xmark_gray") cancelButton.setImage(xmarkImage, for: .normal) if input.isSelected { contentView.backgroundColor = .blu500 - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .bold, size: 11), lineHeight: 1.15) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .bold, size: 11), lineHeight: 1.15) titleLabel.textColor = .w100 contentView.layer.borderColor = UIColor.blu500.cgColor } else { contentView.backgroundColor = .clear - titleLabel.setLineHeightText(text: input.title, font: .KorFont(style: .medium, size: 11), lineHeight: 1.15) + titleLabel.setLineHeightText(text: input.title, font: .korFont(style: .medium, size: 11), lineHeight: 1.15) titleLabel.textColor = .g400 contentView.layer.borderColor = UIColor.g200.cgColor } cancelButton.isHidden = !input.isCancelAble - + if input.isCancelAble { contentStackView.snp.updateConstraints { make in make.trailing.equalToSuperview().inset(8) diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift index 6feaa53b..41617011 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct SearchCountTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = SearchCountTitleSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), @@ -38,7 +38,7 @@ struct SearchCountTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift index 7ae94994..bc765fe4 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchCountTitleSection/SearchCountTitleSectionCell.swift @@ -7,49 +7,47 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class SearchCountTitleSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let countLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.textColor = .g400 return label }() - + private let sortedTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13) - return label + return PPLabel(style: .regular, fontSize: 13) }() - + private let downImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") view.isUserInteractionEnabled = false return view }() - + let sortedButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -64,19 +62,19 @@ private extension SearchCountTitleSectionCell { make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + contentView.addSubview(sortedButton) sortedButton.snp.makeConstraints { make in make.trailing.equalToSuperview() make.centerY.equalToSuperview() } - + sortedButton.addSubview(sortedTitleLabel) sortedTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview() make.centerY.equalToSuperview() } - + sortedButton.addSubview(downImageView) downImageView.snp.makeConstraints { make in make.size.equalTo(22) @@ -92,7 +90,7 @@ extension SearchCountTitleSectionCell: Inputable { var count: Int64 var sortedTitle: String? } - + func injection(with input: Input) { sortedTitleLabel.text = input.sortedTitle countLabel.text = "총 \(input.count)개" diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift index aa09c76a..3731bbaa 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct SearchTitleSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = SearchTitleSectionCell - + 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 SearchTitleSection: Sectionable { // 섹션 생성 let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift index adf7d9da..63dad364 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchTitleSection/SearchTitleSectionCell.swift @@ -7,36 +7,35 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class SearchTitleSectionCell: UICollectionViewCell { - + // MARK: - Components var disposeBag = DisposeBag() - + private let sectionTitleLabel: UILabel = { let label = UILabel() - label.font = .KorFont(style: .bold, size: 16) + label.font = .korFont(style: .bold, size: 16) return label }() - + let titleButton: UIButton = { - let button = UIButton() - return button + return UIButton() }() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } - + override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() @@ -52,7 +51,7 @@ private extension SearchTitleSectionCell { make.centerY.equalToSuperview() make.height.equalTo(22) } - + self.addSubview(titleButton) titleButton.snp.makeConstraints { make in make.centerY.equalToSuperview() @@ -67,14 +66,14 @@ extension SearchTitleSectionCell: Inputable { var title: String? var buttonTitle: String? } - + func injection(with input: Input) { sectionTitleLabel.text = input.title if let buttonTitle = input.buttonTitle { titleButton.isHidden = false let attributes: [NSAttributedString.Key: Any] = [ .underlineStyle: NSUnderlineStyle.single.rawValue, - .font: UIFont.KorFont(style: .regular, size: 13)! + .font: UIFont.korFont(style: .regular, size: 13)! ] let attributedTitle = NSAttributedString(string: buttonTitle, attributes: attributes) titleButton.setAttributedTitle(attributedTitle, for: .normal) diff --git a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift index f7925427..874e3b1a 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/BeforeSearch/View/SearchView.swift @@ -10,19 +10,18 @@ import UIKit import SnapKit final class SearchView: UIView { - + // MARK: - Components 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") } @@ -30,7 +29,7 @@ final class SearchView: UIView { // MARK: - SetUp private extension SearchView { - + func setUpConstraints() { self.addSubview(contentCollectionView) contentCollectionView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift index da326158..c7ab66a9 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryController.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 SearchCategoryController: BaseViewController, View { - + typealias Reactor = SearchCategoryReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SearchCategoryView() private var sections: [any Sectionable] = [] private let cellTapped: PublishSubject = .init() @@ -59,12 +59,12 @@ extension SearchCategoryController { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + cellTapped .map { Reactor.Action.cellTapped(indexPath: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.resetButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -72,7 +72,7 @@ extension SearchCategoryController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.closeButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -80,7 +80,7 @@ extension SearchCategoryController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -88,7 +88,7 @@ extension SearchCategoryController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -105,11 +105,11 @@ extension SearchCategoryController: UICollectionViewDelegate, UICollectionViewDa func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -118,7 +118,7 @@ extension SearchCategoryController: UICollectionViewDelegate, UICollectionViewDa guard let reactor = reactor else { return cell } return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { cellTapped.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift index 8f5e4461..383d676f 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryReactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SearchCategoryReactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear @@ -21,14 +21,14 @@ final class SearchCategoryReactor: Reactor { case resetButtonTapped(controller: BaseViewController) case cellTapped(indexPath: IndexPath) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) case loadView case save(controller: BaseViewController) case reset(controller: BaseViewController) } - + struct State { var sections: [any Sectionable] = [] var categoryIDList: [Int64] = [] @@ -37,9 +37,9 @@ final class SearchCategoryReactor: Reactor { var isSave: Bool = false var isReset: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var originCategoryList: [Int64] @@ -58,13 +58,13 @@ final class SearchCategoryReactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + // MARK: - init init(originCategoryList: [Int64]) { self.initialState = State() self.originCategoryList = originCategoryList } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -89,7 +89,7 @@ final class SearchCategoryReactor: Reactor { return Observable.just(.reset(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -111,7 +111,7 @@ final class SearchCategoryReactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ tagSection diff --git a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift index 42ebb089..f8a452ca 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/CategoryController/SearchCategoryView.swift @@ -10,48 +10,45 @@ import UIKit import SnapKit final class SearchCategoryView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "카테고리를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "카테고리를 선택해주세요") }() - + let closeButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + let contentCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.isScrollEnabled = false return view }() - + let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + let resetButton: PPButton = { - let button = PPButton(style: .secondary, text: "초기화") - return button + return PPButton(style: .secondary, text: "초기화") }() - + 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") } @@ -59,14 +56,14 @@ final class SearchCategoryView: UIView { // MARK: - SetUp private extension SearchCategoryView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(12) } - + self.addSubview(closeButton) closeButton.snp.makeConstraints { make in make.size.equalTo(24) diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift index 9a16aa29..c9b9fe6f 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainController.swift @@ -7,39 +7,39 @@ import UIKit -import SnapKit +import Pageboy +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import Pageboy +import SnapKit import Tabman final class SearchMainController: BaseTabmanController, View { - + typealias Reactor = SearchMainReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SearchMainView() - + var beforeController: SearchController = { let controller = SearchController() controller.reactor = SearchReactor() return controller }() - + var afterController: SearchResultController = { let controller = SearchResultController() controller.reactor = SearchResultReactor() return controller }() - + lazy var controllers = [ beforeController, afterController ] - + var isResponseTextField: Bool = false } @@ -49,7 +49,7 @@ extension SearchMainController { super.viewDidLoad() setUp() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !isResponseTextField { @@ -57,7 +57,7 @@ extension SearchMainController { isResponseTextField = true } } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) tabBarController?.tabBar.isHidden = true @@ -95,7 +95,7 @@ extension SearchMainController { } }) .disposed(by: disposeBag) - + // mainView.searchTextField.rx.controlEvent(.editingDidEndOnExit) // .withUnretained(self) // .map { (owner, _) in @@ -129,7 +129,6 @@ extension SearchMainController { }) .disposed(by: disposeBag) - mainView.searchTextField.rx.text .withUnretained(self) .subscribe(onNext: { (owner, text) in @@ -140,7 +139,7 @@ extension SearchMainController { } }) .disposed(by: disposeBag) - + mainView.cancelButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -156,7 +155,7 @@ extension SearchMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.clearButton.rx.tap .withUnretained(self) .subscribe { (owner, _) in @@ -164,7 +163,7 @@ extension SearchMainController { owner.mainView.clearButton.isHidden = true } .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -180,18 +179,18 @@ extension SearchMainController: PageboyViewControllerDataSource, TMBarDataSource func barItem(for bar: any Tabman.TMBar, at index: Int) -> any Tabman.TMBarItemable { return TMBarItem(title: "") } - + func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { return controllers.count } - + func viewController( for pageboyViewController: Pageboy.PageboyViewController, at index: Pageboy.PageboyViewController.PageIndex ) -> UIViewController? { return controllers[index] } - + func defaultPage( for pageboyViewController: Pageboy.PageboyViewController ) -> Pageboy.PageboyViewController.Page? { diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift index 29446caa..25c83527 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainReactor.swift @@ -6,33 +6,33 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SearchMainReactor: Reactor { - + // MARK: - Reactor enum Action { case returnSearchKeyWord(text: String?) } - + enum Mutation { case setSearchKeyWord(text: String?) } - + struct State { var searchKeyword: String? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -40,7 +40,7 @@ final class SearchMainReactor: Reactor { return Observable.just(.setSearchKeyWord(text: text)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift index d04eb79d..3d3ffda9 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/Main/SearchMainView.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class SearchMainView: UIView { - + // MARK: - Components private let searchTrailingView: UIView = { let view = UIView() @@ -19,55 +19,55 @@ final class SearchMainView: UIView { view.clipsToBounds = true return view }() - + private let searchIconImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_search_gray") return view }() - + private let searchStackView: UIStackView = { let view = UIStackView() view.spacing = 4 view.alignment = .center return view }() - + let cancelButton: UIButton = { let button = UIButton(type: .system) button.setTitle("취소", for: .normal) button.setTitleColor(.g1000, for: .normal) - button.titleLabel?.font = .KorFont(style: .regular, size: 14) + button.titleLabel?.font = .korFont(style: .regular, size: 14) button.imageView?.contentMode = .scaleAspectFit return button }() - + let searchTextField: UITextField = { let view = UITextField() - view.font = .KorFont(style: .regular, size: 14) - view.setPlaceholder(text: "팝업스토어명을 입력해보세요", color: .g400, font: .KorFont(style: .regular, size: 14)!) + view.font = .korFont(style: .regular, size: 14) + view.setPlaceholder(text: "팝업스토어명을 입력해보세요", color: .g400, font: .korFont(style: .regular, size: 14)!) return view }() - + let clearButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_clearButton"), for: .normal) return button }() - + private var headerStackView: UIStackView = { let view = UIStackView() view.alignment = .center view.spacing = 16 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -75,7 +75,7 @@ final class SearchMainView: UIView { // MARK: - SetUp private extension SearchMainView { - + func setUpConstraints() { searchTrailingView.snp.makeConstraints { make in make.height.equalTo(37) @@ -96,15 +96,15 @@ private extension SearchMainView { searchStackView.addArrangedSubview(searchIconImageView) searchStackView.addArrangedSubview(searchTextField) searchStackView.addArrangedSubview(clearButton) - + searchIconImageView.snp.makeConstraints { make in make.size.equalTo(20) } - + searchTextField.snp.makeConstraints { make in make.height.equalTo(21) } - + clearButton.snp.makeConstraints { make in make.size.equalTo(16) } diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift index 12c816d6..4fb31f8a 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedController.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 SearchSortedController: BaseViewController, View { - + typealias Reactor = SearchSortedReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SearchSortedView() } @@ -51,7 +51,7 @@ extension SearchSortedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.filterSegmentControl.rx.controlEvent(.valueChanged) .withUnretained(self) .map({ (owner, _) in @@ -59,7 +59,7 @@ extension SearchSortedController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.sortedSegmentControl.rx.controlEvent(.valueChanged) .withUnretained(self) .map({ (owner, _) in @@ -67,7 +67,7 @@ extension SearchSortedController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.saveButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -75,7 +75,7 @@ extension SearchSortedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift index 747b9bfa..12dfc2d5 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SearchSortedReactor: Reactor { - + // MARK: - Reactor enum Action { case closeButtonTapped(controller: BaseViewController) @@ -18,29 +18,29 @@ final class SearchSortedReactor: Reactor { case changeSortedIndex(index: Int) case saveButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToRecentScene(controller: BaseViewController) case loadView case save(controller: BaseViewController) } - + struct State { var filterIndex: Int var sortedIndex: Int var saveButtonIsEnable: Bool = false var isSave: Bool = false } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var originFilterIndex: Int var originSortedIndex: Int private var selectedFilterIndex: Int private var selectedSortedIndex: Int - + // MARK: - init init(filterIndex: Int, sortedIndex: Int) { self.initialState = State(filterIndex: filterIndex, sortedIndex: sortedIndex) @@ -49,7 +49,7 @@ final class SearchSortedReactor: Reactor { self.selectedFilterIndex = filterIndex self.selectedSortedIndex = sortedIndex } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -65,7 +65,7 @@ final class SearchSortedReactor: Reactor { return Observable.just(.save(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -74,7 +74,7 @@ final class SearchSortedReactor: Reactor { case .loadView: newState.filterIndex = selectedFilterIndex newState.sortedIndex = selectedSortedIndex - + if selectedFilterIndex != originFilterIndex || selectedSortedIndex != originSortedIndex { newState.saveButtonIsEnable = true } else { diff --git a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift index d528513c..243560f1 100644 --- a/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift +++ b/Poppool/Poppool/Presentation/Scene/Search/SortedController/SearchSortedView.swift @@ -10,50 +10,44 @@ import UIKit import SnapKit final class SearchSortedView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "노출 순서를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "노출 순서를 선택해주세요") }() - + let closeButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + private let filterTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "노출 조건") - return label + return PPLabel(style: .regular, fontSize: 13, text: "노출 조건") }() - + let filterSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["오픈", "종료"], selectedSegmentIndex: 0) - return control + return PPSegmentedControl(type: .base, segments: ["오픈", "종료"], selectedSegmentIndex: 0) }() - + private let sortedTitleLabel: PPLabel = { - let label = PPLabel(style: .regular, fontSize: 13, text: "팝업순서") - return label + return PPLabel(style: .regular, fontSize: 13, text: "팝업순서") }() - + let sortedSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl(type: .base, segments: ["신규순", "인기순"], selectedSegmentIndex: 0) - return control + return PPSegmentedControl(type: .base, segments: ["신규순", "인기순"], selectedSegmentIndex: 0) }() - + let saveButton: PPButton = { - let button = PPButton(style: .primary, text: "저장", disabledText: "저장") - return button + return PPButton(style: .primary, text: "저장", disabledText: "저장") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -61,45 +55,45 @@ final class SearchSortedView: UIView { // MARK: - SetUp private extension SearchSortedView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().inset(20) make.top.equalToSuperview().inset(32) } - + self.addSubview(closeButton) closeButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(20) make.centerY.equalTo(titleLabel) } - + self.addSubview(filterTitleLabel) filterTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) } - + self.addSubview(filterSegmentControl) filterSegmentControl.snp.makeConstraints { make in make.top.equalTo(filterTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(sortedTitleLabel) sortedTitleLabel.snp.makeConstraints { make in make.top.equalTo(filterSegmentControl.snp.bottom).offset(20) make.leading.equalToSuperview().inset(20) } - + self.addSubview(sortedSegmentControl) sortedSegmentControl.snp.makeConstraints { make in make.top.equalTo(sortedTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(saveButton) saveButton.snp.makeConstraints { make in make.top.equalTo(sortedSegmentControl.snp.bottom).offset(32) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift index 6580a573..6fd26f5c 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainController.swift @@ -7,46 +7,46 @@ import UIKit -import SnapKit +import Pageboy +import ReactorKit import RxCocoa import RxSwift -import ReactorKit -import Pageboy +import SnapKit import Tabman final class SignUpMainController: BaseTabmanController, View { - + typealias Reactor = SignUpMainReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SignUpMainView() - + var step1Controller: SignUpStep1Controller = { let controller = SignUpStep1Controller() controller.reactor = SignUpStep1Reactor() return controller }() - + var step2Controller: SignUpStep2Controller = { let controller = SignUpStep2Controller() controller.reactor = SignUpStep2Reactor() return controller }() - + var step3Controller: SignUpStep3Controller = { let controller = SignUpStep3Controller() controller.reactor = SignUpStep3Reactor() return controller }() - + var step4Controller: SignUpStep4Controller = { let controller = SignUpStep4Controller() controller.reactor = SignUpStep4Reactor() return controller }() - + lazy var controllers = [ step1Controller, step2Controller, @@ -78,7 +78,7 @@ private extension SignUpMainController { // MARK: - Methods extension SignUpMainController { func bind(reactor: Reactor) { - + // 취소버튼 이벤트 mainView.headerView.cancelButton.rx.tap .withUnretained(self) @@ -87,7 +87,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // 뒤로가기 버튼 mainView.headerView.backButton.rx.tap .withUnretained(self) @@ -97,7 +97,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step1 button tap 이벤트 step1Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -107,7 +107,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step2 button tap 이벤트 step2Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -117,7 +117,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 button tap 이벤트 step3Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -127,7 +127,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 Skip button tap 이벤트 step3Controller.mainView.skipButton.rx.tap .withUnretained(self) @@ -137,7 +137,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 button tap 이벤트 step4Controller.mainView.completeButton.rx.tap .withUnretained(self) @@ -148,7 +148,7 @@ extension SignUpMainController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 Skip button tap 이벤트 step4Controller.mainView.skipButton.rx.tap .withUnretained(self) @@ -160,7 +160,6 @@ extension SignUpMainController { .bind(to: reactor.action) .disposed(by: disposeBag) - // step1 Terms 이벤트 step1Controller.reactor?.state .map({ (state) in @@ -169,7 +168,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step2 nickName 이벤트 step2Controller.reactor?.state .map({ state in @@ -177,7 +176,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step3 category 이벤트 step3Controller.reactor?.state .map({ state in @@ -185,7 +184,7 @@ extension SignUpMainController { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + // step4 gender 이벤트 step4Controller.reactor?.state .map({ state in @@ -205,7 +204,6 @@ extension SignUpMainController { .bind(to: reactor.action) .disposed(by: disposeBag) - reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -225,18 +223,18 @@ extension SignUpMainController: PageboyViewControllerDataSource, TMBarDataSource func barItem(for bar: any Tabman.TMBar, at index: Int) -> any Tabman.TMBarItemable { return TMBarItem(title: "") } - + func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { return controllers.count } - + func viewController( for pageboyViewController: Pageboy.PageboyViewController, at index: Pageboy.PageboyViewController.PageIndex ) -> UIViewController? { return controllers[index] } - + func defaultPage( for pageboyViewController: Pageboy.PageboyViewController ) -> Pageboy.PageboyViewController.Page? { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift index 238105b7..363a7ec0 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Main/SignUpMainReactor.swift @@ -6,11 +6,11 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpMainReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped(controller: BaseTabmanController) @@ -27,7 +27,7 @@ final class SignUpMainReactor: Reactor { case changeGender(gender: String?) case changeAge(age: Int?) } - + enum Mutation { case moveToLoginScene(controller: BaseTabmanController) case increasePageIndex(controller: BaseTabmanController, currentIndex: Int) @@ -41,7 +41,7 @@ final class SignUpMainReactor: Reactor { case setGender(gender: String?) case setAge(age: Int?) } - + struct State { var currentIndex: Int = 0 var isMarketingAgree: Bool = false @@ -52,25 +52,25 @@ final class SignUpMainReactor: Reactor { var categoryIDList: [Int64] = [] var age: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + private var authrizationCode: String? - + private var signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) private let userDefaultService = UserDefaultService() var isFirstResponderCase: Bool - + // MARK: - init init(isFirstResponderCase: Bool, authrizationCode: String?) { self.initialState = State() self.authrizationCode = authrizationCode self.isFirstResponderCase = isFirstResponderCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -105,7 +105,7 @@ final class SignUpMainReactor: Reactor { ]) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -121,9 +121,9 @@ final class SignUpMainReactor: Reactor { guard let socialType = userDefaultService.fetch(key: "socialType"), let nickName = newState.nickName, let gender = newState.gender else { return newState } - + signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) - + signUpAPIUseCase.trySignUp( nickName: nickName, gender: gender, @@ -146,7 +146,7 @@ final class SignUpMainReactor: Reactor { ToastMaker.createToast(message: "회원가입 실패:\(error.localizedDescription)") } .disposed(by: disposeBag) - + case .skipStep3(let controller, let currentIndex): if newState.categoryIDList.count >= 5 { newState.categorys = Array(newState.categoryIDList.shuffled().prefix(5)) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift index e3b9d5c4..a4928637 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Main/View/SignUpMainView.swift @@ -10,18 +10,18 @@ import UIKit import SnapKit final class SignUpMainView: UIView { - + // MARK: - Components let headerView: PPCancelHeaderView = PPCancelHeaderView() - + let progressIndicator: PPProgressIndicator = PPProgressIndicator(totalStep: 4, startPoint: 1) - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -29,7 +29,7 @@ final class SignUpMainView: UIView { // MARK: - SetUp private extension SignUpMainView { - + func setUpConstraints() { self.addSubview(headerView) headerView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift index f0ab226f..7556c572 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteController.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpCompleteController: BaseViewController, View { - + typealias Reactor = SignUpCompleteReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SignUpCompleteView() } @@ -43,7 +43,7 @@ private extension SignUpCompleteController { // MARK: - Methods extension SignUpCompleteController { func bind(reactor: Reactor) { - + mainView.bottomButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -51,7 +51,7 @@ extension SignUpCompleteController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -65,17 +65,17 @@ extension SignUpCompleteController { let categoryString = state.categoryTitles.enumerated() .map { "#\($0.element)" } .joined(separator: ", ") - + let attributedText = NSMutableAttributedString( string: categoryString, attributes: [ - .font: UIFont.KorFont(style: .bold, size: 15)!, + .font: UIFont.korFont(style: .bold, size: 15)!, .foregroundColor: UIColor.g600, NSAttributedString.Key.paragraphStyle: paragraphStyle ] ) attributedText.append(NSAttributedString(string: "와 연관된 팝업스토어 정보를 안내해드릴게요.", attributes: [ - .font: UIFont.KorFont(style: .regular, size: 15)!, + .font: UIFont.korFont(style: .regular, size: 15)!, .foregroundColor: UIColor.g600, NSAttributedString.Key.paragraphStyle: paragraphStyle ])) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift index 7c5e14a1..9269d52a 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteReactor.swift @@ -6,27 +6,27 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpCompleteReactor: Reactor { - + // MARK: - Reactor enum Action { case completeButtonTapped(controller: BaseViewController) } - + enum Mutation { case moveToHomeScene(controller: BaseViewController) } - + struct State { var nickName: String var categoryTitles: [String] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() var isFirstResponderCase: Bool @@ -35,7 +35,7 @@ final class SignUpCompleteReactor: Reactor { self.initialState = State(nickName: nickName, categoryTitles: categoryTitles) self.isFirstResponderCase = isFirstResponderCase } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -43,7 +43,7 @@ final class SignUpCompleteReactor: Reactor { return Observable.just(.moveToHomeScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case .moveToHomeScene(let controller): diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift index 73b5739c..b0ee7a2a 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/SignUpComplete/SignUpCompleteView.swift @@ -10,47 +10,43 @@ import UIKit import SnapKit final class SignUpCompleteView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "image_signUp_complete") return view }() - + private let titleTopLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "가입완료") - return label + return PPLabel(style: .bold, fontSize: 20, text: "가입완료") }() - + private let titleMiddleStackView: UIStackView = { - let view = UIStackView() - return view + return UIStackView() }() - + let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.textColor = .blu500 return label }() - + private let nickNameSubLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "님의") - return label + return PPLabel(style: .bold, fontSize: 20, text: "님의") }() - + private let titleBottomLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 20, text: "피드를 확인해보세요") - return label + return PPLabel(style: .bold, fontSize: 20, text: "피드를 확인해보세요") }() - + private let titleStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.alignment = .center return view }() - + let descriptionLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 15) label.textColor = .g600 @@ -59,18 +55,17 @@ final class SignUpCompleteView: UIView { label.textAlignment = .center return label }() - + let bottomButton: PPButton = { - let button = PPButton(style: .primary, text: "바로가기") - return button + return PPButton(style: .primary, text: "바로가기") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -78,7 +73,7 @@ final class SignUpCompleteView: UIView { // MARK: - SetUp private extension SignUpCompleteView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -86,27 +81,27 @@ private extension SignUpCompleteView { make.size.equalTo(80) make.top.equalToSuperview().inset(124) } - + titleMiddleStackView.addArrangedSubview(nickNameLabel) titleMiddleStackView.addArrangedSubview(nickNameSubLabel) - + titleStackView.addArrangedSubview(titleTopLabel) titleStackView.addArrangedSubview(titleMiddleStackView) titleStackView.addArrangedSubview(titleBottomLabel) - + self.addSubview(titleStackView) titleStackView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(imageView.snp.bottom).offset(32) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleStackView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) - + } - + self.addSubview(bottomButton) bottomButton.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift index 80cc81d2..1fd6c671 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Controller.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpStep1Controller: BaseViewController, View { - + typealias Reactor = SignUpStep1Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep1View() } @@ -44,13 +44,13 @@ private extension SignUpStep1Controller { // MARK: - Methods extension SignUpStep1Controller { func bind(reactor: Reactor) { - + // totalButton Tap 이벤트 mainView.totalButton.button.rx.tap .map { Reactor.Action.totalButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + // terms Tap 이벤트 mainView.terms1Button.button.rx.tap .map { Reactor.Action.termsButtonTapped(index: 1)} @@ -68,7 +68,7 @@ extension SignUpStep1Controller { .map { Reactor.Action.termsButtonTapped(index: 4)} .bind(to: reactor.action) .disposed(by: disposeBag) - + // terms Detail Button 이벤트 mainView.terms1Button.righticonButton.rx.tap .withUnretained(self) @@ -98,18 +98,18 @@ extension SignUpStep1Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - + // selectedIndex가 4일 경우 전체 선택 버튼 활성화 및 비활성화 if state.selectedIndex.count == 4 { owner.mainView.totalButton.isSelected.accept(true) } else { owner.mainView.totalButton.isSelected.accept(false) } - + // 현 selectedIndex에 따라 view 변경 let termsViews = [ owner.mainView.terms1Button, @@ -121,7 +121,7 @@ extension SignUpStep1Controller { let isSelected = state.selectedIndex.contains(index + 1) view.isSelected.accept(isSelected) } - + // selectedIndex가 1,2,3을 포함할 시 completeButton 활성화 if state.selectedIndex.contains(1) && state.selectedIndex.contains(2) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift index 467e416f..b4f838db 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/SignUpStep1Reactor.swift @@ -7,38 +7,38 @@ import Foundation import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep1Reactor: Reactor { - + // MARK: - Reactor enum Action { case totalButtonTapped case termsButtonTapped(index: Int) case termsRightButtonTapped(index: Int, controller: BaseViewController) } - + enum Mutation { case setTotalSelected case setSelectedIndex(index: Int) case moveToTermsDetailScene(index: Int, controller: BaseViewController) } - + struct State { var selectedIndex: [Int] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +50,7 @@ final class SignUpStep1Reactor: Reactor { return Observable.just(.moveToTermsDetailScene(index: index, controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -75,7 +75,7 @@ final class SignUpStep1Reactor: Reactor { } return newState } - + func getTitle(index: Int) -> String { if index == 1 { return "[필수] 이용약관" @@ -85,7 +85,7 @@ final class SignUpStep1Reactor: Reactor { return "[선택] 위치정보 이용약관" } } - + func getContent(index: Int) -> String { if let path = Bundle.main.path(forResource: "Terms", ofType: "plist"), let dict = NSDictionary(contentsOfFile: path) as? [String: String], diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift index 0fead2c6..3a5e5a4b 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpCheckBoxButton.swift @@ -7,33 +7,31 @@ import UIKit -import SnapKit -import RxSwift import RxCocoa +import RxSwift +import SnapKit final class SignUpCheckBoxButton: UIView { - + // MARK: - Components private let checkImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_checkBox") return view }() - + private let buttonLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 15, text: "약관에 모두 동의할게요") - return label + return PPLabel(style: .bold, fontSize: 15, text: "약관에 모두 동의할게요") }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let disposeBag = DisposeBag() - + let isSelected: BehaviorRelay = .init(value: false) - + // MARK: - init init() { super.init(frame: .zero) @@ -43,7 +41,7 @@ final class SignUpCheckBoxButton: UIView { self.layer.cornerRadius = 4 self.clipsToBounds = true } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -51,7 +49,7 @@ final class SignUpCheckBoxButton: UIView { // MARK: - SetUp private extension SignUpCheckBoxButton { - + func setUpConstraints() { self.addSubview(checkImageView) checkImageView.snp.makeConstraints { make in @@ -59,20 +57,20 @@ private extension SignUpCheckBoxButton { make.centerY.equalToSuperview() make.leading.equalTo(20) } - + self.addSubview(buttonLabel) buttonLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(checkImageView.snp.trailing).offset(8) make.trailing.equalToSuperview().inset(20) } - + self.addSubview(button) button.snp.makeConstraints { make in make.edges.equalToSuperview() } } - + func bind() { button.rx.tap .withUnretained(self) @@ -80,7 +78,7 @@ private extension SignUpCheckBoxButton { owner.isSelected.accept(!owner.isSelected.value) } .disposed(by: disposeBag) - + isSelected .withUnretained(self) .subscribe { (owner, isSelected) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift index abd7ef42..5bbab051 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpStep1View.swift @@ -10,42 +10,40 @@ import UIKit import SnapKit final class SignUpStep1View: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20, text: "서비스 이용을 위한\n약관을 확인해주세요") label.numberOfLines = 0 return label }() - + let totalButton: SignUpCheckBoxButton = { - let button = SignUpCheckBoxButton() - return button + return SignUpCheckBoxButton() }() - + let terms1Button: SignUpTermsView = SignUpTermsView(title: "[필수] 이용약관") let terms2Button: SignUpTermsView = SignUpTermsView(title: "[필수] 개인정보 수집 및 이용") let terms3Button: SignUpTermsView = SignUpTermsView(title: "[필수] 만 14세 이상") let terms4Button: SignUpTermsView = SignUpTermsView(title: "[선택] 위치정보 이용약관") - + let termsStackView: UIStackView = { let view = UIStackView() view.axis = .vertical view.spacing = 16 return view }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,7 +51,7 @@ final class SignUpStep1View: UIView { // MARK: - SetUp private extension SignUpStep1View { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -61,14 +59,14 @@ private extension SignUpStep1View { make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(56) } - + self.addSubview(totalButton) totalButton.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(49) } - + self.addSubview(termsStackView) termsStackView.snp.makeConstraints { make in make.top.equalTo(totalButton.snp.bottom).offset(36) @@ -78,14 +76,14 @@ private extension SignUpStep1View { termsStackView.addArrangedSubview(terms2Button) termsStackView.addArrangedSubview(terms3Button) termsStackView.addArrangedSubview(terms4Button) - + self.addSubview(completeButton) completeButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) make.bottom.equalToSuperview() make.height.equalTo(52) } - + terms3Button.righticonButton.isHidden = true } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift index 8230c466..cdfd3a56 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step1/View/SignUpTermsView.swift @@ -7,40 +7,39 @@ import UIKit -import SnapKit -import RxSwift import RxCocoa +import RxSwift +import SnapKit final class SignUpTermsView: UIView { - + // MARK: - Components private let imageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_check") return view }() - + private let titleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 14) label.text = "some" return label }() - + let righticonButton: UIButton = { let view = UIButton() view.setImage(UIImage(named: "icon_right_gray"), for: .normal) return view }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + let isSelected: BehaviorRelay = .init(value: false) - + let disposeBag = DisposeBag() - + // MARK: - init init(title: String?) { super.init(frame: .zero) @@ -48,7 +47,7 @@ final class SignUpTermsView: UIView { bind() titleLabel.text = title } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -56,7 +55,7 @@ final class SignUpTermsView: UIView { // MARK: - SetUp private extension SignUpTermsView { - + func setUpConstraints() { self.addSubview(imageView) imageView.snp.makeConstraints { make in @@ -64,27 +63,27 @@ private extension SignUpTermsView { make.centerY.equalToSuperview() make.leading.equalToSuperview() } - + self.addSubview(righticonButton) righticonButton.snp.makeConstraints { make in make.size.equalTo(22) make.top.bottom.trailing.equalToSuperview() } - + self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(imageView.snp.trailing).offset(8) make.trailing.equalTo(righticonButton.snp.leading) } - + self.addSubview(button) button.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview() make.trailing.equalTo(righticonButton.snp.leading) } } - + func bind() { button.rx.tap .withUnretained(self) @@ -92,7 +91,7 @@ private extension SignUpTermsView { owner.isSelected.accept(!owner.isSelected.value) } .disposed(by: disposeBag) - + isSelected .withUnretained(self) .subscribe { (owner, isSelected) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift index 1828b8a6..ce9e6152 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/IntroState.swift @@ -14,7 +14,7 @@ enum IntroState { case validateActive case longLength case longLengthActive - + var borderColor: UIColor? { switch self { case .empty, .validate: @@ -25,7 +25,7 @@ enum IntroState { return .re500 } } - + var description: String? { switch self { case .empty, .emptyActive, .validate, .validateActive: @@ -34,7 +34,7 @@ enum IntroState { return "최대 30글자까지 입력해주세요" } } - + var textColor: UIColor? { switch self { case .empty, .emptyActive, .validate, .validateActive: @@ -43,7 +43,7 @@ enum IntroState { return .re500 } } - + var textFieldTextColor: UIColor? { switch self { case .longLength, .longLengthActive: @@ -52,7 +52,7 @@ enum IntroState { return .g1000 } } - + var placeHolderIsHidden: Bool { switch self { case .empty, .emptyActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift index 964d5894..7693833f 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/NickNameState.swift @@ -26,10 +26,10 @@ enum NickNameState { case shortLengthActive case longLength case longLengthActive - + var borderColor: UIColor? { switch self { - case .empty, .duplicated, .validate, .check , .myNickName: + case .empty, .duplicated, .validate, .check, .myNickName: return .g200 case .emptyActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive: return .g1000 @@ -37,7 +37,7 @@ enum NickNameState { return .re500 } } - + var description: String? { switch self { case .empty, .emptyActive: @@ -62,18 +62,18 @@ enum NickNameState { return nil } } - + var textColor: UIColor? { switch self { case .empty, .emptyActive, .check, .checkActive: return .g500 case .length, .lengthActive, .korAndEng, .korAndEngActive, .duplicated, .duplicatedActive, .shortLength, .shortLengthActive, .longLength, .longLengthActive: return .re500 - case .validate, .validateActive , .myNickName ,.myNickNameActive: + case .validate, .validateActive, .myNickName, .myNickNameActive: return .blu500 } } - + var textFieldTextColor: UIColor? { switch self { case .length, .lengthActive, .korAndEng, .korAndEngActive, .duplicated, .duplicatedActive, .shortLength, .shortLengthActive, .longLength, .longLengthActive: @@ -82,25 +82,25 @@ enum NickNameState { return .g1000 } } - + var isHiddenClearButton: Bool { switch self { - case .lengthActive , .korAndEngActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive, .shortLengthActive, .longLengthActive: + case .lengthActive, .korAndEngActive, .duplicatedActive, .validateActive, .checkActive, .myNickNameActive, .shortLengthActive, .longLengthActive: return false default: return true } } - + var isHiddenCheckButton: Bool { switch self { - case .length , .korAndEng, .duplicated, .validate, .check, .shortLength, .longLength ,.myNickName: + case .length, .korAndEng, .duplicated, .validate, .check, .shortLength, .longLength, .myNickName: return false default: return true } } - + var isShakeAnimation: Bool { switch self { case .lengthActive, .korAndEngActive, .duplicatedActive: @@ -109,7 +109,7 @@ enum NickNameState { return false } } - + var duplicatedCheckButtonIsEnabled: Bool { switch self { case .check, .checkActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift index 3b588360..42293868 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Controller.swift @@ -7,19 +7,19 @@ import UIKit -import SnapKit -import RxCocoa -import RxSwift import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class SignUpStep2Controller: BaseViewController, View { - + typealias Reactor = SignUpStep2Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep2View() } @@ -45,29 +45,29 @@ private extension SignUpStep2Controller { // MARK: - Methods extension SignUpStep2Controller { func bind(reactor: Reactor) { - + mainView.rx.tapGesture() .withUnretained(self) .subscribe { (owner, _) in owner.view.endEditing(true) } .disposed(by: disposeBag) - + mainView.textField.rx.text .map { Reactor.Action.inputNickName(text: $0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.textField.rx.controlEvent(.editingDidBegin) .map { Reactor.Action.beginNickNameInput } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.textField.rx.controlEvent(.editingDidEnd) .map { Reactor.Action.endNickNameInput } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.clearButton.rx.tap .withUnretained(self) .map({ (owner, _) in @@ -76,16 +76,16 @@ extension SignUpStep2Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.duplicatedCheckButton.rx.tap .map { Reactor.Action.duplicatedButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in - + // duplicatedButton Active set switch state.nickNameState { case .check, .checkActive: @@ -98,11 +98,11 @@ extension SignUpStep2Controller { owner.mainView.textFieldTrailingView.layer.borderColor = state.nickNameState.borderColor?.cgColor owner.mainView.textDescriptionLabel.text = state.nickNameState.description owner.mainView.textDescriptionLabel.textColor = state.nickNameState.textColor - + // clearButton, Duplicated Button set owner.mainView.duplicatedCheckButton.isHidden = state.nickNameState.isHiddenCheckButton owner.mainView.clearButton.isHidden = state.nickNameState.isHiddenClearButton - + // count Label set if let nickName = state.nickName { owner.mainView.textCountLabel.text = "\(nickName.count) / 10자" @@ -114,8 +114,7 @@ extension SignUpStep2Controller { default: owner.mainView.textCountLabel.textColor = .g500 } - - + // completeButton isActive set switch state.nickNameState { case .validate, .validateActive: diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift index c46fd041..9604a80d 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2Reactor.swift @@ -8,11 +8,11 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep2Reactor: Reactor { - + // MARK: - Reactor enum Action { case inputNickName(text: String?) @@ -21,32 +21,32 @@ final class SignUpStep2Reactor: Reactor { case clearButtonTapped case duplicatedButtonTapped } - + enum Mutation { case setNickNameState(text: String?) case setActiveState(isActive: Bool) case setDuplicatedSet(isDuplicated: Bool) case resetNickName } - + struct State { var nickNameState: NickNameState = .empty var isActiveInput: Bool = false var nickName: String? = nil } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) private var nickName: String? - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -66,7 +66,7 @@ final class SignUpStep2Reactor: Reactor { return Observable.just(.resetNickName) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -78,7 +78,7 @@ final class SignUpStep2Reactor: Reactor { newState.isActiveInput = isActive newState.nickNameState = checkNickNameState(text: newState.nickName, isActive: newState.isActiveInput) case .setDuplicatedSet(let isDuplicated): - newState.nickNameState = isDuplicated + newState.nickNameState = isDuplicated ? newState.isActiveInput ? .duplicatedActive : .duplicated : newState.isActiveInput ? .validateActive : .validate case .resetNickName: @@ -87,24 +87,22 @@ final class SignUpStep2Reactor: Reactor { } return newState } - + func checkNickNameState(text: String?, isActive: Bool) -> NickNameState { guard let text = text else { return isActive ? .emptyActive : .empty } // textEmpty Check if text.isEmpty { return isActive ? .emptyActive : .empty } - - + // textLength Check if text.count < 2 { return isActive ? .shortLengthActive : .shortLength } if text.count > 10 { return isActive ? .longLengthActive : .longLength } - + // kor and end Check let pattern = "^[가-힣a-zA-Z\\s]+$" // 허용하는 문자만 검사 - let regex = try! NSRegularExpression(pattern: pattern) + guard let regex = try? NSRegularExpression(pattern: pattern) else { return .empty } let range = NSRange(location: 0, length: text.utf16.count) if regex.firstMatch(in: text, options: [], range: range) == nil { return isActive ? .korAndEngActive : .korAndEng } - return isActive ? .checkActive : .check } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift index 1cb8caed..984e0d56 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step2/SignUpStep2View.swift @@ -10,26 +10,26 @@ import UIKit import SnapKit final class SignUpStep2View: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20, text: "팝풀에서 사용할\n별명을 설정해볼까요?") label.numberOfLines = 0 return label }() - + private let descriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 15, text: "이후 이 별명으로 팝풀에서 활동할 예정이에요.") label.textColor = .g600 return label }() - + let completeButton: PPButton = { let button = PPButton(style: .primary, text: "확인", disabledText: "다음") button.isEnabled = false return button }() - + let textFieldTrailingView: UIStackView = { let view = UIStackView() view.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) @@ -41,33 +41,33 @@ final class SignUpStep2View: UIView { view.layer.borderWidth = 1 return view }() - + let textField: UITextField = { let textField = UITextField() textField.placeholder = "별명을 입력해주세요" - textField.font = .KorFont(style: .medium, size: 14) + textField.font = .korFont(style: .medium, size: 14) return textField }() - + let clearButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_clearButton"), for: .normal) return button }() - + let textDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "temptemp" return label }() - + let textCountLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "0/10자" label.textColor = .g500 return label }() - + let duplicatedCheckButton: UIButton = { let button = UIButton() let title = "중복체크" @@ -75,7 +75,7 @@ final class SignUpStep2View: UIView { let attributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13)!, // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g1000 // 텍스트 색상 ] @@ -83,7 +83,7 @@ final class SignUpStep2View: UIView { let disabledAttributedTitle = NSAttributedString( string: title, attributes: [ - .font: UIFont.KorFont(style: .regular, size: 13)!, // 폰트 + .font: UIFont.korFont(style: .regular, size: 13)!, // 폰트 .underlineStyle: NSUnderlineStyle.single.rawValue, // 밑줄 스타일 .foregroundColor: UIColor.g300 // 텍스트 색상 ] @@ -93,13 +93,13 @@ final class SignUpStep2View: UIView { button.setAttributedTitle(disabledAttributedTitle, for: .disabled) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -107,7 +107,7 @@ final class SignUpStep2View: UIView { // MARK: - SetUp private extension SignUpStep2View { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -115,13 +115,13 @@ private extension SignUpStep2View { make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(56) } - + self.addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(textFieldTrailingView) textFieldTrailingView.snp.makeConstraints { make in make.top.equalTo(descriptionLabel.snp.bottom).offset(48) @@ -134,26 +134,26 @@ private extension SignUpStep2View { clearButton.snp.makeConstraints { make in make.size.equalTo(16) } - + self.addSubview(textDescriptionLabel) textDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(textFieldTrailingView.snp.bottom).offset(6) make.leading.equalToSuperview().inset(24) } - + self.addSubview(textCountLabel) textCountLabel.snp.makeConstraints { make in make.centerY.equalTo(textDescriptionLabel) make.trailing.equalToSuperview().inset(24) } - + self.addSubview(completeButton) completeButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) make.bottom.equalToSuperview() make.height.equalTo(52) } - + textField.snp.makeConstraints { make in make.height.equalTo(52) } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift index 0260f4d3..4b85d7c3 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Controller.swift @@ -7,23 +7,23 @@ import UIKit -import SnapKit -import RxCocoa -import RxSwift import ReactorKit +import RxCocoa import RxGesture +import RxSwift +import SnapKit final class SignUpStep3Controller: BaseViewController, View { - + typealias Reactor = SignUpStep3Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep3View() - + private var sections: [any Sectionable] = [] - + private let selectedTag: PublishSubject = .init() } @@ -63,12 +63,12 @@ extension SignUpStep3Controller { .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + selectedTag .map { Reactor.Action.selectedTag(indexPath: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in @@ -85,19 +85,18 @@ extension SignUpStep3Controller: UICollectionViewDelegate, UICollectionViewDataS func numberOfSections(in collectionView: UICollectionView) -> Int { return sections.count } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return sections[section].dataCount } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - let cell = sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) - return cell + return sections[indexPath.section].getCell(collectionView: collectionView, indexPath: indexPath) } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedTag.onNext(indexPath) } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift index 50aff05f..30fbc30b 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/SignUpStep3Reactor.swift @@ -8,35 +8,35 @@ import UIKit import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep3Reactor: Reactor { - + // MARK: - Reactor enum Action { case viewWillAppear case selectedTag(indexPath: IndexPath) } - + enum Mutation { case loadView } - + struct State { var sections: [any Sectionable] = [] var selectedCategory: [Int64] = [] var selectedCategoryTitle: [String] = [] var categoryIDList: [Int64] = [] } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() private let signUpAPIUseCase = SignUpAPIUseCaseImpl(repository: SignUpRepositoryImpl(provider: ProviderImpl())) private var cetegoryIDList: [Int64] = [] - + lazy var compositionalLayout: UICollectionViewCompositionalLayout = { UICollectionViewCompositionalLayout { [weak self] section, env in guard let self = self else { @@ -50,14 +50,14 @@ final class SignUpStep3Reactor: Reactor { return getSection()[section].getSection(section: section, env: env) } }() - + private var categorySection: TagSection = TagSection(inputDataList: []) - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -79,14 +79,14 @@ final class SignUpStep3Reactor: Reactor { } else { ToastMaker.createToast(message: "최대 5개까지 선택할 수 있어요") } - + } else { categorySection.inputDataList[indexPath.row].isSelected.toggle() } return Observable.just(.loadView) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { @@ -98,7 +98,7 @@ final class SignUpStep3Reactor: Reactor { } return newState } - + func getSection() -> [any Sectionable] { return [ categorySection diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift index 5de5d4b5..04c18968 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/SignUpStep3View.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class SignUpStep3View: UIView { - + // MARK: - Components let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) @@ -18,64 +18,62 @@ final class SignUpStep3View: UIView { label.text = "하이" return label }() - + private let titleTopLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "님에 대해" return label }() - + private let titleBottomLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "조금 더 알려주시겠어요?" return label }() - + private let subTitleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.text = "관심이 있는 카테고리를 선택해주세요" return label }() - + private let subTitleDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "최대 5개까지 선택할 수 있어요." return label }() - + let categoryCollectionView: UICollectionView = { let view = UICollectionView(frame: .zero, collectionViewLayout: .init()) view.isScrollEnabled = false return view }() - + let skipButton: PPButton = { - let button = PPButton(style: .secondary, text: "건너뛰기") - return button + return PPButton(style: .secondary, text: "건너뛰기") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "다음", disabledText: "다음") - return button + return PPButton(style: .primary, text: "다음", disabledText: "다음") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func setNickName(nickName: String?) { nickNameLabel.text = nickName } @@ -83,7 +81,7 @@ final class SignUpStep3View: UIView { // MARK: - SetUp private extension SignUpStep3View { - + func setUpConstraints() { self.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in @@ -91,35 +89,35 @@ private extension SignUpStep3View { make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(titleTopLabel) titleTopLabel.snp.makeConstraints { make in make.top.equalTo(nickNameLabel) make.leading.equalTo(nickNameLabel.snp.trailing) make.height.equalTo(28) } - + self.addSubview(titleBottomLabel) titleBottomLabel.snp.makeConstraints { make in make.top.equalTo(titleTopLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleBottomLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(22) } - + self.addSubview(subTitleDescriptionLabel) subTitleDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(subTitleLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(18) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) @@ -127,7 +125,7 @@ private extension SignUpStep3View { } buttonStackView.addArrangedSubview(skipButton) buttonStackView.addArrangedSubview(completeButton) - + self.addSubview(categoryCollectionView) categoryCollectionView.snp.makeConstraints { make in make.top.equalTo(subTitleDescriptionLabel.snp.bottom).offset(36) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift index 58f00723..2f508831 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSection.swift @@ -10,17 +10,17 @@ import UIKit import RxSwift struct TagSection: Sectionable { - + var currentPage: PublishSubject = .init() - + typealias CellType = TagSectionCell - + var inputDataList: [CellType.Input] - + var supplementaryItems: [any SectionSupplementaryItemable]? - + var decorationItems: [any SectionDecorationItemable]? - + func setSection(section: Int, env: any NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .estimated(26), @@ -38,7 +38,7 @@ struct TagSection: Sectionable { let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) section.interGroupSpacing = 16 - + return section } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift index 6712e0d5..28e1b592 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step3/View/TagSection/TagSectionCell.swift @@ -7,25 +7,24 @@ import UIKit -import SnapKit import RxSwift +import SnapKit final class TagSectionCell: UICollectionViewCell { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .medium, fontSize: 13) - return label + return PPLabel(style: .medium, fontSize: 13) }() - + let disposeBag = DisposeBag() // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError() } @@ -37,7 +36,7 @@ private extension TagSectionCell { contentView.clipsToBounds = true contentView.layer.cornerRadius = 18 contentView.layer.borderColor = UIColor.g200.cgColor - + contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(14).priority(.high) @@ -52,19 +51,19 @@ extension TagSectionCell: Inputable { var isSelected: Bool var id: Int64? } - + func injection(with input: Input) { titleLabel.text = input.title if input.isSelected { contentView.backgroundColor = .blu500 contentView.layer.borderWidth = 0 titleLabel.textColor = .w100 - titleLabel.font = . KorFont(style: .medium, size: 13) + titleLabel.font = . korFont(style: .medium, size: 13) } else { contentView.backgroundColor = .clear contentView.layer.borderWidth = 1 titleLabel.textColor = .g400 - titleLabel.font = . KorFont(style: .medium, size: 13) + titleLabel.font = . korFont(style: .medium, size: 13) } } } diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift index b3bbf5b0..2a2d3d8b 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedController.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 AgeSelectedController: BaseViewController, View { - + typealias Reactor = AgeSelectedReactor - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = AgeSelectedView() } @@ -51,7 +51,7 @@ extension AgeSelectedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.completeButton.rx.tap .withUnretained(self) .map { (owner, _) in @@ -60,7 +60,7 @@ extension AgeSelectedController { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift index 8e1e2aef..08a1c326 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedReactor.swift @@ -6,36 +6,36 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class AgeSelectedReactor: Reactor { - + // MARK: - Reactor enum Action { case cancelButtonTapped(controller: BaseViewController) case completeButtonTapped(selectedAge: Int, controller: BaseViewController) } - + enum Mutation { case setSelectedAge(selectedAge: Int, controller: BaseViewController) case moveToRecentScene(controller: BaseViewController) } - + struct State { var selectedAge: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init(age: Int?) { self.initialState = State(selectedAge: age) } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -45,7 +45,7 @@ final class AgeSelectedReactor: Reactor { return Observable.just(.setSelectedAge(selectedAge: selectedAge, controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift index 6732f881..6322c58c 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/AgeSelectedModal/AgeSelectedView.swift @@ -10,29 +10,25 @@ import UIKit import SnapKit final class AgeSelectedView: UIView { - + // MARK: - Components private let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 18, text: "나이를 선택해주세요") - return label + return PPLabel(style: .bold, fontSize: 18, text: "나이를 선택해주세요") }() - + let picker: PPPicker = { let ageRange = (0...100).map { "\($0)세"} - let picker = PPPicker(components: ageRange) - return picker + return PPPicker(components: ageRange) }() - + let cancelButton: PPButton = { - let button = PPButton(style: .secondary, text: "취소") - return button + return PPButton(style: .secondary, text: "취소") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually @@ -44,7 +40,7 @@ final class AgeSelectedView: UIView { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -52,21 +48,21 @@ final class AgeSelectedView: UIView { // MARK: - SetUp private extension AgeSelectedView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(picker) picker.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(24) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(208) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.top.equalTo(picker.snp.bottom).offset(24) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift index 17d64901..772bf195 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Controller.swift @@ -7,18 +7,18 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SignUpStep4Controller: BaseViewController, View { - + typealias Reactor = SignUpStep4Reactor - + // MARK: - Properties var disposeBag = DisposeBag() - + var mainView = SignUpStep4View() } @@ -50,7 +50,7 @@ extension SignUpStep4Controller { }) .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.ageSelectedButton.button.rx.tap .withUnretained(self) .map { (owner, _) in @@ -58,7 +58,7 @@ extension SignUpStep4Controller { } .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.state .withUnretained(self) .subscribe { (owner, state) in diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift index 50d87d2f..02d72a7f 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/SignUpStep4Reactor.swift @@ -6,39 +6,39 @@ // import ReactorKit -import RxSwift import RxCocoa +import RxSwift final class SignUpStep4Reactor: Reactor { - + // MARK: - Reactor enum Action { case selectedGender(index: Int) case ageSelectedButtonTapped(controller: BaseViewController) case ageSelected(age: Int?) } - + enum Mutation { case setGender(index: Int) case setAge(age: Int?) case moveToAgeSelectedScene(controller: BaseViewController) } - + struct State { var selectedGenderIndex: Int = 2 var age: Int? } - + // MARK: - properties - + var initialState: State var disposeBag = DisposeBag() - + // MARK: - init init() { self.initialState = State() } - + // MARK: - Reactor Methods func mutate(action: Action) -> Observable { switch action { @@ -50,7 +50,7 @@ final class SignUpStep4Reactor: Reactor { return Observable.just(.moveToAgeSelectedScene(controller: controller)) } } - + func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift index 55ec1d8a..43774bdf 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/AgeSelectedButton.swift @@ -10,38 +10,38 @@ import UIKit import SnapKit final class AgeSelectedButton: UIView { - + // MARK: - Components private let contentStackView: UIStackView = { let view = UIStackView() view.alignment = .center return view }() - + private let defaultLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "나이를 선택해주세요") label.textColor = .g400 return label }() - + private let rightImageView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "icon_dropdown") return view }() - + private let ageTitleLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 11, text: "나이") label.textColor = .g400 return label }() - + private let ageLabel: PPLabel = { let label = PPLabel(style: .medium, fontSize: 14, text: "") label.textColor = .g1000 return label }() - + private let verticalStackView: UIStackView = { let view = UIStackView() view.axis = .vertical @@ -50,18 +50,17 @@ final class AgeSelectedButton: UIView { view.spacing = 4 return view }() - + let button: UIButton = { - let button = UIButton() - return button + return UIButton() }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -69,12 +68,12 @@ final class AgeSelectedButton: UIView { // MARK: - SetUp private extension AgeSelectedButton { - + func setUpConstraints() { self.layer.borderWidth = 1 self.layer.cornerRadius = 4 self.layer.borderColor = UIColor.g200.cgColor - + self.addSubview(contentStackView) contentStackView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) @@ -85,11 +84,11 @@ private extension AgeSelectedButton { } verticalStackView.addArrangedSubview(ageTitleLabel) verticalStackView.addArrangedSubview(ageLabel) - + contentStackView.addArrangedSubview(defaultLabel) contentStackView.addArrangedSubview(verticalStackView) contentStackView.addArrangedSubview(rightImageView) - + self.addSubview(button) button.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -101,7 +100,7 @@ extension AgeSelectedButton: Inputable { struct Input { var age: Int? } - + func injection(with input: Input) { if let age = input.age { verticalStackView.isHidden = false diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift index c9e19f9c..52453a36 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/Step4/Main/View/SignUpStep4View.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class SignUpStep4View: UIView { - + // MARK: - Components let nickNameLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) @@ -18,84 +18,80 @@ final class SignUpStep4View: UIView { label.text = "하이" return label }() - + private let titleTopLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "님에 대해" return label }() - + private let titleBottomLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 20) label.text = "조금 더 알려주시겠어요?" return label }() - + private let subTitleLabel: PPLabel = { let label = PPLabel(style: .bold, fontSize: 16) label.text = "해당되시는 성별 / 나이대를 알려주세요" return label }() - + private let subTitleDescriptionLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 12) label.text = "가장 잘 맞는 팝업스토어를 소개해드릴게요." return label }() - + private let genderTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.text = "성별" return label }() - + let genderSegmentControl: PPSegmentedControl = { - let control = PPSegmentedControl( + return PPSegmentedControl( type: .base, segments: ["남성", "여성", "선택안함"], selectedSegmentIndex: 2 ) - return control }() - + private let ageTitleLabel: PPLabel = { let label = PPLabel(style: .regular, fontSize: 13) label.text = "나이" return label }() - + let ageSelectedButton: AgeSelectedButton = { - let button = AgeSelectedButton() - return button + return AgeSelectedButton() }() - + let skipButton: PPButton = { - let button = PPButton(style: .secondary, text: "건너뛰기") - return button + return PPButton(style: .secondary, text: "건너뛰기") }() - + let completeButton: PPButton = { - let button = PPButton(style: .primary, text: "확인", disabledText: "확인") - return button + return PPButton(style: .primary, text: "확인", disabledText: "확인") }() - + private let buttonStackView: UIStackView = { let view = UIStackView() view.distribution = .fillEqually view.spacing = 12 return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func setNickName(nickName: String?) { nickNameLabel.text = nickName } @@ -103,7 +99,7 @@ final class SignUpStep4View: UIView { // MARK: - SetUp private extension SignUpStep4View { - + func setUpConstraints() { self.addSubview(nickNameLabel) nickNameLabel.snp.makeConstraints { make in @@ -111,62 +107,62 @@ private extension SignUpStep4View { make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(titleTopLabel) titleTopLabel.snp.makeConstraints { make in make.top.equalTo(nickNameLabel) make.leading.equalTo(nickNameLabel.snp.trailing) make.height.equalTo(28) } - + self.addSubview(titleBottomLabel) titleBottomLabel.snp.makeConstraints { make in make.top.equalTo(titleTopLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(28) } - + self.addSubview(subTitleLabel) subTitleLabel.snp.makeConstraints { make in make.top.equalTo(titleBottomLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(22) } - + self.addSubview(subTitleDescriptionLabel) subTitleDescriptionLabel.snp.makeConstraints { make in make.top.equalTo(subTitleLabel.snp.bottom) make.leading.equalToSuperview().inset(20) make.height.equalTo(18) } - + self.addSubview(genderTitleLabel) genderTitleLabel.snp.makeConstraints { make in make.top.equalTo(subTitleDescriptionLabel.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) make.height.equalTo(20) } - + self.addSubview(genderSegmentControl) genderSegmentControl.snp.makeConstraints { make in make.top.equalTo(genderTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) } - + self.addSubview(ageTitleLabel) ageTitleLabel.snp.makeConstraints { make in make.top.equalTo(genderSegmentControl.snp.bottom).offset(36) make.leading.equalToSuperview().inset(20) make.height.equalTo(20) } - + self.addSubview(ageSelectedButton) ageSelectedButton.snp.makeConstraints { make in make.top.equalTo(ageTitleLabel.snp.bottom).offset(8) make.leading.trailing.equalToSuperview().inset(20) make.height.equalTo(72) } - + self.addSubview(buttonStackView) buttonStackView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(20) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift b/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift index 987ddee1..79f1c5f2 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailController.swift @@ -7,32 +7,32 @@ import UIKit -import SnapKit import RxCocoa import RxSwift +import SnapKit final class TermsDetailController: BaseViewController { - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = TermsDetailView() - + init(title: String?, content: String?) { super.init() let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.2 let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.KorFont(style: .regular, size: 14), + .font: UIFont.korFont(style: .regular, size: 14), .paragraphStyle: paragraphStyle ] mainView.contentTextView.attributedText = NSAttributedString(string: content ?? "", attributes: attributes) mainView.titleLabel.text = title - + } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -55,7 +55,7 @@ private extension TermsDetailController { make.edges.equalTo(view.safeAreaLayoutGuide) } } - + func bind() { mainView.xmarkButton.rx.tap .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift b/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift index 9a12dc81..6c5b7d7c 100644 --- a/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift +++ b/Poppool/Poppool/Presentation/Scene/SignUp/TermsDetail/TermsDetailView.swift @@ -10,13 +10,12 @@ import UIKit import SnapKit final class TermsDetailView: UIView { - + // MARK: - Components let titleLabel: PPLabel = { - let label = PPLabel(style: .bold, fontSize: 15) - return label + return PPLabel(style: .bold, fontSize: 15) }() - + let contentTextView: UITextView = { let view = UITextView() view.isSelectable = false @@ -25,19 +24,18 @@ final class TermsDetailView: UIView { return view }() - let xmarkButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "icon_xmark"), for: .normal) return button }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -45,7 +43,7 @@ final class TermsDetailView: UIView { // MARK: - SetUp private extension TermsDetailView { - + func setUpConstraints() { self.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -53,14 +51,14 @@ private extension TermsDetailView { make.top.equalToSuperview().inset(25) make.height.equalTo(21) } - + self.addSubview(xmarkButton) xmarkButton.snp.makeConstraints { make in make.size.equalTo(24) make.trailing.equalToSuperview().inset(16) make.centerY.equalTo(titleLabel) } - + self.addSubview(contentTextView) contentTextView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(43) diff --git a/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift b/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift index f0eb9ac8..c4fb9af4 100644 --- a/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift +++ b/Poppool/Poppool/Presentation/Scene/Splash/SplashController.swift @@ -7,20 +7,20 @@ import UIKit -import SnapKit +import ReactorKit import RxCocoa import RxSwift -import ReactorKit +import SnapKit final class SplashController: BaseViewController { - + // MARK: - Properties var disposeBag = DisposeBag() - + private var mainView = SplashView() private let authAPIUseCase = AuthAPIUseCaseImpl(repository: AuthAPIRepositoryImpl(provider: ProviderImpl())) private let keyChainService = KeyChainService() - + private var rootViewController: UIViewController? } @@ -43,7 +43,7 @@ private extension SplashController { make.edges.equalTo(view.safeAreaLayoutGuide) } } - + func playAnimation() { mainView.animationView.play { [weak self] _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -51,15 +51,15 @@ private extension SplashController { } } } - + func setRootview() { authAPIUseCase.postTokenReissue() .withUnretained(self) .subscribe(onNext: { (owner, response) in let newAccessToken = response.accessToken ?? "" let newRefreshToken = response.refreshToken ?? "" - let _ = owner.keyChainService.saveToken(type: .accessToken, value: newAccessToken) - let _ = owner.keyChainService.saveToken(type: .refreshToken, value: newRefreshToken) + _ = owner.keyChainService.saveToken(type: .accessToken, value: newAccessToken) + _ = owner.keyChainService.saveToken(type: .refreshToken, value: newRefreshToken) let navigationController = WaveTabBarController() owner.rootViewController = navigationController }, onError: { [weak self] _ in @@ -71,7 +71,7 @@ private extension SplashController { }) .disposed(by: disposeBag) } - + func changeRootView() { view.window?.rootViewController = rootViewController view.window?.makeKeyAndVisible() diff --git a/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift b/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift index 08b21aee..db657eb4 100644 --- a/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift +++ b/Poppool/Poppool/Presentation/Scene/Splash/View/SplashView.swift @@ -7,25 +7,25 @@ import UIKit -import SnapKit import Lottie +import SnapKit final class SplashView: UIView { - + // MARK: - Components - + let animationView: LottieAnimationView = { let view = LottieAnimationView(name: "PP_splash") view.contentMode = .scaleAspectFit return view }() - + // MARK: - init init() { super.init(frame: .zero) setUpConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -33,7 +33,7 @@ final class SplashView: UIView { // MARK: - SetUp private extension SplashView { - + func setUpConstraints() { addSubview(animationView) animationView.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift b/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift index 35d94f0c..efac523a 100644 --- a/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift +++ b/Poppool/Poppool/Presentation/Scene/TabbarController/TabbarController.swift @@ -8,9 +8,9 @@ import UIKit class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { - + private let waveLayer = CAShapeLayer() - + private let dotView: UIView = { let view = UIView() view.layer.borderWidth = 0.6 @@ -18,7 +18,7 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { view.backgroundColor = .blu500 return view }() - + override func viewDidLoad() { super.viewDidLoad() addSomeTabItems() @@ -26,36 +26,36 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { setupWaveTabBar() delegate = self } - + private func setupWaveTabBar() { // TabBar의 배경 투명 설정 tabBar.backgroundImage = UIImage() tabBar.shadowImage = UIImage() tabBar.isTranslucent = true - + // Wave Layer 설정 waveLayer.fillColor = UIColor.white.cgColor tabBar.layer.insertSublayer(waveLayer, at: 0) - + // Dot 설정 dotView.frame.size = CGSize(width: 12, height: 12) dotView.layer.cornerRadius = 6 tabBar.addSubview(dotView) } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() updateWavePath() updateDotPosition(animated: false) updateItem() } - + private func updateItem() { let tabBarItemViews = tabBar.subviews.filter { $0.isUserInteractionEnabled } - + if selectedIndex < tabBarItemViews.count { let selectedView = tabBarItemViews[selectedIndex] - + // 애니메이션 적용 UIView.animate( withDuration: 0.3, @@ -71,7 +71,7 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { ) } } - + private func updateWavePath(animated: Bool = false) { guard let items = tabBar.items else { return } let tabWidth = tabBar.bounds.width / CGFloat(items.count) @@ -139,12 +139,12 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { guard let items = tabBar.items else { return } let tabWidth = tabBar.bounds.width / CGFloat(items.count) let selectedTabX = CGFloat(selectedIndex) * tabWidth - + let targetCenter = CGPoint( x: selectedTabX + tabWidth / 2, y: -12 ) - + if animated { UIView.animate(withDuration: 1, delay: 0, @@ -158,20 +158,20 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { dotView.center = targetCenter } } - + // 탭 선택 시 애니메이션 적용 func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { updateWavePath(animated: true) updateDotPosition(animated: true) updateItem() } - + func setUp() { self.selectedIndex = 1 self.tabBar.barTintColor = .g200 self.tabBar.tintColor = .blu500 } - + func resizeImage(image: UIImage?, targetSize: CGSize) -> UIImage? { guard let image = image else { return nil } let size = image.size @@ -180,32 +180,32 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { let heightRatio = targetSize.height / size.height let scaleFactor = min(widthRatio, heightRatio) - + let scaledImageSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor) UIGraphicsBeginImageContextWithOptions(scaledImageSize, false, 0.0) image.draw(in: CGRect(origin: .zero, size: scaledImageSize)) - + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resizedImage } - + func addSomeTabItems() { let provider = ProviderImpl() let mapController = MapViewController() let mapUseCase = DefaultMapUseCase(repository: DefaultMapRepository(provider: provider)) - let directionRepository = DefaultMapDirectionRepository(provider: provider) + let directionRepository = DefaultMapDirectionRepository(provider: provider) mapController.reactor = MapReactor(useCase: mapUseCase, directionRepository: directionRepository) let homeController = HomeController() homeController.reactor = HomeReactor() - + let myPageController = MyPageController() myPageController.reactor = MyPageReactor() - + let iconSize = CGSize(width: 32, height: 32) // 탭바 아이템 생성 mapController.tabBarItem = UITabBarItem( @@ -223,33 +223,33 @@ class WaveTabBarController: UITabBarController, UITabBarControllerDelegate { image: resizeImage(image: UIImage(named: "icon_tabbar_menu"), targetSize: iconSize), selectedImage: resizeImage(image: UIImage(named: "icon_tabbar_menu"), targetSize: iconSize) ) - + // 네비게이션 컨트롤러 설정 let map = UINavigationController(rootViewController: mapController) let home = UINavigationController(rootViewController: homeController) let myPage = UINavigationController(rootViewController: myPageController) - + viewControllers = [map, home, myPage] - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.2 // 기본 값보다 높은 라인 간격을 설정 - + // 폰트 설정 let appearance = UITabBarAppearance() appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ - .font: UIFont.KorFont(style: .medium, size: 11)!, + .font: UIFont.korFont(style: .medium, size: 11)!, .paragraphStyle: paragraphStyle ] appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ - .font: UIFont.KorFont(style: .bold, size: 11)!, + .font: UIFont.korFont(style: .bold, size: 11)!, .paragraphStyle: paragraphStyle ] - + let verticalOffset: CGFloat = 4 // 원하는 간격 (양수: 아래로 이동, 음수: 위로 이동) appearance.stackedLayoutAppearance.normal.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: verticalOffset) appearance.stackedLayoutAppearance.selected.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: verticalOffset) - + tabBar.standardAppearance = appearance } - + } diff --git a/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift b/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift index 330cad1e..639a66e5 100644 --- a/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift +++ b/Poppool/Poppool/Presentation/Utills/Controllers/BaseTabmanController.swift @@ -7,8 +7,8 @@ import UIKit -import Tabman import Pageboy +import Tabman class BaseTabmanController: TabmanViewController { init() { @@ -20,17 +20,17 @@ class BaseTabmanController: TabmanViewController { line: #line ) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .systemBackground self.navigationController?.navigationBar.isHidden = true } - + deinit { Logger.log( message: "\(self) deinit", diff --git a/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift b/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift index 8c2b573f..2184b672 100644 --- a/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift +++ b/Poppool/Poppool/Presentation/Utills/Controllers/BaseViewController.swift @@ -7,14 +7,14 @@ import UIKit -import RxSwift import RxCocoa +import RxSwift class BaseViewController: UIViewController { - + var systemStatusBarIsDark: BehaviorRelay = .init(value: true) var systemStatusBarDisposeBag = DisposeBag() - + init() { super.init(nibName: nil, bundle: nil) Logger.log( @@ -24,23 +24,23 @@ class BaseViewController: UIViewController { line: #line ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white self.navigationController?.navigationBar.isHidden = true systemStatusBarIsDarkBind() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) systemStatusBarIsDark.accept(systemStatusBarIsDark.value) } - + deinit { Logger.log( message: "\(self) deinit", @@ -49,7 +49,7 @@ class BaseViewController: UIViewController { line: #line ) } - + func systemStatusBarIsDarkBind() { systemStatusBarIsDark .withUnretained(self) diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift index 6a2abf42..5b37a557 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift +++ b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionDecorationItem.swift @@ -11,13 +11,13 @@ import UIKit /// 제네릭 타입 `View`는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. struct SectionDecorationItem: SectionDecorationItemable { typealias ReusableView = View - + /// 데코레이션 뷰의 종류를 나타내는 문자열입니다. var elementKind: String - + /// 데코레이션 뷰의 인스턴스입니다. var reusableView: ReusableView - + /// 데코레이션 뷰에 주입될 데이터입니다. var viewInput: ReusableView.Input } @@ -25,16 +25,16 @@ struct SectionDecorationItem: Sectio /// `SectionDecorationItemable` 프로토콜은 데코레이션 뷰에 대한 인터페이스를 정의합니다. /// 이 프로토콜을 준수하는 타입은 데코레이션 뷰를 설정하고 반환하는 기능을 가져야 합니다. protocol SectionDecorationItemable { - + /// 데코레이션 뷰의 타입을 정의합니다. 이 뷰는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. associatedtype ReusableView: UICollectionReusableView & Inputable - + /// 데코레이션 뷰의 종류를 나타내는 문자열입니다. var elementKind: String { get set } - + /// 데코레이션 뷰의 인스턴스입니다. var reusableView: ReusableView { get set } - + /// 데코레이션 뷰에 주입될 데이터입니다. var viewInput: ReusableView.Input { get set } } diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift index a8c41655..2837db0c 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift +++ b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/SectionSupplementaryItem.swift @@ -22,31 +22,31 @@ struct SectionSupplementaryItem: Sec /// `SectionSupplementaryItemable` 프로토콜은 Supplementary View에 대한 인터페이스를 정의합니다. /// 해당 프로토콜을 준수하는 타입은 Supplementary View를 설정하고 반환하는 기능을 가져야 합니다. protocol SectionSupplementaryItemable { - + /// Supplementary View의 타입을 정의합니다. 이 뷰는 `UICollectionReusableView`와 `Inputable` 프로토콜을 준수해야 합니다. associatedtype ReusableView: UICollectionReusableView, Inputable - + /// Supplementary View의 너비를 정의합니다. var widthDimension: NSCollectionLayoutDimension { get set } - + /// Supplementary View의 높이를 정의합니다. var heightDimension: NSCollectionLayoutDimension { get set } - + /// Supplementary View의 종류를 나타내는 문자열입니다. var elementKind: String { get set } - + /// Supplementary View의 정렬 방법을 정의합니다. var alignment: NSRectAlignment { get set } - + /// Supplementary View의 인스턴스입니다. var reusableView: ReusableView { get set } - + /// Supplementary View에 주입될 데이터입니다. var viewInput: ReusableView.Input { get set } } extension SectionSupplementaryItemable { - + /// 주어진 인덱스 경로에 해당하는 Supplementary View를 생성하고 반환합니다. /// /// - Parameters: @@ -59,13 +59,13 @@ extension SectionSupplementaryItemable { viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath ) -> UICollectionReusableView { - + collectionView.register( ReusableView.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: reusableView.identifiers ) - + guard let view = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: reusableView.identifiers, diff --git a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift index dd23e99e..158f9092 100644 --- a/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift +++ b/Poppool/Poppool/Presentation/Utills/Interfaces/Sectionable/Sectionable.swift @@ -13,22 +13,22 @@ import SnapKit /// `Sectionable` 프로토콜은 컬렉션 뷰의 섹션 및 셀을 설정하기 위한 인터페이스를 정의합니다. /// 해당 프로토콜은 제네릭 타입 `CellType`을 사용하여 유연하게 컬렉션 뷰 셀을 처리할 수 있습니다. protocol Sectionable { - + /// 컬렉션 뷰 셀의 타입을 정의합니다. 이 셀은 `UICollectionViewCell`과 `Inputable` 프로토콜을 준수해야 합니다. associatedtype CellType: UICollectionViewCell, Inputable - + /// 셀에 입력될 데이터의 리스트를 정의합니다. var inputDataList: [CellType.Input] { get set } - + /// 섹션에 추가될 Supplementary View의 리스트를 정의합니다. var supplementaryItems: [any SectionSupplementaryItemable]? { get set } - + /// 섹션에 추가될 Decoration View의 리스트를 정의합니다. var decorationItems: [any SectionDecorationItemable]? { get set } - + /// 섹션의 페이지 var currentPage: PublishSubject { get set } - + /// 주어진 섹션 인덱스와 레이아웃 환경을 바탕으로 `NSCollectionLayoutSection`을 반환합니다. /// /// - Parameters: @@ -36,7 +36,7 @@ protocol Sectionable { /// - env: 레이아웃 환경 /// - Returns: 섹션 레이아웃을 반환합니다. func getSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection - + /// 섹션의 레이아웃을 설정합니다. /// /// - Parameters: @@ -44,7 +44,7 @@ protocol Sectionable { /// - env: 레이아웃 환경 /// - Returns: 설정된 섹션 레이아웃을 반환합니다. func setSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection - + /// 주어진 인덱스 경로에 해당하는 셀을 반환합니다. /// /// - Parameters: @@ -52,7 +52,7 @@ protocol Sectionable { /// - indexPath: 셀의 인덱스 경로 /// - Returns: 셀을 반환합니다. func getCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell - + /// Supplementary View를 반환합니다. /// /// - Parameters: @@ -68,7 +68,7 @@ protocol Sectionable { } extension Sectionable { - + /// `setSection` 메서드를 호출하여 섹션 레이아웃을 설정하고, Supplementary View를 추가합니다. /// /// - Parameters: @@ -76,55 +76,53 @@ extension Sectionable { /// - env: 레이아웃 환경 /// - Returns: 설정된 섹션 레이아웃을 반환합니다. func getSection(section: Int, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - + let section = setSection(section: section, env: env) section.visibleItemsInvalidationHandler = { _, contentOffset, environment in let bannerIndex = Int(max(0, round(contentOffset.x / environment.container.contentSize.width))) // 음수가 되는 것을 방지하기 위해 max 사용 currentPage.onNext(bannerIndex) } - + if let supplementaryItems = supplementaryItems { - + let items = supplementaryItems.map { - + let size = NSCollectionLayoutSize( widthDimension: $0.widthDimension, heightDimension: $0.heightDimension ) - let sectionItem = NSCollectionLayoutBoundarySupplementaryItem( + + return NSCollectionLayoutBoundarySupplementaryItem( layoutSize: size, elementKind: $0.elementKind, alignment: $0.alignment ) - - return sectionItem } section.boundarySupplementaryItems = items } - + if let decorationItems = decorationItems { - + let items = decorationItems.map { - - let sectionItem = NSCollectionLayoutDecorationItem.background( + + return NSCollectionLayoutDecorationItem.background( elementKind: $0.elementKind ) - return sectionItem } section.decorationItems = items } return section } - + /// `inputDataList`의 비어 있지 않은지를 반환합니다. var isEmpty: Bool { return inputDataList.isEmpty } - + /// `inputDataList`의 아이템 수를 반환합니다. var dataCount: Int { return inputDataList.count - + } /// 주어진 인덱스 경로에 해당하는 셀을 생성하고 반환합니다. /// @@ -149,7 +147,7 @@ extension Sectionable { cell.injection(with: input) return cell } - + /// 주어진 Supplementary View를 생성하고 반환합니다. /// /// - Parameters: @@ -162,7 +160,7 @@ extension Sectionable { viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath ) -> UICollectionReusableView { - + guard let item = supplementaryItems?.filter({ $0.elementKind == kind }).first else { Logger.log( message: "ReusableView Not Register", @@ -172,8 +170,7 @@ extension Sectionable { ) fatalError() } - - let view = item.getItem(collectionView: collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) - return view + + return item.getItem(collectionView: collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) } } diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift b/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift index a23d52c3..27f6d3df 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift +++ b/Poppool/Poppool/Presentation/Utills/ToastMaker/BookMarkToastView.swift @@ -10,9 +10,9 @@ import UIKit import SnapKit final class BookMarkToastView: UIView { - + // MARK: - Components - + private let bgView: UIView = { let view = UIView() view.backgroundColor = .pb70 @@ -20,17 +20,17 @@ final class BookMarkToastView: UIView { view.clipsToBounds = true return view }() - + private let bookMarkLabel: UILabel = { let label = UILabel() - label.setLineHeightText(text: "찜한 팝업에 저장했어요", font: .KorFont(style: .regular, size: 15), lineHeight: 1) + label.setLineHeightText(text: "찜한 팝업에 저장했어요", font: .korFont(style: .regular, size: 15), lineHeight: 1) label.textColor = .w100 return label }() - + private let unbookMarkLabel: UILabel = { let label = PPLabel(style: .regular, fontSize: 15, text: "찜한 팝업을 해제했어요") - label.setLineHeightText(text: "찜한 팝업을 해제했어요", font: .KorFont(style: .regular, size: 15), lineHeight: 1) + label.setLineHeightText(text: "찜한 팝업을 해제했어요", font: .korFont(style: .regular, size: 15), lineHeight: 1) label.textColor = .w100 return label }() @@ -42,10 +42,10 @@ final class BookMarkToastView: UIView { button.setTitleColor(.blu300, for: .normal) button.layer.cornerRadius = 4 button.clipsToBounds = true - button.titleLabel?.font = .KorFont(style: .medium, size: 12) + button.titleLabel?.font = .korFont(style: .medium, size: 12) return button }() - + // MARK: - init init(isBookMark: Bool) { super.init(frame: .zero) @@ -59,7 +59,7 @@ final class BookMarkToastView: UIView { setUpUnBookMarkConstraints() } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -67,7 +67,7 @@ final class BookMarkToastView: UIView { // MARK: - SetUp private extension BookMarkToastView { - + func setUpBookMarkConstraints() { bgView.addSubview(moveButton) moveButton.snp.makeConstraints { make in @@ -83,7 +83,7 @@ private extension BookMarkToastView { make.centerY.equalToSuperview() } } - + func setUpUnBookMarkConstraints() { bgView.addSubview(unbookMarkLabel) unbookMarkLabel.snp.makeConstraints { make in diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift b/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift index a52b772c..54f6fdf7 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift +++ b/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastMaker.swift @@ -7,14 +7,14 @@ import UIKit -import SnapKit -import RxSwift import RxCocoa +import RxSwift +import SnapKit final class ToastMaker { - + // MARK: - Properties - + /// 현재 디바이스 최상단 Window를 지정 static var window: UIWindow? { return UIApplication @@ -23,7 +23,7 @@ final class ToastMaker { .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } .first { $0.isKeyWindow } } - + /// 최상단의 ViewController를 가져오는 메서드 private static func topViewController( _ rootViewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController @@ -39,32 +39,32 @@ final class ToastMaker { } return rootViewController } - + private static var currentToast: ToastView? private static var currentBookMarkToast: BookMarkToastView? private static var disposeBag = DisposeBag() } extension ToastMaker { - + // MARK: - Method - + /// 토스트 메시지를 생성하는 메서드 /// - Parameter message: 토스트 메세지에 담길 String 타입 static func createToast(message: String) { - + currentToast?.removeFromSuperview() currentToast = nil let toastMSG = ToastView(message: message) guard let window = window else { return } window.addSubview(toastMSG) currentToast = toastMSG - + toastMSG.snp.makeConstraints { make in make.bottom.equalTo(window.snp.bottom).inset(120) make.centerX.equalTo(window.snp.centerX) } - + UIView.animate( withDuration: 0.3, delay: 4, @@ -76,11 +76,11 @@ extension ToastMaker { if currentToast == toastMSG { currentToast = nil } } } - + /// 토스트 메시지를 생성하는 메서드 /// - Parameter message: 토스트 메세지에 담길 String 타입 static func createBookMarkToast(isBookMark: Bool) { - + currentBookMarkToast?.removeFromSuperview() currentBookMarkToast = nil disposeBag = DisposeBag() @@ -96,7 +96,7 @@ extension ToastMaker { owner.navigationController?.pushViewController(nextController, animated: true) }) .disposed(by: disposeBag) - + if isBookMark { toastMSG.snp.makeConstraints { make in make.bottom.equalTo(currentVC.view.snp.bottom).inset(120) @@ -108,8 +108,7 @@ extension ToastMaker { make.centerX.equalTo(currentVC.view.snp.centerX) } } - - + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { UIView.animate( withDuration: 0.3, delay: 0, @@ -122,6 +121,6 @@ extension ToastMaker { disposeBag = DisposeBag() } } - + } } diff --git a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift b/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift index e20c3c72..79124cca 100644 --- a/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift +++ b/Poppool/Poppool/Presentation/Utills/ToastMaker/ToastView.swift @@ -11,9 +11,9 @@ import SnapKit /// 토스트 메시지를 담는 view 객체입니다 final class ToastView: UIView { - + // MARK: - Properties - + private let bgView: UIView = { let view = UIView() view.backgroundColor = .pb70 @@ -22,42 +22,42 @@ final class ToastView: UIView { view.sizeToFit() return view }() - + private let messageLabel: UILabel = { let label = UILabel() label.textColor = .w100 - label.font = .KorFont(style: .regular, size: 15) + label.font = .korFont(style: .regular, size: 15) return label }() - + // MARK: - Initializer - + init(message: String) { super.init(frame: .zero) setup() messageLabel.text = message } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension ToastView { - + // MARK: - Method - + private func setup() { addSubview(bgView) bgView.addSubview(messageLabel) - + bgView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalTo(snp.bottom) make.top.equalTo(snp.top) make.height.equalTo(38) } - + messageLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) make.centerY.equalToSuperview() diff --git a/Poppool/Poppool/Resource/Info.plist b/Poppool/Poppool/Resource/Info.plist index 9b1010ac..560ae1a9 100644 --- a/Poppool/Poppool/Resource/Info.plist +++ b/Poppool/Poppool/Resource/Info.plist @@ -2,6 +2,10 @@ + ITSAppUsesNonExemptEncryption + + CFBundleShortVersionString + $(MARKETING_VERSION) CFBundleURLTypes @@ -13,8 +17,10 @@ - ITSAppUsesNonExemptEncryption - + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + KAKAO_AUTH_APP_KEY + ${KAKAO_AUTH_APP_KEY} LSApplicationQueriesSchemes instagram @@ -22,7 +28,17 @@ kakaomap tmap maps + kakaokompassauth + kakaotalk + NAVER_MAP_CLIENT_ID + ${NAVER_MAP_CLIENT_ID} + POPPOOL_API_KEY + ${POPPOOL_API_KEY} + POPPOOL_BASE_URL + ${POPPOOL_BASE_URL} + POPPOOL_S3_BASE_URL + ${POPPOOL_S3_BASE_URL} UIAppFonts GothicA1-Bold.ttf diff --git a/Poppool/Poppool/Resource/KeyPath.swift b/Poppool/Poppool/Resource/KeyPath.swift new file mode 100644 index 00000000..4bc23b90 --- /dev/null +++ b/Poppool/Poppool/Resource/KeyPath.swift @@ -0,0 +1,30 @@ +import Foundation + +enum KeyPath { + static var kakaoAuthAppKey: String { + return getValue(forKey: "KAKAO_AUTH_APP_KEY") + } + + static var popPoolBaseURL: String { + return getValue(forKey: "POPPOOL_BASE_URL") + } + + static var popPoolS3BaseURL: String { + return getValue(forKey: "POPPOOL_S3_BASE_URL") + } + + static var popPoolAPIKey: String { + return getValue(forKey: "POPPOOL_API_KEY") + } + + static var naverMapClientID: String { + return getValue(forKey: "NAVER_MAP_CLIENT_ID") + } + + private static func getValue(forKey key: String) -> String { + guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String else { + fatalError("Missing key: \(key) in Info.plist") + } + return value + } +} diff --git a/Poppool/PoppoolTests/PoppoolTests.swift b/Poppool/PoppoolTests/PoppoolTests.swift deleted file mode 100644 index 931a598f..00000000 --- a/Poppool/PoppoolTests/PoppoolTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// PoppoolTests.swift -// PoppoolTests -// -// Created by Porori on 11/24/24. -// - -import XCTest -@testable import Poppool - -final class PoppoolTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Poppool/PoppoolUITests/PoppoolUITests.swift b/Poppool/PoppoolUITests/PoppoolUITests.swift deleted file mode 100644 index cf8347f1..00000000 --- a/Poppool/PoppoolUITests/PoppoolUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PoppoolUITests.swift -// PoppoolUITests -// -// Created by Porori on 11/24/24. -// - -import XCTest - -final class PoppoolUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift b/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift deleted file mode 100644 index e80f88dd..00000000 --- a/Poppool/PoppoolUITests/PoppoolUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PoppoolUITestsLaunchTests.swift -// PoppoolUITests -// -// Created by Porori on 11/24/24. -// - -import XCTest - -final class PoppoolUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -}