diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2953e8d3e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +max_line_length = off + +[*.{kt,kts}] +ktlint_function_naming_ignore_when_annotated_with=Composable \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..8848ed0d1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @TeamDATEROAD/DATEROAD-ANDROID \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 000000000..f2c9c6403 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,13 @@ +--- +name: ISSUE_TEMPLATE +about: "기능, UI, 문서 개선 및 추가 요청을 위한 템플릿입니다 \U0001F680" +title: '' +labels: '' +assignees: '' + +--- + +## What is this issue? 🛠 + +## Progress 🏃‍♀️ +- [ ] \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..8a0c26be9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Related issue 🛠 +- closed #이슈넘버 + +## Work Description ✏️ +- 작업 내용 + +## Screenshot 📸 + + +## Uncompleted Tasks 😅 +- [ ] Task1 + +## To Reviewers 📢 \ No newline at end of file diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml new file mode 100644 index 000000000..02df1b7f0 --- /dev/null +++ b/.github/workflows/android_cd.yml @@ -0,0 +1,100 @@ +name: DATEROAD CD + +on: + push: + branches: + - main + +defaults: + run: + shell: bash + working-directory: . + +jobs: + build: + name: CD + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Gradle cache + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Change gradlew permissions + run: chmod +x ./gradlew + + - name: Touch local properties + run: touch local.properties + + - name: Decode google-services.json + env: + FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} + run: echo $FIREBASE_SECRET > app/google-services.json + + - name: Access local properties + env: + HFM_BASE_URL: ${{ secrets.BASE_URL }} + IO_SENTRY_TOKEN: ${{ secrets.IO_SENTRY_DSN }} + KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + run: | + echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties + echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties + echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties + echo "amplitude.dev.api.key=\"AMPLITUDE_API_KEY\"" >> local.properties + + - name: Access sentry properties + env: + DEFAULTS_ORG: ${{ secrets.DEFAULTS_ORG }} + DEFAULTS_PROJECT: ${{ secrets.DEFAULTS_PROJECT }} + run: | + echo "defaults.org=$DEFAULTS_ORG" >> sentry.properties + echo "defaults.project=$DEFAULTS_PROJECT" >> sentry.properties + + - name: Build Release APK + run: | + ./gradlew :app:assembleRelease + + - name: Upload Release APK + uses: actions/upload-artifact@v3 + with: + name: release + path: ./app/build/outputs/apk/release/app-release-unsigned.apk + + - name: Discord Notify - Success + if: ${{ success() }} + uses: sarisia/actions-status-discord@v1 + with: + title: ✅ Release Test가 완료되었습니다! 🔥 + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + color: B7FF1D + username: DATEROAD-ANDROID 🍫 + content: | + Release Test가 완료되었습니다! + [❇️ APK를 다운로드해 보세요! ❇️](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + - name: Discord Notify - Failure + if: ${{ failure() }} + uses: sarisia/actions-status-discord@v1 + with: + title: ❌ Release Test Failed ❌ + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + color: FF0000 + username: DATEROAD-ANDROID 🍫 + content: 에러를 확인해 주세요 🫨 \ No newline at end of file diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml new file mode 100644 index 000000000..992f41369 --- /dev/null +++ b/.github/workflows/android_ci.yml @@ -0,0 +1,97 @@ +name: DATEROAD CI +on: + pull_request: + branches: [ develop, main ] + +defaults: + run: + shell: bash + working-directory: . + +jobs: + build: + name: CI + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Change gradlew permissions + run: chmod +x ./gradlew + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Touch local properties + run: touch local.properties + + - name: Decode google-services.json + env: + FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} + run: echo $FIREBASE_SECRET > app/google-services.json + + - name: Access local properties + env: + HFM_BASE_URL: ${{ secrets.BASE_URL }} + IO_SENTRY_TOKEN: ${{ secrets.IO_SENTRY_DSN }} + KAKAO_NATIVE_APP_KEY_MANIFEST: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} + run: | + echo "dev.base.url=\"$BASE_URL\"" >> local.properties + echo "io.sentry.dsn=\"$IO_SENTRY_DSN\"" >> local.properties + echo "kakao.native.app.key.manifest=\"$KAKAO_NATIVE_APP_KEY_MANIFEST\"" >> local.properties + echo "kakao.native.app.key=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties + echo "amplitude.dev.api.key=\"AMPLITUDE_API_KEY\"" >> local.properties + + - name: Access sentry properties + env: + DEFAULTS_ORG: ${{ secrets.DEFAULTS_ORG }} + DEFAULTS_PROJECT: ${{ secrets.DEFAULTS_PROJECT }} + AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} + run: | + echo "defaults.org=$DEFAULTS_ORG" >> sentry.properties + echo "defaults.project=$DEFAULTS_PROJECT" >> sentry.properties + echo "auth.token=$AUTH_TOKEN" >> sentry.properties + + - name: Lint Check + run: ./gradlew ktlintCheck -PcompileSdkVersion=34 + + - name: Build with Gradle + run: ./gradlew build -PcompileSdkVersion=34 + + - name: Discord Notify - Success + if: ${{ success() }} + uses: sarisia/actions-status-discord@v1 + with: + title: ✅ PR Success ✅ + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + color: D3EB77 + username: DATEROAD-ANDROID 🍫 + content: PR이 완료되었습니다! 👩‍❤️‍👨 + + - name: Discord Notify - Failure + if: ${{ failure() }} + uses: sarisia/actions-status-discord@v1 + with: + title: ❌ PR Failed ❌ + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + color: FF0000 + username: DATEROAD-ANDROID 🍫 + content: 에러를 확인해 주세요 🫨 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f4dca9151 --- /dev/null +++ b/.gitignore @@ -0,0 +1,188 @@ +# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,kotlin + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties +sentry.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Kotlin ### +# Compiled class file +*.class + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin \ No newline at end of file diff --git a/README.md b/README.md index 269a4c49c..899c0a211 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,186 @@ -# DATEROAD-ANDROID -우리랑 더미데이트 하러 갈래? 🤍 +# 쉽고 빠른 데이트로 가는 지름길, 데이트로드 👩🏻‍❤️‍👨🏻 + +![50](https://github.com/user-attachments/assets/3e0c4678-25de-4e48-8939-581733a9ec5b) +데이트로드는 ‘장소 중심’이 아닌 ‘코스 중심’ 데이트 공유 서비스로 사용자가 직접 데이트 코스를 등록하고 공유합니다. + + + +## 💟 Contributors + +| [🐸배지현 Lead](https://github.com/jihyunniiii) | [신민석](https://github.com/t1nm1ksun) | [이현진](https://github.com/2hyunjinn) | +|:-------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:| +| | | | +| `포인트내역`
`내가열람한코스+내가등록한코스`
`코스둘러보기`
`코스등록하기+일정등록하기`
`코스수정하기` | `온보딩/회원가입`
`스플래시`
`프로필등록하기`
`코스상세`
`마이페이지`
`웹뷰`
`관리자아카이빙` | `메인페이지`
`다가올 데이트일정`
`지난데이트일정`
`다가올데이트 상세페이지`
`지난데이트 상세페이지` + + +--- +## 📷 **시연영상** +| ![온보딩/회원가입](https://github.com/user-attachments/assets/6d6c4558-06a1-408a-b862-fcbd25662d01) | ![메인스크린](https://github.com/user-attachments/assets/be098dcc-5540-4c91-be27-3280362cf07f) | ![일정등록하기+코스등록하기](https://github.com/user-attachments/assets/1007d61d-8f91-4dd0-b544-4c3e00bb4af4) | +|:------------------------:|:--------------------------:|:----------------------:| +| `온보딩/회원가입` | `메인스크린` | `코스상세+일정등록하기` | + +| ![데이트 일정](https://github.com/user-attachments/assets/59469c43-75af-419c-a2b7-e7a08bfabaf2) | ![마이페이지](https://github.com/user-attachments/assets/4281f19e-5cba-486e-be17-14977b56d557) | +|:------------------------:|:--------------------------:| +| `데이트 일정` | `마이페이지` | + + + +## 👋 커밋 컨벤션 +[Git Convention & Branch Strategy](https://www.notion.so/hooooooni/Git-Convention-Branch-Strategy-fdcac833649d41beaea4fc5c4f7250a8) + + +## 👋 코드 컨벤션 +[Android Coding Convention](https://www.notion.so/hooooooni/Android-Coding-Convention-019d81b86cdb44cf8ab3ffa55c10c64d) + + +## 👋 브랜치전략 +**브랜치 유형** +- **main** : 완성된 버전의 코드를 저장하는 브랜치 +- **develop** : 개발이 진행되는 동안 완성된 코드를 저장하는 브랜치 +- **feature** : 작은 단위의 작업이 진행되는 브랜치 +- **hotfix** : 긴급한 오류를 해결하는 브랜치 + +- 해당 작업을 위한 브랜치를 파서 작업합니다. +- 작업 완료 후 PR을 날리고 팀원들에게 크로스체크 후 머지합니다. + +예시) + +- dev/feat-main-view +- dev/add-font-res + + +## 📁 *****Foldering***** +``` +📂 app +┣ 📂 manifests +┃ ┣ 📜 AndroidManifest.xml +┣ 📂 kotlin+java +┃ ┣ 📂 org.sopt.dateroad +┃ ┃ ┣ 📂 data +┃ ┃ ┃ ┣ 📂 datalocal +┃ ┃ ┃ ┃ ┣ 📂 datasource +┃ ┃ ┃ ┃ ┣ 📂 datasourceimpl +┃ ┃ ┃ ┣ 📂 dataremote +┃ ┃ ┃ ┃ ┣ 📂 datasource +┃ ┃ ┃ ┃ ┣ 📂 datasourceimpl +┃ ┃ ┃ ┃ ┣ 📂 interceptor +┃ ┃ ┃ ┃ ┣ 📂 model +┃ ┃ ┃ ┃ ┃ ┣ 📂 base +┃ ┃ ┃ ┃ ┃ ┣ 📂 request +┃ ┃ ┃ ┃ ┃ ┣ 📂 response +┃ ┃ ┃ ┃ ┣ 📂 service +┃ ┃ ┃ ┃ ┣ 📂 util +┃ ┃ ┃ ┣ 📂 mapper +┃ ┃ ┃ ┃ ┣ 📂 todata +┃ ┃ ┃ ┃ ┣ 📂 todomain +┃ ┃ ┃ ┃ ┣ 📂 toEntity +┃ ┃ ┃ ┣ 📂 repositoryimpl +┃ ┃ ┣ 📂 di +┃ ┃ ┣ 📂 domain +┃ ┃ ┃ ┣ 📂 model +┃ ┃ ┃ ┣ 📂 repository +┃ ┃ ┃ ┣ 📂 type +┃ ┃ ┃ ┣ 📂 usecase +┃ ┃ ┃ ┣ 📂 util +┃ ┃ ┣ 📂 presentation +┃ ┃ ┃ ┣ 📂 model +┃ ┃ ┃ ┣ 📂 type +┃ ┃ ┃ ┣ 📂 ui +┃ ┃ ┃ ┃ ┣ 📂 component +┃ ┃ ┃ ┃ ┃ 📂 coursedetail +┃ ┃ ┃ ┃ ┣ 📂 enroll +┃ ┃ ┃ ┃ ┣ 📂 home +┃ ┃ ┃ ┃ ┣ 📂 look +┃ ┃ ┃ ┃ ┣ 📂 mycourse +┃ ┃ ┃ ┃ ┣ 📂 mypage +┃ ┃ ┃ ┃ ┣ 📂 navigator +┃ ┃ ┃ ┃ ┣ 📂 onboarding +┃ ┃ ┃ ┃ ┣ 📂 past +┃ ┃ ┃ ┃ ┣ 📂 pointguide +┃ ┃ ┃ ┃ ┣ 📂 pointhistory +┃ ┃ ┃ ┃ ┣ 📂 profile +┃ ┃ ┃ ┃ ┣ 📂 read +┃ ┃ ┃ ┃ ┣ 📂 signin +┃ ┃ ┃ ┃ ┣ 📂 splash +┃ ┃ ┃ ┃ ┣ 📂 timeline +┃ ┃ ┃ ┃ ┣ 📂 timelinedetail +┃ ┃ ┃ ┣ 📂 util +┃ ┣ 📂 ui.theme +┃ ┣ 📄 DateRoadApp.kt + +``` + + + +## 목차 + +--- + +## 🩷 프로젝트 설명 + +--- + +장소 중심이 아닌 코스 중심의 데이트 코스 공유 서비스 데이트로드입니다. + +데이트로드에서는 다른 커플들의 실제 데이트 코스 후기를 포인트를 통해 열람할 수 있습니다. + +코스 둘러보기를 통해 마음에 드는 코스를 클릭하고 미리보기를 통해 사전정보를 획득할 수 있습니다. + +포인트가 없다고 걱정하지 마세요. 최초 3회는 무료로 데이트 코스를 열람할 수 있습니다. 해당 코스대로 데이트를 떠나고 싶다면 내 일정에 추가하기 버튼을 눌러 내 데이트 일정으로 등록할 수도 있습니다. + +## 📝 문제상황 정의 + +--- + +![6](https://github.com/user-attachments/assets/b489f192-ae95-40b0-9694-fdc121ffe192) + + +- 기존 앱은 코스가 아닌 장소 중심, 이로 인해 데이트 코스를 찾기 위해 여러 앱을 쓰며 피로감을 느낌 +- 광고가 아닌 직접 방문한 사람의 후기를 기반으로 데이트 코스를 짜고 싶어 하는 니즈 존재 + +## 🎯 핵심 타겟 + +--- + +- 센스 있게 데이트 코스를 짜고 싶은 여자/남자친구 +- 색다른 데이트 코스를 찾기 위해 인스타그램 등을 탐색하는 커플 +- 네이버 블로그, 인스타그램을 통해 여러 번 데이트 장소의 후기를 얻는 커플 + +## 📍 주요 기능 + +--- + +### 1️⃣ 코스 등록하기 및 열람 + +![Instagram_post_-_4](https://github.com/user-attachments/assets/bc81ceda-fc99-4156-af11-e73f3d2b7396) + +![Instagram_post_-_5](https://github.com/user-attachments/assets/3a30183c-a625-4a18-b7e3-685e2f01bc2c) + + +- 내가 한 데이트 코스를 등록하고 포인트를 획득할 수 있습니다. +- 다른 커플들이 한 데이트를 포인트를 사용해 열람할 수 있습니다. +- 코스 상세 페이지에서 ‘내 일정에 추가하기’ 버튼을 눌러 내 데이트 일정으로 불러올 수 있습니다. + +### 2️⃣ 일정 등록하기 및 열람 + +![Instagram_post_-_10](https://github.com/user-attachments/assets/4698b585-7369-4cb8-b3ca-6ebde11e5586) + +![Instagram_post_-_6](https://github.com/user-attachments/assets/c5666dee-da05-41b9-8934-739e50de6498) + +- 내 데이트 일정을 등록할 수 있습니다. +- 내 데이트 일정을 확인할 수 있습니다. +- 지난 데이트는 코스 등록하기로 연동해 등록하고 포인트를 받을 수 있습니다. +- 카카오톡 공유하기를 통해 데이트 일정을 연인에게 공유할 수 있습니다. + +## 💰 비즈니스 모델 + +--- + +> **포인트를 통한 수익 모델** +> +- 유저들은 데이트 코스를 등록하고 포인트를 획득해 제휴 매장에 할인받아 방문합니다. +- 구글 애드센스를 연결하여 광고를 시청하면 포인트를 획득할 수 있습니다. 데이트로드는 광고 수익을 얻을 수 있습니다. + +> **입점처를 통한 수익 모델** +> +- 입점 가게는 매장을 홍보하고 유저 방문으로 매출을 증가시키며, 광고주는 유저에게 광고를 노출하여 제품이나 서비스를 홍보합니다. 데이트로드는 이를 통해 수익을 창출하고, 모든 참여자가 상호 이익을 얻는 생태계를 구축합니다. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..851bb4dfd --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,136 @@ +import java.util.Properties + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.google.services) + alias(libs.plugins.google.firebase.crashlytics) + alias(libs.plugins.dagger.hilt) + alias(libs.plugins.ktlint) + alias(libs.plugins.sentry) +} + +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) +} + +android { + namespace = "org.sopt.dateroad" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "org.sopt.dateroad" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = libs.versions.versionCode.get().toInt() + versionName = libs.versions.versionName.get() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + + buildConfigField("String", "KAKAO_NATIVE_APP_KEY", properties["kakao.native.app.key"].toString()) + + manifestPlaceholders["IO_SENTRY_DSN"] = properties["io.sentry.dsn"] as String + manifestPlaceholders["KAKAO_NATIVE_APP_KEY_MANIFEST"] = properties["kakao.native.app.key.manifest"] as String + } + + buildTypes { + debug { + isMinifyEnabled = false + buildConfigField("String", "BASE_URL", properties["dev.base.url"].toString()) + buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.dev.api.key"].toString()) + } + + release { + isMinifyEnabled = true + isShrinkResources = true + buildConfigField("String", "BASE_URL", properties["prod.base.url"].toString()) + buildConfigField("String", "AMPLITUDE_API_KEY", properties["amplitude.prod.api.key"].toString()) + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = libs.versions.jvmTarget.get() + } + buildFeatures { + viewBinding = true + buildConfig = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Test + testImplementation(libs.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.test) + + // Debug + debugImplementation(libs.bundles.debug) + + // AndroidX + implementation(libs.bundles.androidx) + implementation(platform(libs.androidx.compose.bom)) + + // Google + implementation(platform(libs.google.firebase.bom)) + implementation(libs.google.firebase.crashlytics) + + // Network + implementation(platform(libs.okhttp.bom)) + implementation(libs.bundles.okhttp) + implementation(libs.bundles.retrofit) + implementation(libs.kotlinx.serialization.json) + + // Hilt + implementation(libs.bundles.hilt) + kapt(libs.hilt.compiler) + + // Coil + implementation(libs.coil.compose) + + // Timber + implementation(libs.timber) + + // Kakao + implementation(libs.bundles.kakao) + + // View Pager + implementation(libs.bundles.pager) + + // Web View + implementation(libs.accompanist.webview) + + // Amplitude + implementation(libs.amplitude) + + // Lottie + implementation(libs.lottie.compose) +} + +ktlint { + android = true + debug = true + coloredOutput = true + verbose = true + outputToConsole = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..c252b8837 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,50 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +-keep class com.kakao.sdk.**.model.* { ; } +-keep class * extends com.google.gson.TypeAdapter +-keep interface com.kakao.sdk.**.*Api + +-keepattributes Signature, InnerClasses, EnclosingMethod +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations +-keepattributes AnnotationDefault +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} +-dontwarn javax.annotation.** +-dontwarn kotlin.Unit +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> +-keep,allowobfuscation,allowshrinking class retrofit2.Response +-keep,allowobfuscation,allowshrinking interface retrofit2.Call + +-dontwarn javax.annotation.** +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz +-dontwarn org.codehaus.mojo.animal_sniffer.* +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** \ No newline at end of file diff --git a/app/src/androidTest/java/org/sopt/dateroad/ExampleInstrumentedTest.kt b/app/src/androidTest/java/org/sopt/dateroad/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..4f17cfdac --- /dev/null +++ b/app/src/androidTest/java/org/sopt/dateroad/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.sopt.dateroad", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c02129b7f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/loading.json b/app/src/main/assets/loading.json new file mode 100644 index 000000000..85684c5db --- /dev/null +++ b/app/src/main/assets/loading.json @@ -0,0 +1,494 @@ +{ + "assets": [], + "ddd": 0, + "fr": 30, + "h": 360, + "ip": 0, + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Oval 1", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 180, + 180 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 43, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "sh", + "hd": false, + "nm": "Oval 1", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + -8.284, + 0 + ], + [ + 0, + -8.284 + ], + [ + 8.284, + 0 + ], + [ + 0, + 8.284 + ] + ], + "o": [ + [ + 8.284, + 0 + ], + [ + 0, + 8.284 + ], + [ + -8.284, + 0 + ], + [ + 0, + -8.284 + ] + ], + "v": [ + [ + 0, + -15 + ], + [ + 15, + 0 + ], + [ + 0, + 15 + ], + [ + -15, + 0 + ] + ] + } + } + }, + { + "ty": "st", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.29, + 0.235, + 0.937 + ] + }, + "lc": 2, + "lj": 1, + "ml": 28.96, + "o": { + "a": 0, + "k": 100 + }, + "w": { + "a": 0, + "k": 7 + } + }, + { + "ty": "tm", + "hd": false, + "bm": 0, + "e": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 1 + ], + "i": { + "x": 0.56, + "y": 1 + }, + "o": { + "x": 0.44, + "y": 0 + } + }, + { + "t": 30, + "s": [ + 100 + ], + "i": { + "x": 0.75, + "y": 0.75 + }, + "o": { + "x": 0.25, + "y": 0.25 + } + }, + { + "t": 42, + "s": [ + 100 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "o": { + "a": 0, + "k": 0 + }, + "s": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 0 + ], + "i": { + "x": 0.44, + "y": 0.98 + }, + "o": { + "x": 0.6, + "y": 0.01 + } + }, + { + "t": 42, + "s": [ + 99 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "m": 1 + } + ] + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Oval 2", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 15, + 15 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 180, + 180 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 43, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "el", + "hd": false, + "nm": "Oval 2", + "p": { + "a": 0, + "k": [ + 15, + 15 + ] + }, + "s": { + "a": 0, + "k": [ + 30, + 30 + ] + }, + "d": 1 + }, + { + "ty": "st", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.922, + 0.922, + 0.953 + ] + }, + "lc": 1, + "lj": 1, + "ml": 28.96, + "o": { + "a": 0, + "k": 100 + }, + "w": { + "a": 0, + "k": 7 + } + } + ] + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Screen", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 180, + 180 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 180, + 180 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 43, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "Screen Group", + "bm": 0, + "it": [ + { + "ty": "rc", + "hd": false, + "nm": "Screen", + "d": 1, + "p": { + "a": 0, + "k": [ + 180, + 180 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 360, + 360 + ] + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 1, + 1, + 1 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + } + ], + "meta": { + "g": "@phase-software/lottie-exporter 0.7.0" + }, + "nm": "", + "op": 42, + "v": "5.6.0", + "w": 360 +} \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..d478a1046 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/org/sopt/dateroad/DateRoadApp.kt b/app/src/main/java/org/sopt/dateroad/DateRoadApp.kt new file mode 100644 index 000000000..245a42e39 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/DateRoadApp.kt @@ -0,0 +1,33 @@ +package org.sopt.dateroad + +import android.app.Application +import androidx.appcompat.app.AppCompatDelegate +import com.kakao.sdk.common.KakaoSdk +import dagger.hilt.android.HiltAndroidApp +import org.sopt.dateroad.BuildConfig.KAKAO_NATIVE_APP_KEY +import org.sopt.dateroad.presentation.util.amplitude.AmplitudeUtils.initAmplitude +import timber.log.Timber + +@HiltAndroidApp +class DateRoadApp : Application() { + override fun onCreate() { + super.onCreate() + + setTimber() + setDarkMode() + setKakao() + initAmplitude(applicationContext) + } + + private fun setTimber() { + if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + } + + private fun setDarkMode() { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + + private fun setKakao() { + KakaoSdk.init(this, KAKAO_NATIVE_APP_KEY) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/datalocal/datasource/UserInfoLocalDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/datalocal/datasource/UserInfoLocalDataSource.kt new file mode 100644 index 000000000..954932f82 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/datalocal/datasource/UserInfoLocalDataSource.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.data.datalocal.datasource + +interface UserInfoLocalDataSource { + var accessToken: String + var refreshToken: String + var nickname: String + fun clear() +} diff --git a/app/src/main/java/org/sopt/dateroad/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt new file mode 100644 index 000000000..50ec5d965 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt @@ -0,0 +1,52 @@ +package org.sopt.dateroad.data.datalocal.datasourceimpl + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.kakao.sdk.auth.Constants.ACCESS_TOKEN +import com.kakao.sdk.auth.Constants.REFRESH_TOKEN +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import org.sopt.dateroad.BuildConfig +import org.sopt.dateroad.data.datalocal.datasource.UserInfoLocalDataSource + +class UserInfoLocalDataSourceImpl @Inject constructor( + @ApplicationContext context: Context +) : UserInfoLocalDataSource { + private val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + private val sharedPreferences: SharedPreferences = if (BuildConfig.DEBUG) { + context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) + } else { + EncryptedSharedPreferences.create( + context, + FILE_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + override var accessToken: String + get() = sharedPreferences.getString(ACCESS_TOKEN, "").toString() + set(value) = sharedPreferences.edit { putString(ACCESS_TOKEN, value) } + + override var refreshToken: String + get() = sharedPreferences.getString(REFRESH_TOKEN, "").toString() + set(value) = sharedPreferences.edit { putString(REFRESH_TOKEN, value) } + + override var nickname: String + get() = sharedPreferences.getString(NICK_NAME, "").toString() + set(value) = sharedPreferences.edit { putString(NICK_NAME, value) } + + override fun clear() = sharedPreferences.edit { clear() } + + companion object { + const val FILE_NAME = "DateRoadLocalDataSource" + const val NICK_NAME = "NickName" + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AdvertisementRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AdvertisementRemoteDataSource.kt new file mode 100644 index 000000000..47a706039 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AdvertisementRemoteDataSource.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementsDto + +interface AdvertisementRemoteDataSource { + suspend fun getAdvertisementDetail(advertisementId: Int): ResponseAdvertisementDetailDto + suspend fun getHomeAdvertisements(): ResponseAdvertisementsDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AuthRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AuthRemoteDataSource.kt new file mode 100644 index 000000000..8819d33d6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/AuthRemoteDataSource.kt @@ -0,0 +1,21 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.model.request.RequestSignInDto +import org.sopt.dateroad.data.dataremote.model.request.RequestWithdrawDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAuthDto + +interface AuthRemoteDataSource { + suspend fun deleteSignOut() + + suspend fun deleteWithdraw(requestWithdrawDto: RequestWithdrawDto) + + suspend fun getNicknameCheck(name: String): Int + + suspend fun postSignIn(authorization: String, requestSignInDto: RequestSignInDto): ResponseAuthDto + + suspend fun postSignUp(image: MultipartBody.Part?, userSignUpData: RequestBody, tags: RequestBody): ResponseAuthDto + + suspend fun patchEditProfile(name: RequestBody, tags: RequestBody, image: MultipartBody.Part?, isDefaultImage: RequestBody) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/CourseRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/CourseRemoteDataSource.kt new file mode 100644 index 000000000..2dbb97a27 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/CourseRemoteDataSource.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.model.response.ResponseCourseDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto + +interface CourseRemoteDataSource { + suspend fun deleteCourse(courseId: Int) + + suspend fun deleteCourseLike(courseId: Int) + + suspend fun getCourseDetail(courseId: Int): ResponseCourseDetailDto + + suspend fun getFilteredCourses(country: String?, city: String?, cost: Int?): ResponseCoursesDto + + suspend fun getSortedCourses(sortBy: String): ResponseCoursesDto + + suspend fun postCourse(images: List, course: RequestBody, tags: RequestBody, places: RequestBody) + + suspend fun postCourseLike(courseId: Int) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/MyCourseRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/MyCourseRemoteDataSource.kt new file mode 100644 index 000000000..f97d46661 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/MyCourseRemoteDataSource.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto + +interface MyCourseRemoteDataSource { + suspend fun getMyCourseEnroll(): ResponseCoursesDto + + suspend fun getMyCourseRead(): ResponseCoursesDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/ProfileRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/ProfileRemoteDataSource.kt new file mode 100644 index 000000000..42f1836dd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/ProfileRemoteDataSource.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import org.sopt.dateroad.data.dataremote.model.response.ResponseProfileDto + +interface ProfileRemoteDataSource { + suspend fun getProfile(): ResponseProfileDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/TimelineRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/TimelineRemoteDataSource.kt new file mode 100644 index 000000000..b210627c5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/TimelineRemoteDataSource.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import org.sopt.dateroad.data.dataremote.model.request.RequestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseNearestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelineDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelinesDto + +interface TimelineRemoteDataSource { + suspend fun deleteTimeline(timelineId: Int) + + suspend fun getTimelineDetail(timelineId: Int): ResponseTimelineDetailDto + + suspend fun getTimelines(timelineTimeType: String): ResponseTimelinesDto + + suspend fun getNearestTimeline(): ResponseNearestTimelineDto + + suspend fun postTimeline(requestTimelineDto: RequestTimelineDto) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/UserPointRemoteDataSource.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/UserPointRemoteDataSource.kt new file mode 100644 index 000000000..e6d980c29 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasource/UserPointRemoteDataSource.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.data.dataremote.datasource + +import org.sopt.dateroad.data.dataremote.model.request.RequestUsePointDto +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointHistoryDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseUserPointDto + +interface UserPointRemoteDataSource { + suspend fun getUserPoint(): ResponseUserPointDto + + suspend fun getPointHistory(): ResponsePointHistoryDto + + suspend fun postUsePoint(courseId: Int, requestUsePointDto: RequestUsePointDto) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AdvertisementRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AdvertisementRemoteDataSourceImpl.kt new file mode 100644 index 000000000..90615fe7d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AdvertisementRemoteDataSourceImpl.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.AdvertisementRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementsDto +import org.sopt.dateroad.data.dataremote.service.AdvertisementService + +class AdvertisementRemoteDataSourceImpl @Inject constructor( + private val advertisementService: AdvertisementService +) : AdvertisementRemoteDataSource { + override suspend fun getAdvertisementDetail(advertisementId: Int): ResponseAdvertisementDetailDto = advertisementService.getAdvertisementDetail(advertisementId = advertisementId) + + override suspend fun getHomeAdvertisements(): ResponseAdvertisementsDto = advertisementService.getHomeAdvertisements() +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt new file mode 100644 index 000000000..e36ddc3d0 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/AuthRemoteDataSourceImpl.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.datasource.AuthRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.request.RequestSignInDto +import org.sopt.dateroad.data.dataremote.model.request.RequestWithdrawDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAuthDto +import org.sopt.dateroad.data.dataremote.service.AuthService + +class AuthRemoteDataSourceImpl @Inject constructor( + private val authService: AuthService +) : AuthRemoteDataSource { + override suspend fun deleteSignOut() { + authService.deleteSignOut() + } + + override suspend fun deleteWithdraw(requestWithdrawDto: RequestWithdrawDto) = + authService.deleteWithdraw(requestWithdrawDto = requestWithdrawDto) + + override suspend fun getNicknameCheck(name: String) = authService.getNicknameCheck(name = name).code() + + override suspend fun postSignIn(authorization: String, requestSignInDto: RequestSignInDto): ResponseAuthDto = + authService.postSignIn(requestSignInDto = requestSignInDto) + + override suspend fun postSignUp(image: MultipartBody.Part?, userSignUpData: RequestBody, tags: RequestBody): ResponseAuthDto = + authService.postSignUp(image = image, userSignUpData = userSignUpData, tags = tags) + + override suspend fun patchEditProfile(name: RequestBody, tags: RequestBody, image: MultipartBody.Part?, isDefaultImage: RequestBody) = + authService.patchProfile(name = name, tags = tags, image = image, isDefaultImage = isDefaultImage) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/CourseRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/CourseRemoteDataSourceImpl.kt new file mode 100644 index 000000000..1de52e073 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/CourseRemoteDataSourceImpl.kt @@ -0,0 +1,27 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.datasource.CourseRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.response.ResponseCourseDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto +import org.sopt.dateroad.data.dataremote.service.CourseService + +class CourseRemoteDataSourceImpl @Inject constructor( + private val courseService: CourseService +) : CourseRemoteDataSource { + override suspend fun deleteCourse(courseId: Int) = courseService.deleteCourse(courseId = courseId) + + override suspend fun deleteCourseLike(courseId: Int) = courseService.deleteCourseLike(courseId = courseId) + + override suspend fun getCourseDetail(courseId: Int): ResponseCourseDetailDto = courseService.getCourseDetail(courseId = courseId) + + override suspend fun getFilteredCourses(country: String?, city: String?, cost: Int?): ResponseCoursesDto = courseService.getFilteredCourses(country = country, city = city, cost = cost) + + override suspend fun getSortedCourses(sortBy: String): ResponseCoursesDto = courseService.getSortedCourses(sortBy = sortBy) + + override suspend fun postCourse(images: List, course: RequestBody, tags: RequestBody, places: RequestBody) = courseService.postCourse(images = images, course = course, tags = tags, places = places) + + override suspend fun postCourseLike(courseId: Int) = courseService.postCourseLike(courseId = courseId) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/MyCourseRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/MyCourseRemoteDataSourceImpl.kt new file mode 100644 index 000000000..b8490c0d1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/MyCourseRemoteDataSourceImpl.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.MyCourseRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto +import org.sopt.dateroad.data.dataremote.service.MyCourseService + +class MyCourseRemoteDataSourceImpl @Inject constructor( + private val myCourseService: MyCourseService +) : MyCourseRemoteDataSource { + override suspend fun getMyCourseEnroll(): ResponseCoursesDto = myCourseService.getMyCourseEnroll() + + override suspend fun getMyCourseRead(): ResponseCoursesDto = myCourseService.getMyCourseRead() +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/ProfileRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/ProfileRemoteDataSourceImpl.kt new file mode 100644 index 000000000..856cd7562 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/ProfileRemoteDataSourceImpl.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.ProfileRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.response.ResponseProfileDto +import org.sopt.dateroad.data.dataremote.service.ProfileService + +class ProfileRemoteDataSourceImpl @Inject constructor( + private val profileService: ProfileService +) : ProfileRemoteDataSource { + + override suspend fun getProfile(): ResponseProfileDto = + profileService.getProfile() +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/TimelineRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/TimelineRemoteDataSourceImpl.kt new file mode 100644 index 000000000..66624bb6b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/TimelineRemoteDataSourceImpl.kt @@ -0,0 +1,23 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.TimelineRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.request.RequestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseNearestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelineDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelinesDto +import org.sopt.dateroad.data.dataremote.service.TimelineService + +class TimelineRemoteDataSourceImpl @Inject constructor( + private val timelineService: TimelineService +) : TimelineRemoteDataSource { + override suspend fun deleteTimeline(timelineId: Int) = timelineService.deleteTimeline(timelineId = timelineId) + + override suspend fun getTimelineDetail(timelineId: Int): ResponseTimelineDetailDto = timelineService.getTimelineDetail(timelineId = timelineId) + + override suspend fun getTimelines(timelineTimeType: String): ResponseTimelinesDto = timelineService.getTimelines(timelineTimeType = timelineTimeType) + + override suspend fun getNearestTimeline(): ResponseNearestTimelineDto = timelineService.getNearestTimeline() + + override suspend fun postTimeline(requestTimelineDto: RequestTimelineDto) = timelineService.postTimeline(requestTimelineDto = requestTimelineDto) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/UserPointRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/UserPointRemoteDataSourceImpl.kt new file mode 100644 index 000000000..6bccb202c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/datasourceimpl/UserPointRemoteDataSourceImpl.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.data.dataremote.datasourceimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.UserPointRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.request.RequestUsePointDto +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointHistoryDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseUserPointDto +import org.sopt.dateroad.data.dataremote.service.UserPointService + +class UserPointRemoteDataSourceImpl @Inject constructor( + private val userPointService: UserPointService +) : UserPointRemoteDataSource { + override suspend fun getUserPoint(): ResponseUserPointDto = userPointService.getUserPoint() + + override suspend fun getPointHistory(): ResponsePointHistoryDto = userPointService.getPointHistory() + + override suspend fun postUsePoint(courseId: Int, requestUsePointDto: RequestUsePointDto) = userPointService.postUsePoint(courseId = courseId, requestUsePointDto = requestUsePointDto) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/interceptor/AuthInterceptor.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/interceptor/AuthInterceptor.kt new file mode 100644 index 000000000..6aeb44c3e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/interceptor/AuthInterceptor.kt @@ -0,0 +1,119 @@ +package org.sopt.dateroad.data.dataremote.interceptor + +import android.app.Application +import android.content.Intent +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.sopt.dateroad.BuildConfig +import org.sopt.dateroad.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.dateroad.data.dataremote.model.response.ResponseRefreshTokenDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.REISSUE +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USERS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION + +class AuthInterceptor @Inject constructor( + private val json: Json, + private val localStorage: UserInfoLocalDataSource, + private val context: Application +) : Interceptor { + + private val mutex = Mutex() + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val authRequest = if (localStorage.accessToken.isNotBlank()) originalRequest.newAuthBuilder() else originalRequest + var response = chain.proceed(authRequest) + + when (response.code) { + CODE_TOKEN_EXPIRE -> { + response.close() + response = handleTokenExpiration(chain = chain, originalRequest = originalRequest, requestAccessToken = localStorage.accessToken) + } + } + + return response + } + + private fun Request.newAuthBuilder() = + this.newBuilder().addHeader(AUTHORIZATION, localStorage.accessToken).build() + + private fun handleTokenExpiration(chain: Interceptor.Chain, originalRequest: Request, requestAccessToken: String): Response = + runBlocking { + mutex.withLock { + when (isTokenValid(requestAccessToken = requestAccessToken, currentAccessToken = localStorage.accessToken)) { + true -> chain.proceed(originalRequest.newAuthBuilder()) + false -> handleTokenRefresh(chain = chain, originalRequest = originalRequest, refreshToken = localStorage.refreshToken) + } + } + } + + private fun isTokenValid(requestAccessToken: String, currentAccessToken: String): Boolean = requestAccessToken != currentAccessToken && currentAccessToken.isNotBlank() + + private fun handleTokenRefresh(chain: Interceptor.Chain, originalRequest: Request, refreshToken: String): Response = + patchTokenRefresh(chain = chain, originalRequest = originalRequest, refreshToken = refreshToken).let { refreshTokenResponse -> + when (refreshTokenResponse.isSuccessful) { + true -> handleTokenRefreshSuccess(chain = chain, originalRequest = originalRequest, refreshTokenResponse = refreshTokenResponse) + false -> handleTokenRefreshFailed(refreshTokenResponse = refreshTokenResponse) + } + } + + private fun patchTokenRefresh(chain: Interceptor.Chain, originalRequest: Request, refreshToken: String): Response = chain.proceed( + originalRequest.newBuilder() + .patch("".toRequestBody(null)) + .url("${BuildConfig.BASE_URL}$API/$VERSION/$USERS/$REISSUE") + .addHeader(AUTHORIZATION, refreshToken) + .build() + ) + + private fun handleTokenRefreshSuccess(chain: Interceptor.Chain, originalRequest: Request, refreshTokenResponse: Response): Response { + val responseRefreshToken = json.decodeFromString( + refreshTokenResponse.body?.string() ?: throw IllegalStateException("\"refreshTokenResponse is null $refreshTokenResponse\"") + ) + + with(localStorage) { + accessToken = BEARER + responseRefreshToken.accessToken + refreshToken = responseRefreshToken.refreshToken + } + + refreshTokenResponse.close() + + return chain.proceed(originalRequest.newAuthBuilder()) + } + + private fun handleTokenRefreshFailed(refreshTokenResponse: Response): Response { + refreshTokenResponse.close() + + CoroutineScope(Dispatchers.Main).launch { + with(context) { + CoroutineScope(Dispatchers.Main).launch { + startActivity( + Intent.makeRestartActivityTask( + packageManager.getLaunchIntentForPackage(packageName)?.component + ) + ) + } + } + } + + localStorage.clear() + + return refreshTokenResponse + } + + companion object { + const val CODE_TOKEN_EXPIRE = 401 + const val AUTHORIZATION = "Authorization" + const val BEARER = "Bearer " + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/base/BaseResponse.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/base/BaseResponse.kt new file mode 100644 index 000000000..32f032f80 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/base/BaseResponse.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.base + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BaseResponse( + @SerialName("code") + val code: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestCourseDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestCourseDto.kt new file mode 100644 index 000000000..9202191ab --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestCourseDto.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestCourseDto( + @SerialName("title") + val title: String, + @SerialName("date") + val date: String, + @SerialName("startAt") + val startAt: String, + @SerialName("country") + val country: String, + @SerialName("city") + val city: String, + @SerialName("description") + val description: String, + @SerialName("cost") + val cost: Int +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestPlaceDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestPlaceDto.kt new file mode 100644 index 000000000..16d48c8af --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestPlaceDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPlaceDto( + @SerialName("sequence") + val sequence: Int, + @SerialName("title") + val title: String, + @SerialName("duration") + val duration: Float +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignInDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignInDto.kt new file mode 100644 index 000000000..44e5c7bbf --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignInDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class RequestSignInDto( + @SerialName("platform") + val platform: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignUpDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignUpDto.kt new file mode 100644 index 000000000..892b8a500 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestSignUpDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestSignUpDto( + @SerialName("userSignUpReq") + val userSignUpReq: RequestUserSignUpInfoDto, + @SerialName("image") + val image: String, + @SerialName("tag") + val tag: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagDto.kt new file mode 100644 index 000000000..096d865eb --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestTagDto( + @SerialName("tag") + val tag: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagsDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagsDto.kt new file mode 100644 index 000000000..d3fb81079 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTagsDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestTagsDto( + @SerialName("tag") + val tag: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTimelineDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTimelineDto.kt new file mode 100644 index 000000000..8b95c2171 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestTimelineDto.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestTimelineDto( + @SerialName("title") + val title: String, + @SerialName("date") + val date: String, + @SerialName("startAt") + val startAt: String, + @SerialName("tags") + val tags: List, + @SerialName("country") + val country: String, + @SerialName("city") + val city: String, + @SerialName("places") + val places: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUsePointDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUsePointDto.kt new file mode 100644 index 000000000..2f88c19d2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUsePointDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestUsePointDto( + @SerialName("point") + val point: Int, + @SerialName("type") + val type: String, + @SerialName("description") + val description: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUserSignUpInfoDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUserSignUpInfoDto.kt new file mode 100644 index 000000000..0284125c9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestUserSignUpInfoDto.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestUserSignUpInfoDto( + @SerialName("name") + val name: String, + @SerialName("platform") + val platform: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestWithdrawDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestWithdrawDto.kt new file mode 100644 index 000000000..ff9d2c29a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/request/RequestWithdrawDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestWithdrawDto( + @SerialName("authCode") + val authCode: String? = null +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDetailDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDetailDto.kt new file mode 100644 index 000000000..f8334742d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDetailDto.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseAdvertisementDetailDto( + @SerialName("images") + val images: List, + @SerialName("title") + val title: String, + @SerialName("createAt") + val createAt: String, + @SerialName("description") + val description: String, + @SerialName("adTagType") + val advertisementTagType: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDto.kt new file mode 100644 index 000000000..f5ac2ca00 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementDto.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseAdvertisementDto( + @SerialName("advertisementId") + val advertisementId: Int, + @SerialName("thumbnail") + val thumbnail: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementsDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementsDto.kt new file mode 100644 index 000000000..b416efb94 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAdvertisementsDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class ResponseAdvertisementsDto( + @SerialName("advertisementDtoResList") + val advertisements: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAuthDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAuthDto.kt new file mode 100644 index 000000000..23eb755de --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseAuthDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseAuthDto( + @SerialName("userId") + val userId: Int, + @SerialName("accessToken") + val accessToken: String, + @SerialName("refreshToken") + val refreshToken: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDetailDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDetailDto.kt new file mode 100644 index 000000000..acc674248 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDetailDto.kt @@ -0,0 +1,42 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseCourseDetailDto( + @SerialName("courseId") + val courseId: Int, + @SerialName("images") + val images: List, + @SerialName("like") + val like: Int, + @SerialName("totalTime") + val totalTime: Float, + @SerialName("date") + val date: String, + @SerialName("city") + val city: String, + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("startAt") + val startAt: String, + @SerialName("places") + val places: List, + @SerialName("totalCost") + val totalCost: Int, + @SerialName("tags") + val tags: List, + @SerialName("isAccess") + val isAccess: Boolean, + @SerialName("free") + val free: Int, + @SerialName("totalPoint") + val totalPoint: Int, + @SerialName("isCourseMine") + val isCourseMine: Boolean, + @SerialName("isUserLiked") + val isUserLiked: Boolean +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDto.kt new file mode 100644 index 000000000..37ab0d23f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCourseDto.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseCourseDto( + @SerialName("courseId") + val courseId: Int, + @SerialName("thumbnail") + val thumbnail: String, + @SerialName("title") + val title: String, + @SerialName("city") + val city: String, + @SerialName("cost") + val cost: Int, + @SerialName("duration") + val duration: Float, + @SerialName("like") + val like: Int +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCoursesDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCoursesDto.kt new file mode 100644 index 000000000..3772ef754 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseCoursesDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseCoursesDto( + @SerialName("courses") + val courses: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseImageDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseImageDto.kt new file mode 100644 index 000000000..8499aa7b8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseImageDto.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseImageDto( + @SerialName("imageUrl") + val imageUrl: String, + @SerialName("sequence") + val sequence: Int +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseNearestTimelineDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseNearestTimelineDto.kt new file mode 100644 index 000000000..1955fffd1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseNearestTimelineDto.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseNearestTimelineDto( + @SerialName("dateId") + val timelineId: Int, + @SerialName("dDay") + val dDay: Int, + @SerialName("dateName") + val dateName: String, + @SerialName("month") + val month: Int, + @SerialName("day") + val day: Int, + @SerialName("startAt") + val startAt: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePlaceDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePlaceDto.kt new file mode 100644 index 000000000..06a6a601a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePlaceDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePlaceDto( + @SerialName("sequence") + val sequence: Int, + @SerialName("title") + val title: String, + @SerialName("duration") + val duration: Float +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointDto.kt new file mode 100644 index 000000000..afcb8a1e6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePointDto( + @SerialName("point") + val point: Int, + @SerialName("description") + val description: String, + @SerialName("createdAt") + val createdAt: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointHistoryDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointHistoryDto.kt new file mode 100644 index 000000000..a54bbe2a8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointHistoryDto.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePointHistoryDto( + @SerialName("gained") + val gained: ResponsePointsDto, + @SerialName("used") + val used: ResponsePointsDto +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointsDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointsDto.kt new file mode 100644 index 000000000..164d4c9c1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponsePointsDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePointsDto( + @SerialName("points") + val points: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseProfileDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseProfileDto.kt new file mode 100644 index 000000000..ce9b8b895 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseProfileDto.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseProfileDto( + @SerialName("name") + val name: String, + @SerialName("tags") + val tags: List, + @SerialName("point") + val point: Int, + @SerialName("imageUrl") + val imageUrl: String? +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseRefreshTokenDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseRefreshTokenDto.kt new file mode 100644 index 000000000..df2d33d65 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseRefreshTokenDto.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseRefreshTokenDto( + @SerialName("userId") val userId: Int, + @SerialName("accessToken") val accessToken: String, + @SerialName("refreshToken") val refreshToken: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTagDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTagDto.kt new file mode 100644 index 000000000..9a0f4788f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTagDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseTagDto( + @SerialName("tag") + val tag: String +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDetailDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDetailDto.kt new file mode 100644 index 000000000..1bcb40f94 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDetailDto.kt @@ -0,0 +1,24 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseTimelineDetailDto( + @SerialName("dateId") + val timelineId: Int, + @SerialName("title") + val title: String, + @SerialName("startAt") + val startAt: String, + @SerialName("city") + val city: String, + @SerialName("tags") + val tags: List, + @SerialName("date") + val date: String, + @SerialName("places") + val places: List, + @SerialName("dDay") + val dDay: Int +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDto.kt new file mode 100644 index 000000000..7f64a8a1c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelineDto.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseTimelineDto( + @SerialName("dateId") + val timelineId: Int, + @SerialName("title") + val title: String, + @SerialName("date") + val date: String, + @SerialName("city") + val city: String, + @SerialName("tags") + val tags: List, + @SerialName("dDay") + val dDay: Int +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelinesDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelinesDto.kt new file mode 100644 index 000000000..bd9cc434b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseTimelinesDto.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseTimelinesDto( + @SerialName("dates") + val timelines: List +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseUserPointDto.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseUserPointDto.kt new file mode 100644 index 000000000..b66e19374 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/model/response/ResponseUserPointDto.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseUserPointDto( + @SerialName("name") + val name: String, + @SerialName("point") + val point: Int, + @SerialName("image") + val image: String? +) diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AdvertisementService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AdvertisementService.kt new file mode 100644 index 000000000..4e9005954 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AdvertisementService.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.data.dataremote.service + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementsDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.ADVERTISEMENTS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.ADVERTISEMENT_ID +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.GET +import retrofit2.http.Path + +interface AdvertisementService { + @GET("$API/$VERSION/$ADVERTISEMENTS/{$ADVERTISEMENT_ID}") + suspend fun getAdvertisementDetail( + @Path(ADVERTISEMENT_ID) advertisementId: Int + ): ResponseAdvertisementDetailDto + + @GET("$API/$VERSION/$ADVERTISEMENTS") + suspend fun getHomeAdvertisements(): ResponseAdvertisementsDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AuthService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AuthService.kt new file mode 100644 index 000000000..409e445c5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/AuthService.kt @@ -0,0 +1,67 @@ +package org.sopt.dateroad.data.dataremote.service + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.model.request.RequestSignInDto +import org.sopt.dateroad.data.dataremote.model.request.RequestWithdrawDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseAuthDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.CHECK +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.IS_DEFAULT_IMAGE +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.NAME +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.SIGNUP +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.SIGN_IN +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.SIGN_OUT +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.TAG +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.TAGS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USERS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USER_SIGN_UP_DATA +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.WITHDRAW +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.Multipart +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Query + +interface AuthService { + @DELETE("$API/$VERSION/$USERS/$SIGN_OUT") + suspend fun deleteSignOut() + + @HTTP(method = "DELETE", hasBody = true, path = "$API/$VERSION/$USERS/$WITHDRAW") + suspend fun deleteWithdraw( + @Body requestWithdrawDto: RequestWithdrawDto + ) + + @GET("$API/$VERSION/$USERS/$CHECK") + suspend fun getNicknameCheck( + @Query(NAME) name: String + ): Response + + @POST("$API/$VERSION/$USERS/$SIGN_IN") + suspend fun postSignIn( + @Body requestSignInDto: RequestSignInDto + ): ResponseAuthDto + + @Multipart + @POST("$API/$VERSION/$USERS/$SIGNUP") + suspend fun postSignUp( + @Part image: MultipartBody.Part?, + @Part(USER_SIGN_UP_DATA) userSignUpData: RequestBody, + @Part(TAG) tags: RequestBody + ): ResponseAuthDto + + @Multipart + @PATCH("$API/$VERSION/$USERS") + suspend fun patchProfile( + @Part(NAME) name: RequestBody, + @Part(TAGS) tags: RequestBody, + @Part image: MultipartBody.Part?, + @Part(IS_DEFAULT_IMAGE)isDefaultImage: RequestBody + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/CourseService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/CourseService.kt new file mode 100644 index 000000000..3c9b24602 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/CourseService.kt @@ -0,0 +1,69 @@ +package org.sopt.dateroad.data.dataremote.service + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.sopt.dateroad.data.dataremote.model.response.ResponseCourseDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.CITY +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COST +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COUNTRY +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSE +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSE_ID +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.LIKES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.PLACES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.SORT +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.SORT_BY +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.TAGS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query + +interface CourseService { + @DELETE("$API/$VERSION/$COURSES/{$COURSE_ID}") + suspend fun deleteCourse( + @Path(COURSE_ID) courseId: Int + ) + + @DELETE("$API/$VERSION/$COURSES/{$COURSE_ID}/$LIKES") + suspend fun deleteCourseLike( + @Path(COURSE_ID) courseId: Int + ) + + @GET("$API/$VERSION/$COURSES/{$COURSE_ID}") + suspend fun getCourseDetail( + @Path(COURSE_ID) courseId: Int + ): ResponseCourseDetailDto + + @GET("$API/$VERSION/$COURSES") + suspend fun getFilteredCourses( + @Query(COUNTRY) country: String?, + @Query(CITY) city: String?, + @Query(COST) cost: Int? + ): ResponseCoursesDto + + @GET("$API/$VERSION/$COURSES/$SORT") + suspend fun getSortedCourses( + @Query(SORT_BY) sortBy: String + ): ResponseCoursesDto + + @Multipart + @POST("$API/$VERSION/$COURSES") + suspend fun postCourse( + @Part images: List, + @Part(COURSE) course: RequestBody, + @Part(TAGS) tags: RequestBody, + @Part(PLACES) places: RequestBody + ) + + @POST("$API/$VERSION/$COURSES/{$COURSE_ID}/$LIKES") + suspend fun postCourseLike( + @Path(COURSE_ID) courseId: Int + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/MyCourseService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/MyCourseService.kt new file mode 100644 index 000000000..f750f7ff5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/MyCourseService.kt @@ -0,0 +1,17 @@ +package org.sopt.dateroad.data.dataremote.service + +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.DATE_ACCESS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USERS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.GET + +interface MyCourseService { + @GET("$API/$VERSION/$COURSES/$USERS") + suspend fun getMyCourseEnroll(): ResponseCoursesDto + + @GET("$API/$VERSION/$COURSES/$DATE_ACCESS") + suspend fun getMyCourseRead(): ResponseCoursesDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/ProfileService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/ProfileService.kt new file mode 100644 index 000000000..ed4dc9eb1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/ProfileService.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.dataremote.service + +import org.sopt.dateroad.data.dataremote.model.response.ResponseProfileDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USERS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.GET + +interface ProfileService { + @GET("$API/$VERSION/$USERS") + suspend fun getProfile(): ResponseProfileDto +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/TimelineService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/TimelineService.kt new file mode 100644 index 000000000..8665d5bb3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/TimelineService.kt @@ -0,0 +1,43 @@ +package org.sopt.dateroad.data.dataremote.service + +import org.sopt.dateroad.data.dataremote.model.request.RequestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseNearestTimelineDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelineDetailDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelinesDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.DATES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.DATE_ID +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.NEAREST +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.TIME +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface TimelineService { + @DELETE("$API/$VERSION/$DATES/{$DATE_ID}") + suspend fun deleteTimeline( + @Path(DATE_ID) timelineId: Int + ) + + @GET("$API/$VERSION/$DATES/{$DATE_ID}") + suspend fun getTimelineDetail( + @Path(DATE_ID) timelineId: Int + ): ResponseTimelineDetailDto + + @GET("$API/$VERSION/$DATES") + suspend fun getTimelines( + @Query(TIME) timelineTimeType: String + ): ResponseTimelinesDto + + @GET("$API/$VERSION/$DATES/$NEAREST") + suspend fun getNearestTimeline(): ResponseNearestTimelineDto + + @POST("$API/$VERSION/$DATES") + suspend fun postTimeline( + @Body requestTimelineDto: RequestTimelineDto + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/service/UserPointService.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/UserPointService.kt new file mode 100644 index 000000000..ec718ef6a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/service/UserPointService.kt @@ -0,0 +1,31 @@ +package org.sopt.dateroad.data.dataremote.service + +import org.sopt.dateroad.data.dataremote.model.request.RequestUsePointDto +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointHistoryDto +import org.sopt.dateroad.data.dataremote.model.response.ResponseUserPointDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.API +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSES +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.COURSE_ID +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.DATE_ACCESS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.MAIN +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.POINTS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.USERS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.VERSION +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface UserPointService { + @GET("$API/$VERSION/$USERS/$MAIN") + suspend fun getUserPoint(): ResponseUserPointDto + + @GET("$API/$VERSION/$POINTS") + suspend fun getPointHistory(): ResponsePointHistoryDto + + @POST("$API/$VERSION/$COURSES/{$COURSE_ID}/$DATE_ACCESS") + suspend fun postUsePoint( + @Path(COURSE_ID) courseId: Int, + @Body requestUsePointDto: RequestUsePointDto + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/util/Constraints.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/util/Constraints.kt new file mode 100644 index 000000000..f9dc93ad5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/util/Constraints.kt @@ -0,0 +1,87 @@ +package org.sopt.dateroad.data.dataremote.util + +object ApiConstraints { + const val PROFILE_FORM_DATA_IMAGE = "image" + const val COURSE_FORM_DATA_IMAGE = "images" + const val APPLICATION_JSON = "application/json" + const val HTTPS = "https://" + const val API = "api" + const val VERSION = "v1" + const val COURSES = "courses" + const val DATE_ACCESS = "date-access" + const val USERS = "users" + const val REISSUE = "reissue" + const val CHECK = "check" + const val NAME = "name" + const val COURSE_ID = "courseId" + const val DATE_ID = "dateId" + const val LIKES = "likes" + const val SORT = "sort" + const val COUNTRY = "country" + const val CITY = "city" + const val COST = "cost" + const val SORT_BY = "sortBy" + const val POINTS = "points" + const val COURSE = "course" + const val TAGS = "tags" + const val PLACES = "places" + const val MAIN = "main" + const val WITHDRAW = "withdraw" + const val ADVERTISEMENTS = "advertisements" + const val SIGN_IN = "signin" + const val SIGN_OUT = "signout" + const val SIGNUP = "signup" + const val USER_SIGN_UP_DATA = "userSignUpReq" + const val TAG = "tag" + const val ADVERTISEMENT_ID = "advId" + const val DATES = "dates" + const val TIME = "time" + const val NEAREST = "nearest" + const val IS_DEFAULT_IMAGE = "isDefaultImage" +} + +object Cost { + const val COST = "원" +} + +object Duration { + const val DURATION = "시간" +} + +object Like { + const val LIKE_MAX = "999+" + const val THRESHOLD = 999 +} + +object Date { + const val INPUT_FORMAT = "yyyy.MM.dd" + const val COURSE_DETAIL_OUTPUT_FORMAT = "yyyy년 M월 d일 방문" + const val DATE_OUTPUT_FORMAT = "yyyy년 M월 d일" + const val TIMELINE_OUTPUT_FORMAT = "%s\n%d" + const val MAIN_DATE_OUTPUT_FORMAT = "%d월 %d일" + const val D_DAY_OUTPUT_FORMAT = "D-" + const val D_DAY_DEFAULT_LABEL = "D-Day" + const val NEAREST_DATE_START_OUTPUT_FORMAT = " 시작" + const val ADVERTISEMENT_DETAIL_OUTPUT_FORMAT = "yyyy년 M월 d일" +} + +object Point { + const val POINT = " P" + const val GAINED = "+" + const val USED = "-" +} + +object Month { + const val JANUARY = "January" + const val FEBRUARY = "February" + const val MARCH = "March" + const val APRIL = "April" + const val MAY = "May" + const val JUNE = "June" + const val JULY = "July" + const val AUGUST = "August" + const val SEPTEMBER = "September" + const val OCTOBER = "October" + const val NOVEMBER = "November" + const val DECEMBER = "December" +} diff --git a/app/src/main/java/org/sopt/dateroad/data/dataremote/util/ContentUriRequestBody.kt b/app/src/main/java/org/sopt/dateroad/data/dataremote/util/ContentUriRequestBody.kt new file mode 100644 index 000000000..8410e04ee --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/dataremote/util/ContentUriRequestBody.kt @@ -0,0 +1,50 @@ +package org.sopt.dateroad.data.dataremote.util + +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source + +class ContentUriRequestBody( + private val contentResolver: ContentResolver, + private val uri: Uri +) : RequestBody() { + private var fileName = "" + private var size = -1L + + init { + contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DISPLAY_NAME), + null, + null, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) + fileName = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)) + } + } + } + + fun getFileName() = fileName + + override fun contentLength(): Long = size + + override fun contentType(): MediaType? = + contentResolver.getType(uri)?.toMediaTypeOrNull() + + override fun writeTo(sink: BufferedSink) { + contentResolver.openInputStream(uri)?.source()?.use { source -> + sink.writeAll(source) + } + } + + fun toFormData(name: String = ApiConstraints.COURSE_FORM_DATA_IMAGE) = MultipartBody.Part.createFormData(name, getFileName(), this) +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/AreaMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/AreaMapper.kt new file mode 100644 index 000000000..4a12cfbfe --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/AreaMapper.kt @@ -0,0 +1,37 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.domain.type.GyeonggiAreaType +import org.sopt.dateroad.domain.type.GyeonggiAreaType.Companion.fromTitleToGyeonggiAreaType +import org.sopt.dateroad.domain.type.GyeonggiAreaType.Companion.toGyeonggiAreaTitle +import org.sopt.dateroad.domain.type.GyeonggiAreaType.Companion.toGyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType.Companion.fromTitleToIncheonAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType.Companion.toIncheonAreaTitle +import org.sopt.dateroad.domain.type.IncheonAreaType.Companion.toIncheonAreaType +import org.sopt.dateroad.domain.type.SeoulAreaType +import org.sopt.dateroad.domain.type.SeoulAreaType.Companion.fromTitleToSeoulAreaType +import org.sopt.dateroad.domain.type.SeoulAreaType.Companion.toSeoulAreaTitle +import org.sopt.dateroad.domain.type.SeoulAreaType.Companion.toSeoulAreaType + +fun Any?.toAreaTitle(): String = when (this) { + is SeoulAreaType -> this.title + is GyeonggiAreaType -> this.title + is IncheonAreaType -> this.title + else -> "" +} + +fun String.toAreaType(): Any? = + when { + this.toSeoulAreaTitle().isNotEmpty() -> this.toSeoulAreaType() + this.toGyeonggiAreaTitle().isNotEmpty() -> this.toGyeonggiAreaType() + this.toIncheonAreaTitle().isNotEmpty() -> this.toIncheonAreaType() + else -> this + } + +fun String.fromTitleToAreaType(): Any? = + when { + this.fromTitleToSeoulAreaType() != null -> this.fromTitleToSeoulAreaType() + this.fromTitleToGyeonggiAreaType() != null -> this.fromTitleToGyeonggiAreaType() + this.fromTitleToIncheonAreaType() != null -> this.fromTitleToIncheonAreaType() + else -> this + } diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CostMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CostMapper.kt new file mode 100644 index 000000000..7e6d5ffbb --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CostMapper.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import java.text.NumberFormat +import java.util.Locale +import org.sopt.dateroad.data.dataremote.util.Cost +import org.sopt.dateroad.presentation.util.TotalCostZero.ZERO_COST + +fun Int.toCost(): String = if (this == 0) { + ZERO_COST +} else { + "${NumberFormat.getNumberInstance(Locale.KOREA).format(this)}${Cost.COST}" +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CourseDetailMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CourseDetailMapper.kt new file mode 100644 index 000000000..6e9b5e2f9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/CourseDetailMapper.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.model.Enroll + +fun CourseDetail.toEnroll() = Enroll( + title = this.title, + startAt = this.startAt, + tags = this.tags, + country = this.city.toRegionType(), + city = this.city.fromTitleToAreaType(), + places = this.places +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/DurationMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/DurationMapper.kt new file mode 100644 index 000000000..62a471998 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/DurationMapper.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.data.dataremote.util.Duration + +fun Float.toDuration(): String { + val formattedDuration = if (this % 1.0 == 0.0) { + "%.0f".format(this) + } else { + "%.1f".format(this) + } + return "$formattedDuration${Duration.DURATION}" +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/LikeMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/LikeMapper.kt new file mode 100644 index 000000000..cbff53cba --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/LikeMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.data.dataremote.util.Like + +fun Int.toLike(): String = when { + this < Like.THRESHOLD -> this.toString() + else -> Like.LIKE_MAX +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/PointMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/PointMapper.kt new file mode 100644 index 000000000..89082d366 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/PointMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.data.dataremote.util.Point + +fun Int.toPoint(): String = this.toString() + Point.POINT + +fun Int.toGainedPoint(): String = Point.GAINED + this + Point.POINT + +fun Int.toUsedPoint(): String = Point.USED + this + Point.POINT diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/RegionMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/RegionMapper.kt new file mode 100644 index 000000000..e02d98788 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/RegionMapper.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.domain.type.GyeonggiAreaType.Companion.fromTitleToGyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType.Companion.fromTitleToIncheonAreaType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.type.SeoulAreaType.Companion.fromTitleToSeoulAreaType + +fun String.toRegionType(): RegionType? = when { + this.fromTitleToSeoulAreaType() != null -> RegionType.SEOUL + this.fromTitleToGyeonggiAreaType() != null -> RegionType.GYEONGGI + this.fromTitleToIncheonAreaType() != null -> RegionType.INCHEON + else -> null +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineDetailMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineDetailMapper.kt new file mode 100644 index 000000000..87ddadabf --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineDetailMapper.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.model.TimelineDetail + +fun TimelineDetail.toEnroll() = Enroll( + title = this.title, + startAt = this.startAt, + country = this.city.toRegionType(), + city = this.city.fromTitleToAreaType(), + tags = this.tags, + date = this.date.fromDateToEnrollDate(), + places = this.places +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineMapper.kt new file mode 100644 index 000000000..2f45b9218 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/toEntity/TimelineMapper.kt @@ -0,0 +1,35 @@ +package org.sopt.dateroad.data.mapper.toEntity + +import java.text.SimpleDateFormat +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale +import org.sopt.dateroad.data.dataremote.model.response.ResponseNearestTimelineDto +import org.sopt.dateroad.data.dataremote.util.Date +import org.sopt.dateroad.domain.type.MonthType + +fun String.toCourseDetailDate(): String = SimpleDateFormat(Date.INPUT_FORMAT, Locale.getDefault()).parse(this)?.let { SimpleDateFormat(Date.COURSE_DETAIL_OUTPUT_FORMAT, Locale.getDefault()).format(it) } ?: "" + +fun String.toBasicDates(): String = SimpleDateFormat(Date.INPUT_FORMAT, Locale.getDefault()).parse(this)?.let { SimpleDateFormat(Date.DATE_OUTPUT_FORMAT, Locale.getDefault()).format(it) } ?: "" + +fun String.fromCourseDetailToEnrollDate(): String = LocalDate.parse(this, DateTimeFormatter.ofPattern(Date.COURSE_DETAIL_OUTPUT_FORMAT)).format(DateTimeFormatter.ofPattern(Date.INPUT_FORMAT)) + +fun String.fromDateToEnrollDate(): String = LocalDate.parse(this, DateTimeFormatter.ofPattern(Date.DATE_OUTPUT_FORMAT)).format(DateTimeFormatter.ofPattern(Date.INPUT_FORMAT)) + +fun Int.toDDayString(): String = when { + this > 0 -> "${Date.D_DAY_OUTPUT_FORMAT}$this" + this == 0 -> Date.D_DAY_DEFAULT_LABEL + else -> "" +} + +fun String.toStartAtString(): String = "$this${Date.NEAREST_DATE_START_OUTPUT_FORMAT}" + +fun String.toAdvertisementDetailDate(): String = SimpleDateFormat(Date.INPUT_FORMAT, Locale.getDefault()).parse(this)?.let { SimpleDateFormat(Date.ADVERTISEMENT_DETAIL_OUTPUT_FORMAT, Locale.getDefault()).format(it) } ?: "" + +fun ResponseNearestTimelineDto.toFormattedDate(): String = String.format(Date.MAIN_DATE_OUTPUT_FORMAT, this.month, this.day) + +fun String.toFormattedDate(): String = String.format( + Date.TIMELINE_OUTPUT_FORMAT, + MonthType.fromNumber(this.split(".")[1].toInt()), + this.split(".")[2].toInt() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/EnrollMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/EnrollMapper.kt new file mode 100644 index 000000000..5ff7ff0b7 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/EnrollMapper.kt @@ -0,0 +1,26 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestCourseDto +import org.sopt.dateroad.data.dataremote.model.request.RequestTimelineDto +import org.sopt.dateroad.data.mapper.toEntity.toAreaTitle +import org.sopt.dateroad.domain.model.Enroll + +fun Enroll.toTimelineData(): RequestTimelineDto = RequestTimelineDto( + title = this.title, + date = this.date, + startAt = this.startAt, + tags = this.tags.map { tag -> tag.toData() }, + country = this.country?.title.orEmpty(), + city = this.city.toAreaTitle(), + places = places.mapIndexed { index, place -> place.toData(sequence = index + 1) } +) + +fun Enroll.toCourseData(): RequestCourseDto = RequestCourseDto( + title = this.title, + date = this.date, + startAt = this.startAt, + country = country?.title.orEmpty(), + city = this.city.toAreaTitle(), + description = this.description, + cost = this.cost.toInt() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/PlaceMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/PlaceMapper.kt new file mode 100644 index 000000000..23b1add95 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/PlaceMapper.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestPlaceDto +import org.sopt.dateroad.data.dataremote.util.Duration.DURATION +import org.sopt.dateroad.domain.model.Place + +fun Place.toData(sequence: Int): RequestPlaceDto = RequestPlaceDto( + sequence = sequence, + title = this.title, + duration = duration.substringBefore(DURATION).toFloat() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/ProfileMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/ProfileMapper.kt new file mode 100644 index 000000000..05cb5cfe3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/ProfileMapper.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.model.Profile + +fun Profile.toEditProfile(): EditProfile = EditProfile( + name = this.name, + tags = this.tag, + image = this.imageUrl +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/SignInMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/SignInMapper.kt new file mode 100644 index 000000000..2eab91b42 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/SignInMapper.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestSignInDto +import org.sopt.dateroad.domain.model.SignIn + +fun SignIn.toData(): RequestSignInDto = RequestSignInDto( + platform = this.platform +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagMapper.kt new file mode 100644 index 000000000..7ad5b412f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagMapper.kt @@ -0,0 +1,5 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestTagDto + +fun String.toData() = RequestTagDto(tag = this) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagsMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagsMapper.kt new file mode 100644 index 000000000..a8142bce9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/TagsMapper.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestTagsDto + +fun List.toData(): RequestTagsDto = RequestTagsDto( + tag = this +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UsePointMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UsePointMapper.kt new file mode 100644 index 000000000..462f89135 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UsePointMapper.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestUsePointDto +import org.sopt.dateroad.domain.model.UsePoint + +fun UsePoint.toData(): RequestUsePointDto = RequestUsePointDto( + point = this.point, + type = this.type, + description = this.description +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UserSignUpInfoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UserSignUpInfoMapper.kt new file mode 100644 index 000000000..b647aaea6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todata/UserSignUpInfoMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.mapper.todata + +import org.sopt.dateroad.data.dataremote.model.request.RequestUserSignUpInfoDto +import org.sopt.dateroad.domain.model.UserSignUpInfo + +fun UserSignUpInfo.toData(): RequestUserSignUpInfoDto = RequestUserSignUpInfoDto( + name = this.name, + platform = this.platform +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDetailDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDetailDtoMapper.kt new file mode 100644 index 000000000..a0822e48d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDetailDtoMapper.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementDetailDto +import org.sopt.dateroad.data.mapper.toEntity.toAdvertisementDetailDate +import org.sopt.dateroad.domain.model.AdvertisementDetail +import org.sopt.dateroad.domain.type.AdvertisementTagType.Companion.toAdvertisementTagTitle + +fun ResponseAdvertisementDetailDto.toDomain(): AdvertisementDetail = AdvertisementDetail( + images = this.images.sortedBy { responseImageDto -> responseImageDto.sequence }.map { responseImageDto -> responseImageDto.imageUrl }, + advertisementTagTitle = this.advertisementTagType.toAdvertisementTagTitle(), + title = this.title, + createAt = this.createAt.toAdvertisementDetailDate(), + description = this.description +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDtoMapper.kt new file mode 100644 index 000000000..16b850da4 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementDtoMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementDto +import org.sopt.dateroad.domain.model.Advertisement + +fun ResponseAdvertisementDto.toDomain(): Advertisement = Advertisement( + advertisementId = this.advertisementId, + thumbnail = this.thumbnail +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementsDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementsDtoMapper.kt new file mode 100644 index 000000000..fe2108482 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAdvertisementsDtoMapper.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAdvertisementsDto +import org.sopt.dateroad.domain.model.Advertisement + +fun ResponseAdvertisementsDto.toDomain(): List = this.advertisements.map { responseAdvertisementDto -> responseAdvertisementDto.toDomain() } diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAuthDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAuthDtoMapper.kt new file mode 100644 index 000000000..49882874b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseAuthDtoMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseAuthDto +import org.sopt.dateroad.domain.model.Auth + +fun ResponseAuthDto.toDomain(): Auth = Auth( + accessToken = this.accessToken, + refreshToken = this.refreshToken +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDetailDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDetailDtoMapper.kt new file mode 100644 index 000000000..0563ead39 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDetailDtoMapper.kt @@ -0,0 +1,30 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseCourseDetailDto +import org.sopt.dateroad.data.mapper.toEntity.toCost +import org.sopt.dateroad.data.mapper.toEntity.toCourseDetailDate +import org.sopt.dateroad.data.mapper.toEntity.toDuration +import org.sopt.dateroad.data.mapper.toEntity.toStartAtString +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.type.MoneyTagType.Companion.toCostTagTitle + +fun ResponseCourseDetailDto.toDomain(): CourseDetail = CourseDetail( + courseId = this.courseId, + images = this.images.sortedBy { responseImageDto -> responseImageDto.sequence }.map { responseImageDto -> responseImageDto.imageUrl }, + like = this.like, + totalTime = this.totalTime.toDuration(), + date = this.date.toCourseDetailDate(), + city = this.city, + title = this.title, + description = this.description, + places = this.places.sortedBy { responsePlaceDto -> responsePlaceDto.sequence }.map { responsePlaceDto -> responsePlaceDto.toDomain() }, + totalCostTag = totalCost.toCostTagTitle(), + totalCost = totalCost.toCost(), + tags = this.tags.map { responseTagDto -> responseTagDto.tag }, + isAccess = this.isAccess, + free = this.free, + totalPoint = this.totalPoint, + isCourseMine = this.isCourseMine, + isUserLiked = this.isUserLiked, + startAt = this.startAt.toStartAtString() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDtoMapper.kt new file mode 100644 index 000000000..fd0e3ac2f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCourseDtoMapper.kt @@ -0,0 +1,17 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseCourseDto +import org.sopt.dateroad.data.mapper.toEntity.toDuration +import org.sopt.dateroad.data.mapper.toEntity.toLike +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.type.MoneyTagType.Companion.toCostTagTitle + +fun ResponseCourseDto.toDomain(): Course = Course( + courseId = this.courseId, + thumbnail = this.thumbnail, + title = this.title, + city = this.city, + cost = this.cost.toCostTagTitle(), + duration = this.duration.toDuration(), + like = this.like.toLike() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCoursesDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCoursesDtoMapper.kt new file mode 100644 index 000000000..11f273854 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseCoursesDtoMapper.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseCoursesDto +import org.sopt.dateroad.domain.model.Course + +fun ResponseCoursesDto.toDomain(): List = this.courses.map { course -> course.toDomain() } diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseNearestTimelineDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseNearestTimelineDtoMapper.kt new file mode 100644 index 000000000..444e56971 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseNearestTimelineDtoMapper.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseNearestTimelineDto +import org.sopt.dateroad.data.mapper.toEntity.toDDayString +import org.sopt.dateroad.data.mapper.toEntity.toFormattedDate +import org.sopt.dateroad.data.mapper.toEntity.toStartAtString +import org.sopt.dateroad.domain.model.NearestTimeline + +fun ResponseNearestTimelineDto.toDomain(): NearestTimeline = NearestTimeline( + timelineId = this.timelineId, + dDay = this.dDay.toDDayString(), + dateName = this.dateName, + date = toFormattedDate(), + startAt = this.startAt.toStartAtString() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt new file mode 100644 index 000000000..063510b47 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePlaceDtoMapper.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponsePlaceDto +import org.sopt.dateroad.data.mapper.toEntity.toDuration +import org.sopt.dateroad.domain.model.Place + +fun ResponsePlaceDto.toDomain() = Place( + title = this.title, + duration = this.duration.toDuration() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointDtoMapper.kt new file mode 100644 index 000000000..e074ef98e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointDtoMapper.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointDto +import org.sopt.dateroad.data.mapper.toEntity.toGainedPoint +import org.sopt.dateroad.data.mapper.toEntity.toUsedPoint +import org.sopt.dateroad.domain.model.Point + +fun ResponsePointDto.toGainedPointDomain() = Point( + point = this.point.toGainedPoint(), + description = this.description, + createdAt = this.createdAt +) + +fun ResponsePointDto.toUsedPointDomain() = Point( + point = this.point.toUsedPoint(), + description = this.description, + createdAt = this.createdAt +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointHistoryDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointHistoryDtoMapper.kt new file mode 100644 index 000000000..bb7a4e572 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointHistoryDtoMapper.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointHistoryDto +import org.sopt.dateroad.domain.model.PointHistory + +fun ResponsePointHistoryDto.toDomain(): PointHistory = PointHistory( + gained = this.gained.toGainedPointDomain(), + used = this.used.toUsedPointDomain() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointsDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointsDtoMapper.kt new file mode 100644 index 000000000..50dcb1f10 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponsePointsDtoMapper.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponsePointsDto +import org.sopt.dateroad.domain.model.Point + +fun ResponsePointsDto.toGainedPointDomain(): List = this.points.map { responsePointDto -> + responsePointDto.toGainedPointDomain() +} + +fun ResponsePointsDto.toUsedPointDomain(): List = this.points.map { responsePointDto -> + responsePointDto.toUsedPointDomain() +} diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseProfileDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseProfileDtoMapper.kt new file mode 100644 index 000000000..cdc89005c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseProfileDtoMapper.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseProfileDto +import org.sopt.dateroad.data.mapper.toEntity.toPoint +import org.sopt.dateroad.domain.model.Profile + +fun ResponseProfileDto.toDomain(): Profile = Profile( + name = this.name, + tag = this.tags, + point = this.point.toPoint(), + imageUrl = this.imageUrl +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTagDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTagDtoMapper.kt new file mode 100644 index 000000000..df287c663 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTagDtoMapper.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseTagDto +import org.sopt.dateroad.presentation.type.DateTagType + +fun ResponseTagDto.toDomain(): DateTagType = DateTagType.valueOf(this.tag) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDetailDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDetailDtoMapper.kt new file mode 100644 index 000000000..65fe05a1e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDetailDtoMapper.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelineDetailDto +import org.sopt.dateroad.data.mapper.toEntity.toBasicDates +import org.sopt.dateroad.data.mapper.toEntity.toDDayString +import org.sopt.dateroad.data.mapper.toEntity.toStartAtString +import org.sopt.dateroad.domain.model.TimelineDetail + +fun ResponseTimelineDetailDto.toDomain(): TimelineDetail = TimelineDetail( + timelineId = this.timelineId, + title = this.title, + startAt = this.startAt.toStartAtString(), + city = this.city, + tags = this.tags.map { responseTagDto -> responseTagDto.tag }, + date = this.date.toBasicDates(), + places = this.places.sortedBy { responsePlaceDto -> responsePlaceDto.sequence }.map { it.toDomain() }, + dDay = this.dDay.toDDayString() +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDtoMapper.kt new file mode 100644 index 000000000..2835a0aee --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseTimelineDtoMapper.kt @@ -0,0 +1,25 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseTimelineDto +import org.sopt.dateroad.data.mapper.toEntity.toBasicDates +import org.sopt.dateroad.data.mapper.toEntity.toDDayString +import org.sopt.dateroad.data.mapper.toEntity.toFormattedDate +import org.sopt.dateroad.domain.model.Timeline + +fun ResponseTimelineDto.toFutureTimelineDomain(): Timeline = Timeline( + timelineId = this.timelineId, + dDay = this.dDay.toDDayString(), + title = this.title, + date = this.date.toFormattedDate(), + city = this.city, + tags = this.tags.map { it.toDomain() } +) + +fun ResponseTimelineDto.toPastTimelineDomain(): Timeline = Timeline( + timelineId = this.timelineId, + dDay = this.dDay.toDDayString(), + title = this.title, + date = this.date.toBasicDates(), + city = this.city, + tags = this.tags.map { it.toDomain() } +) diff --git a/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseUserPointDtoMapper.kt b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseUserPointDtoMapper.kt new file mode 100644 index 000000000..dc2f1ddef --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/mapper/todomain/ResponseUserPointDtoMapper.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.data.mapper.todomain + +import org.sopt.dateroad.data.dataremote.model.response.ResponseUserPointDto +import org.sopt.dateroad.data.mapper.toEntity.toPoint +import org.sopt.dateroad.domain.model.UserPoint + +fun ResponseUserPointDto.toDomain(): UserPoint = UserPoint( + name = this.name, + point = this.point.toPoint(), + imageUrl = this.image +) diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AdvertisementRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AdvertisementRepositoryImpl.kt new file mode 100644 index 000000000..f938425b5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AdvertisementRepositoryImpl.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.AdvertisementRemoteDataSource +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.Advertisement +import org.sopt.dateroad.domain.model.AdvertisementDetail +import org.sopt.dateroad.domain.repository.AdvertisementRepository + +class AdvertisementRepositoryImpl @Inject constructor( + private val advertisementRemoteDataSource: AdvertisementRemoteDataSource +) : AdvertisementRepository { + override suspend fun getAdvertisementDetail(advertisementId: Int): Result = runCatching { + advertisementRemoteDataSource.getAdvertisementDetail(advertisementId = advertisementId).toDomain() + } + + override suspend fun getHomeAdvertisements(): Result> = runCatching { + advertisementRemoteDataSource.getHomeAdvertisements().toDomain() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AuthRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AuthRepositoryImpl.kt new file mode 100644 index 000000000..97bc64d0b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/AuthRepositoryImpl.kt @@ -0,0 +1,61 @@ +package org.sopt.dateroad.data.repositoryimpl + +import android.content.ContentResolver +import android.net.Uri +import javax.inject.Inject +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.sopt.dateroad.data.dataremote.datasource.AuthRemoteDataSource +import org.sopt.dateroad.data.dataremote.model.request.RequestWithdrawDto +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.APPLICATION_JSON +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.HTTPS +import org.sopt.dateroad.data.dataremote.util.ApiConstraints.PROFILE_FORM_DATA_IMAGE +import org.sopt.dateroad.data.dataremote.util.ContentUriRequestBody +import org.sopt.dateroad.data.mapper.todata.toData +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.Auth +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.model.SignIn +import org.sopt.dateroad.domain.model.SignUp +import org.sopt.dateroad.domain.repository.AuthRepository + +class AuthRepositoryImpl @Inject constructor( + private val contentResolver: ContentResolver, + private val authRemoteDataSource: AuthRemoteDataSource +) : AuthRepository { + override suspend fun deleteSignOut(): Result = runCatching { + authRemoteDataSource.deleteSignOut() + } + + override suspend fun deleteWithdraw(authCode: String?): Result = runCatching { + authRemoteDataSource.deleteWithdraw(requestWithdrawDto = RequestWithdrawDto(authCode)) + } + + override suspend fun getNicknameCheck(name: String): Result = runCatching { + authRemoteDataSource.getNicknameCheck(name = name) + } + + override suspend fun postSignIn(authorization: String, signIn: SignIn): Result = runCatching { + authRemoteDataSource.postSignIn(authorization = authorization, requestSignInDto = signIn.toData()).toDomain() + } + + override suspend fun postSignUp(signUp: SignUp): Result = runCatching { + authRemoteDataSource.postSignUp( + image = if (signUp.image.isEmpty()) null else ContentUriRequestBody(contentResolver = contentResolver, uri = Uri.parse(signUp.image)).toFormData(name = PROFILE_FORM_DATA_IMAGE), + userSignUpData = Json.encodeToString(signUp.userSignUpInfo.toData()).toRequestBody(APPLICATION_JSON.toMediaType()), + tags = (Json.encodeToString(signUp.tag.toData()).substringAfter(":").substringBeforeLast("}")).toRequestBody(APPLICATION_JSON.toMediaType()) + ).toDomain() + } + + override suspend fun patchEditProfile(editProfile: EditProfile): Result = runCatching { + authRemoteDataSource.patchEditProfile( + name = editProfile.name.toRequestBody(), + tags = (Json.encodeToString(editProfile.tags.toData()).substringAfter(":").substringBeforeLast("}")).toRequestBody(APPLICATION_JSON.toMediaType()), + image = editProfile.image?.takeIf { image -> image.isNotEmpty() && !image.startsWith(HTTPS) } + ?.let { uri -> ContentUriRequestBody(contentResolver, Uri.parse(uri)).toFormData(name = PROFILE_FORM_DATA_IMAGE) }, + isDefaultImage = Json.encodeToString(editProfile.image.isNullOrEmpty()).toRequestBody(APPLICATION_JSON.toMediaType()) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/CourseRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/CourseRepositoryImpl.kt new file mode 100644 index 000000000..18946243b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/CourseRepositoryImpl.kt @@ -0,0 +1,73 @@ +package org.sopt.dateroad.data.repositoryimpl + +import android.content.ContentResolver +import android.net.Uri +import javax.inject.Inject +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.sopt.dateroad.data.dataremote.datasource.CourseRemoteDataSource +import org.sopt.dateroad.data.dataremote.util.ContentUriRequestBody +import org.sopt.dateroad.data.mapper.todata.toCourseData +import org.sopt.dateroad.data.mapper.todata.toData +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.repository.CourseRepository +import org.sopt.dateroad.domain.type.GyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.type.SeoulAreaType +import org.sopt.dateroad.domain.type.SortByType + +class CourseRepositoryImpl @Inject constructor( + private val contentResolver: ContentResolver, + private val courseRemoteDataSource: CourseRemoteDataSource +) : CourseRepository { + override suspend fun deleteCourse(courseId: Int): Result = runCatching { + courseRemoteDataSource.deleteCourse(courseId = courseId) + } + + override suspend fun deleteCourseLike(courseId: Int): Result = runCatching { + courseRemoteDataSource.deleteCourseLike(courseId = courseId) + } + + override suspend fun getCourseDetail(courseId: Int): Result = runCatching { + courseRemoteDataSource.getCourseDetail(courseId = courseId).toDomain() + } + + override suspend fun getFilteredCourses(country: RegionType?, city: Any?, cost: MoneyTagType?): Result> = runCatching { + courseRemoteDataSource.getFilteredCourses( + country = country?.title, + city = city?.let { + when (it) { + is SeoulAreaType -> it.title + is GyeonggiAreaType -> it.title + is IncheonAreaType -> it.title + else -> null + } + }, + cost = cost?.costParameter + ).toDomain() + } + + override suspend fun getSortedCourses(sortedBy: SortByType): Result> = runCatching { + courseRemoteDataSource.getSortedCourses(sortBy = sortedBy.name).toDomain() + } + + override suspend fun postCourse(enroll: Enroll): Result = runCatching { + courseRemoteDataSource.postCourse( + images = enroll.images.map { image -> ContentUriRequestBody(contentResolver = contentResolver, uri = Uri.parse(image)).toFormData() }, + course = Json.encodeToString(enroll.toCourseData()).toRequestBody("application/json".toMediaType()), + places = Json.encodeToString(enroll.places.mapIndexed { index, place -> place.toData(sequence = index + 1) }).toRequestBody("application/json".toMediaType()), + tags = Json.encodeToString(enroll.tags.map { tag -> tag.toData() }).toRequestBody("application/json".toMediaType()) + ) + } + + override suspend fun postCourseLike(courseId: Int): Result = runCatching { + courseRemoteDataSource.postCourseLike(courseId = courseId) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/MyCourseRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/MyCourseRepositoryImpl.kt new file mode 100644 index 000000000..638dc178b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/MyCourseRepositoryImpl.kt @@ -0,0 +1,19 @@ +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.MyCourseRemoteDataSource +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.repository.MyCourseRepository + +class MyCourseRepositoryImpl @Inject constructor( + private val myCourseRemoteDataSource: MyCourseRemoteDataSource +) : MyCourseRepository { + override suspend fun getMyCourseEnroll(): Result> = runCatching { + myCourseRemoteDataSource.getMyCourseEnroll().toDomain() + } + + override suspend fun getMyCourseRead(): Result> = runCatching { + myCourseRemoteDataSource.getMyCourseRead().toDomain() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/ProfileRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/ProfileRepositoryImpl.kt new file mode 100644 index 000000000..91b18e4d6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/ProfileRepositoryImpl.kt @@ -0,0 +1,16 @@ + +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.ProfileRemoteDataSource +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.Profile +import org.sopt.dateroad.domain.repository.ProfileRepository + +class ProfileRepositoryImpl @Inject constructor( + private val profileDataSource: ProfileRemoteDataSource +) : ProfileRepository { + override suspend fun getUsers(): Result = runCatching { + profileDataSource.getProfile().toDomain() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/TimelineRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/TimelineRepositoryImpl.kt new file mode 100644 index 000000000..5d9e1452d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/TimelineRepositoryImpl.kt @@ -0,0 +1,43 @@ +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.TimelineRemoteDataSource +import org.sopt.dateroad.data.mapper.todata.toTimelineData +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.data.mapper.todomain.toFutureTimelineDomain +import org.sopt.dateroad.data.mapper.todomain.toPastTimelineDomain +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.domain.repository.TimelineRepository +import org.sopt.dateroad.domain.type.TimelineTimeType + +class TimelineRepositoryImpl @Inject constructor( + private val timelineRemoteDataSource: TimelineRemoteDataSource +) : TimelineRepository { + override suspend fun deleteTimeline(timelineId: Int) { + timelineRemoteDataSource.deleteTimeline(timelineId) + } + + override suspend fun getTimelineDetail(timelineId: Int): Result = runCatching { + timelineRemoteDataSource.getTimelineDetail(timelineId).toDomain() + } + + override suspend fun getTimelines(timelineTimeType: TimelineTimeType): Result> = runCatching { + timelineRemoteDataSource.getTimelines(timelineTimeType.name).timelines.map { + when (timelineTimeType) { + TimelineTimeType.PAST -> it.toPastTimelineDomain() + TimelineTimeType.FUTURE -> it.toFutureTimelineDomain() + } + } + } + + override suspend fun getNearestTimeline(): Result = runCatching { + timelineRemoteDataSource.getNearestTimeline().toDomain() + } + + override suspend fun postTimeline(enroll: Enroll) = runCatching { + timelineRemoteDataSource.postTimeline(requestTimelineDto = enroll.toTimelineData()) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserInfoRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserInfoRepositoryImpl.kt new file mode 100644 index 000000000..0ea6797cb --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserInfoRepositoryImpl.kt @@ -0,0 +1,31 @@ +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.dateroad.domain.repository.UserInfoRepository + +class UserInfoRepositoryImpl @Inject constructor( + private val userInfoLocalDataSource: UserInfoLocalDataSource +) : UserInfoRepository { + override fun setAccessToken(accessToken: String) { + userInfoLocalDataSource.accessToken = accessToken + } + + override fun getAccessToken(): String = userInfoLocalDataSource.accessToken + + override fun setRefreshToken(refreshToken: String) { + userInfoLocalDataSource.refreshToken = refreshToken + } + + override fun getRefreshToken(): String = userInfoLocalDataSource.refreshToken + + override fun setNickname(nickname: String) { + userInfoLocalDataSource.nickname = nickname + } + + override fun getNickname(): String = userInfoLocalDataSource.nickname + + override fun clear() { + userInfoLocalDataSource.clear() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserPointRepositoryImpl.kt b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserPointRepositoryImpl.kt new file mode 100644 index 000000000..6bcaa4ade --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/data/repositoryimpl/UserPointRepositoryImpl.kt @@ -0,0 +1,26 @@ +package org.sopt.dateroad.data.repositoryimpl + +import javax.inject.Inject +import org.sopt.dateroad.data.dataremote.datasource.UserPointRemoteDataSource +import org.sopt.dateroad.data.mapper.todata.toData +import org.sopt.dateroad.data.mapper.todomain.toDomain +import org.sopt.dateroad.domain.model.PointHistory +import org.sopt.dateroad.domain.model.UsePoint +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.domain.repository.UserPointRepository + +class UserPointRepositoryImpl @Inject constructor( + private val userPointRemoteDataSource: UserPointRemoteDataSource +) : UserPointRepository { + override suspend fun getUserPoint(): Result = runCatching { + userPointRemoteDataSource.getUserPoint().toDomain() + } + + override suspend fun getPointHistory(): Result = runCatching { + userPointRemoteDataSource.getPointHistory().toDomain() + } + + override suspend fun postUsePoint(courseId: Int, usePoint: UsePoint) { + userPointRemoteDataSource.postUsePoint(courseId = courseId, requestUsePointDto = usePoint.toData()) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/di/ContentResolverModule.kt b/app/src/main/java/org/sopt/dateroad/di/ContentResolverModule.kt new file mode 100644 index 000000000..db59fd779 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/ContentResolverModule.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.di + +import android.content.ContentResolver +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ContentResolverModule { + @Provides + @Singleton + fun providesContentResolver( + @ApplicationContext context: Context + ): ContentResolver = context.contentResolver +} diff --git a/app/src/main/java/org/sopt/dateroad/di/DataSourceModule.kt b/app/src/main/java/org/sopt/dateroad/di/DataSourceModule.kt new file mode 100644 index 000000000..f8aba0fb9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/DataSourceModule.kt @@ -0,0 +1,59 @@ +package org.sopt.dateroad.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import org.sopt.dateroad.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.dateroad.data.datalocal.datasourceimpl.UserInfoLocalDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasource.AdvertisementRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.AuthRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.CourseRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.MyCourseRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.ProfileRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.TimelineRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasource.UserPointRemoteDataSource +import org.sopt.dateroad.data.dataremote.datasourceimpl.AdvertisementRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.AuthRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.CourseRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.MyCourseRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.ProfileRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.TimelineRemoteDataSourceImpl +import org.sopt.dateroad.data.dataremote.datasourceimpl.UserPointRemoteDataSourceImpl + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataSourceModule { + @Binds + @Singleton + abstract fun bindsUserInfoLocalDataSource(userInfoLocalDataSourceImpl: UserInfoLocalDataSourceImpl): UserInfoLocalDataSource + + @Binds + @Singleton + abstract fun bindsAdvertisementDataSource(advertisementRemoteDataSourceImpl: AdvertisementRemoteDataSourceImpl): AdvertisementRemoteDataSource + + @Binds + @Singleton + abstract fun bindsAuthRemoteDataSource(authRemoteDataSourceImpl: AuthRemoteDataSourceImpl): AuthRemoteDataSource + + @Binds + @Singleton + abstract fun bindsCourseRemoteDataSource(courseRemoteDataSourceImpl: CourseRemoteDataSourceImpl): CourseRemoteDataSource + + @Binds + @Singleton + abstract fun bindTimelineRemoteDataSource(timelineRemoteDataSourceImpl: TimelineRemoteDataSourceImpl): TimelineRemoteDataSource + + @Binds + @Singleton + abstract fun bindsMyCourseRemoteDataSource(myCourseRemoteDataSourceImpl: MyCourseRemoteDataSourceImpl): MyCourseRemoteDataSource + + @Binds + @Singleton + abstract fun bindsUserPointRemoteDataSource(userPointRemoteDataSourceImpl: UserPointRemoteDataSourceImpl): UserPointRemoteDataSource + + @Binds + @Singleton + abstract fun bindsProfileDataSource(profileDataSourceImpl: ProfileRemoteDataSourceImpl): ProfileRemoteDataSource +} diff --git a/app/src/main/java/org/sopt/dateroad/di/NetworkModule.kt b/app/src/main/java/org/sopt/dateroad/di/NetworkModule.kt new file mode 100644 index 000000000..9c73fd052 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/NetworkModule.kt @@ -0,0 +1,78 @@ +package org.sopt.dateroad.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.concurrent.TimeUnit +import javax.inject.Singleton +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.dateroad.BuildConfig +import org.sopt.dateroad.BuildConfig.DEBUG +import org.sopt.dateroad.data.dataremote.interceptor.AuthInterceptor +import org.sopt.dateroad.di.qualifier.Auth +import org.sopt.dateroad.di.qualifier.DateRoad +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @OptIn(ExperimentalSerializationApi::class) + @Provides + @Singleton + fun providesJson(): Json = + Json { + isLenient = true + prettyPrint = true + explicitNulls = false + ignoreUnknownKeys = true + } + + @Provides + @Singleton + fun providesOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor, + @Auth authInterceptor: Interceptor + ): OkHttpClient = + OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + writeTimeout(10, TimeUnit.SECONDS) + readTimeout(10, TimeUnit.SECONDS) + addInterceptor(authInterceptor) + if (DEBUG) addInterceptor(loggingInterceptor) + }.build() + + @Provides + @Singleton + fun providesLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + @Provides + @Singleton + @Auth + fun provideAuthInterceptor(interceptor: AuthInterceptor): Interceptor = interceptor + + @ExperimentalSerializationApi + @Provides + @DateRoad + @Singleton + fun providesDateRoadRetrofit( + okHttpClient: OkHttpClient, + json: Json + ): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(okHttpClient) + .addConverterFactory( + json.asConverterFactory(requireNotNull("application/json".toMediaTypeOrNull())) + ) + .build() +} diff --git a/app/src/main/java/org/sopt/dateroad/di/RepositoryModule.kt b/app/src/main/java/org/sopt/dateroad/di/RepositoryModule.kt new file mode 100644 index 000000000..26c1d5088 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/RepositoryModule.kt @@ -0,0 +1,53 @@ +package org.sopt.dateroad.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import org.sopt.dateroad.data.repositoryimpl.AdvertisementRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.AuthRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.CourseRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.MyCourseRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.ProfileRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.TimelineRepositoryImpl +import org.sopt.dateroad.data.repositoryimpl.UserPointRepositoryImpl +import org.sopt.dateroad.domain.repository.AdvertisementRepository +import org.sopt.dateroad.domain.repository.AuthRepository +import org.sopt.dateroad.domain.repository.CourseRepository +import org.sopt.dateroad.domain.repository.MyCourseRepository +import org.sopt.dateroad.domain.repository.ProfileRepository +import org.sopt.dateroad.domain.repository.TimelineRepository +import org.sopt.dateroad.domain.repository.UserPointRepository + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindsAdvertisementRepository(advertisementRepositoryImpl: AdvertisementRepositoryImpl): AdvertisementRepository + + @Binds + @Singleton + abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository + + @Binds + @Singleton + abstract fun bindsCourseRepository(courseRepositoryImpl: CourseRepositoryImpl): CourseRepository + + @Binds + @Singleton + abstract fun bindsTimelineRepository(timelineRepositoryImpl: TimelineRepositoryImpl): TimelineRepository + + @Binds + @Singleton + abstract fun bindsMyCourseRepository(myCourseRepositoryImpl: MyCourseRepositoryImpl): MyCourseRepository + + @Binds + @Singleton + abstract fun bindsUserPointRepository(userPointRepositoryImpl: UserPointRepositoryImpl): UserPointRepository + + @Binds + @Singleton + abstract fun bindsProfileRepository(profileRepositoryImpl: ProfileRepositoryImpl): ProfileRepository +} diff --git a/app/src/main/java/org/sopt/dateroad/di/ServiceModule.kt b/app/src/main/java/org/sopt/dateroad/di/ServiceModule.kt new file mode 100644 index 000000000..7fe17c0fd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/ServiceModule.kt @@ -0,0 +1,54 @@ +package org.sopt.dateroad.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.dateroad.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.dateroad.data.dataremote.service.AdvertisementService +import org.sopt.dateroad.data.dataremote.service.AuthService +import org.sopt.dateroad.data.dataremote.service.CourseService +import org.sopt.dateroad.data.dataremote.service.MyCourseService +import org.sopt.dateroad.data.dataremote.service.ProfileService +import org.sopt.dateroad.data.dataremote.service.TimelineService +import org.sopt.dateroad.data.dataremote.service.UserPointService +import org.sopt.dateroad.data.repositoryimpl.UserInfoRepositoryImpl +import org.sopt.dateroad.di.qualifier.DateRoad +import org.sopt.dateroad.domain.repository.UserInfoRepository +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + fun providesAdvertisementService(@DateRoad retrofit: Retrofit): AdvertisementService = + retrofit.create(AdvertisementService::class.java) + + @Provides + fun providesAuthService(@DateRoad retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) + + @Provides + fun providesCourseService(@DateRoad retrofit: Retrofit): CourseService = + retrofit.create(CourseService::class.java) + + @Provides + fun provideTimelineService(@DateRoad retrofit: Retrofit): TimelineService = + retrofit.create(TimelineService::class.java) + + @Provides + fun providesMyCourseService(@DateRoad retrofit: Retrofit): MyCourseService = + retrofit.create(MyCourseService::class.java) + + @Provides + fun providesUserPointService(@DateRoad retrofit: Retrofit): UserPointService = + retrofit.create(UserPointService::class.java) + + @Provides + fun provideUserInfoRepository(userInfoLocalDataSource: UserInfoLocalDataSource): UserInfoRepository = + UserInfoRepositoryImpl(userInfoLocalDataSource) + + @Provides + fun providesProfileService(@DateRoad retrofit: Retrofit): ProfileService = + retrofit.create(ProfileService::class.java) +} diff --git a/app/src/main/java/org/sopt/dateroad/di/qualifier/Qualifier.kt b/app/src/main/java/org/sopt/dateroad/di/qualifier/Qualifier.kt new file mode 100644 index 000000000..73672ec91 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/di/qualifier/Qualifier.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.di.qualifier + +import javax.inject.Qualifier + +@Qualifier +annotation class DateRoad + +@Qualifier +annotation class Auth diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Advertisement.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Advertisement.kt new file mode 100644 index 000000000..cde88ba38 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Advertisement.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.model + +data class Advertisement( + val advertisementId: Int, + val thumbnail: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/AdvertisementDetail.kt b/app/src/main/java/org/sopt/dateroad/domain/model/AdvertisementDetail.kt new file mode 100644 index 000000000..1f8c242a5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/AdvertisementDetail.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.domain.model + +data class AdvertisementDetail( + val images: List = listOf(), + val title: String = "", + val createAt: String = "", + val description: String = "", + val advertisementTagTitle: String = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Auth.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Auth.kt new file mode 100644 index 000000000..c74b29279 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Auth.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.model + +data class Auth( + val accessToken: String, + val refreshToken: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Course.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Course.kt new file mode 100644 index 000000000..78cb95e62 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Course.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.domain.model + +data class Course( + val courseId: Int, + val thumbnail: String, + val title: String, + val city: String, + val cost: String, + val duration: String, + val like: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/CourseDetail.kt b/app/src/main/java/org/sopt/dateroad/domain/model/CourseDetail.kt new file mode 100644 index 000000000..7cfcab67f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/CourseDetail.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.domain.model + +data class CourseDetail( + val courseId: Int = 0, + val images: List = listOf(), + val like: Int = 0, + val totalTime: String = "", + val date: String = "", + val city: String = "", + val title: String = "", + val description: String = "", + val places: List = listOf(), + val totalCostTag: String = "", + val totalCost: String = "", + val tags: List = listOf(), + val isAccess: Boolean = false, + val free: Int = 0, + val totalPoint: Int = 0, + val isCourseMine: Boolean = false, + val isUserLiked: Boolean = false, + val startAt: String = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/EditProfile.kt b/app/src/main/java/org/sopt/dateroad/domain/model/EditProfile.kt new file mode 100644 index 000000000..6a09be0f6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/EditProfile.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.model + +data class EditProfile( + val name: String = "", + val tags: List = listOf(), + val image: String? = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Enroll.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Enroll.kt new file mode 100644 index 000000000..5e71969dc --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Enroll.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad.domain.model + +import org.sopt.dateroad.domain.type.RegionType + +data class Enroll( + val images: List = listOf(), + val title: String = "", + val date: String = "", + val startAt: String = "", + val tags: List = listOf(), + val country: RegionType? = null, + val city: Any? = null, + val places: List = listOf(), + val description: String = "", + val cost: String = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/NearestTimeline.kt b/app/src/main/java/org/sopt/dateroad/domain/model/NearestTimeline.kt new file mode 100644 index 000000000..a24bbd824 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/NearestTimeline.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.domain.model + +data class NearestTimeline( + val timelineId: Int = 0, + val dDay: String = "", + val dateName: String = "", + val date: String = "", + val startAt: String = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Place.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Place.kt new file mode 100644 index 000000000..1f956010d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Place.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.model + +data class Place( + val title: String = "", + val duration: String = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Point.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Point.kt new file mode 100644 index 000000000..1f6933969 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Point.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.model + +data class Point( + val point: String, + val description: String, + val createdAt: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/PointHistory.kt b/app/src/main/java/org/sopt/dateroad/domain/model/PointHistory.kt new file mode 100644 index 000000000..ef7cb3f7c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/PointHistory.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.model + +data class PointHistory( + val gained: List = listOf(), + val used: List = listOf() +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Profile.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Profile.kt new file mode 100644 index 000000000..166056676 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Profile.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.domain.model + +data class Profile( + val name: String = "", + val tag: List = listOf(), + val point: String = "", + val imageUrl: String? = null +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/SignIn.kt b/app/src/main/java/org/sopt/dateroad/domain/model/SignIn.kt new file mode 100644 index 000000000..a30dca249 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/SignIn.kt @@ -0,0 +1,5 @@ +package org.sopt.dateroad.domain.model + +data class SignIn( + val platform: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/SignUp.kt b/app/src/main/java/org/sopt/dateroad/domain/model/SignUp.kt new file mode 100644 index 000000000..c2804fe6c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/SignUp.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.model + +data class SignUp( + val userSignUpInfo: UserSignUpInfo = UserSignUpInfo(), + val image: String = "", + val tag: List = listOf() +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/Timeline.kt b/app/src/main/java/org/sopt/dateroad/domain/model/Timeline.kt new file mode 100644 index 000000000..8ab1469a0 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/Timeline.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.model + +import org.sopt.dateroad.presentation.type.DateTagType + +data class Timeline( + val timelineId: Int = 0, + val dDay: String = "", + val title: String = "", + val date: String = "", + val city: String = "", + val tags: List = listOf() +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/TimelineDetail.kt b/app/src/main/java/org/sopt/dateroad/domain/model/TimelineDetail.kt new file mode 100644 index 000000000..a43e7e3c1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/TimelineDetail.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.model + +data class TimelineDetail( + val timelineId: Int = 0, + val title: String = "", + val startAt: String = "", + val city: String = "", + val tags: List = listOf(), + val date: String = "", + val dDay: String = "", + val places: List = listOf() +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/UsePoint.kt b/app/src/main/java/org/sopt/dateroad/domain/model/UsePoint.kt new file mode 100644 index 000000000..80562846c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/UsePoint.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.model + +data class UsePoint( + val point: Int, + val type: String, + val description: String +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/UserPoint.kt b/app/src/main/java/org/sopt/dateroad/domain/model/UserPoint.kt new file mode 100644 index 000000000..b8adea83f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/UserPoint.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.model + +data class UserPoint( + val name: String = "", + val point: String = "", + val imageUrl: String? = "" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/model/UserSignUpInfo.kt b/app/src/main/java/org/sopt/dateroad/domain/model/UserSignUpInfo.kt new file mode 100644 index 000000000..d78a19561 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/model/UserSignUpInfo.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.model + +data class UserSignUpInfo( + val name: String = "", + val platform: String = "KAKAO" +) diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/AdvertisementRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/AdvertisementRepository.kt new file mode 100644 index 000000000..369bd0b17 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/AdvertisementRepository.kt @@ -0,0 +1,10 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Advertisement +import org.sopt.dateroad.domain.model.AdvertisementDetail + +interface AdvertisementRepository { + suspend fun getAdvertisementDetail(advertisementId: Int): Result + + suspend fun getHomeAdvertisements(): Result> +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/AuthRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/AuthRepository.kt new file mode 100644 index 000000000..2be2cc4da --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/AuthRepository.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Auth +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.model.SignIn +import org.sopt.dateroad.domain.model.SignUp + +interface AuthRepository { + suspend fun deleteSignOut(): Result + + suspend fun deleteWithdraw(authCode: String?): Result + + suspend fun getNicknameCheck(name: String): Result + + suspend fun postSignIn(authorization: String, signIn: SignIn): Result + + suspend fun postSignUp(signUp: SignUp): Result + + suspend fun patchEditProfile(editProfile: EditProfile): Result +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/CourseRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/CourseRepository.kt new file mode 100644 index 000000000..834e54f95 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/CourseRepository.kt @@ -0,0 +1,24 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.type.SortByType + +interface CourseRepository { + suspend fun deleteCourse(courseId: Int): Result + + suspend fun deleteCourseLike(courseId: Int): Result + + suspend fun getCourseDetail(courseId: Int): Result + + suspend fun getFilteredCourses(country: RegionType?, city: Any?, cost: MoneyTagType?): Result> + + suspend fun getSortedCourses(sortedBy: SortByType): Result> + + suspend fun postCourse(enroll: Enroll): Result + + suspend fun postCourseLike(courseId: Int): Result +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/MyCourseRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/MyCourseRepository.kt new file mode 100644 index 000000000..cc6c62b9b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/MyCourseRepository.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Course + +interface MyCourseRepository { + suspend fun getMyCourseEnroll(): Result> + + suspend fun getMyCourseRead(): Result> +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/ProfileRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/ProfileRepository.kt new file mode 100644 index 000000000..215060a5a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/ProfileRepository.kt @@ -0,0 +1,7 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Profile + +interface ProfileRepository { + suspend fun getUsers(): Result +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/TimelineRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/TimelineRepository.kt new file mode 100644 index 000000000..46d1ffe09 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/TimelineRepository.kt @@ -0,0 +1,19 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.domain.type.TimelineTimeType + +interface TimelineRepository { + suspend fun deleteTimeline(timelineId: Int) + + suspend fun getTimelineDetail(timelineId: Int): Result + + suspend fun getTimelines(timelineTimeType: TimelineTimeType): Result> + + suspend fun getNearestTimeline(): Result + + suspend fun postTimeline(enroll: Enroll): Result +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/UserInfoRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/UserInfoRepository.kt new file mode 100644 index 000000000..9cbf7b8b1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/UserInfoRepository.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.domain.repository + +interface UserInfoRepository { + fun setAccessToken(accessToken: String) + fun getAccessToken(): String + fun setRefreshToken(refreshToken: String) + fun getRefreshToken(): String + fun setNickname(nickname: String) + fun getNickname(): String + fun clear() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/repository/UserPointRepository.kt b/app/src/main/java/org/sopt/dateroad/domain/repository/UserPointRepository.kt new file mode 100644 index 000000000..cbf8e89c3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/repository/UserPointRepository.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.repository + +import org.sopt.dateroad.domain.model.PointHistory +import org.sopt.dateroad.domain.model.UsePoint +import org.sopt.dateroad.domain.model.UserPoint + +interface UserPointRepository { + suspend fun getUserPoint(): Result + + suspend fun getPointHistory(): Result + + suspend fun postUsePoint(courseId: Int, usePoint: UsePoint) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/AdvertisementTagType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/AdvertisementTagType.kt new file mode 100644 index 000000000..c444bff04 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/AdvertisementTagType.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Advertisement + +enum class AdvertisementTagType( + val title: String +) { + EDITOR(title = Advertisement.EDITOR), + AD(title = Advertisement.AD), + ABOUT(title = Advertisement.ABOUT), + HOT(title = Advertisement.HOT); + + companion object { + fun String.toAdvertisementTagTitle(): String = entries.find { it.name == this }?.title ?: "" + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/GyeonggiAreaType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/GyeonggiAreaType.kt new file mode 100644 index 000000000..8668ae3f5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/GyeonggiAreaType.kt @@ -0,0 +1,65 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Gyeonggi + +enum class GyeonggiAreaType( + val title: String +) { + GYEONGGI_ENTIRE( + title = Gyeonggi.GYEONGGI_ENTIRE + ), + SEONGNAM( + title = Gyeonggi.SEONGNAM + ), + SUWON( + title = Gyeonggi.SUWON + ), + GOYANG_PAJU( + title = Gyeonggi.GOYANG_PAJU + ), + GIMPO( + title = Gyeonggi.GIMPO + ), + YONGIN_HWASEONG( + title = Gyeonggi.YONGIN_HWASEONG + ), + ANYANG_GWACHEON( + title = Gyeonggi.ANYANG_GWACHEON + ), + POCHEON_YANGJU( + title = Gyeonggi.POCHEON_YANGJU + ), + NAMYANGJU_UIJEONGBU( + title = Gyeonggi.NAMYANGJU_UIJEONGBU + ), + GWANGJU_ICHEON_YEOJU( + title = Gyeonggi.GWANGJU_ICHEON_YEOJU + ), + GAPYEONG_YANGPYEONG( + title = Gyeonggi.GAPYEONG_YANGPYEONG + ), + GUNPO_UIWANG( + title = Gyeonggi.GUNPO_UIWANG + ), + HANAM_GURI( + title = Gyeonggi.HANAM_GURI + ), + SIHEUNG_GWANGMYEONG( + title = Gyeonggi.SIHEUNG_GWANGMYEONG + ), + BUCHEON_ANSHAN( + title = Gyeonggi.BUCHEON_ANSHAN + ), + DONGDUCHEON_YEONCHEON( + title = Gyeonggi.DONGDUCHEON_YEONCHEON + ), + PYEONGTAEK_OSAN_ANSEONG( + title = Gyeonggi.PYEONGTAEK_OSAN_ANSEONG + ); + + companion object { + fun String.toGyeonggiAreaTitle(): String = entries.find { it.name == this }?.title ?: "" + fun String.toGyeonggiAreaType(): GyeonggiAreaType? = entries.find { it.name == this } + fun String.fromTitleToGyeonggiAreaType(): GyeonggiAreaType? = entries.find { it.title == this } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/IncheonAreaType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/IncheonAreaType.kt new file mode 100644 index 000000000..1fb0a468a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/IncheonAreaType.kt @@ -0,0 +1,17 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Incheon + +enum class IncheonAreaType( + val title: String +) { + INCHEON_ENTIRE( + title = Incheon.INCHEON_ENTIRE + ); + + companion object { + fun String.toIncheonAreaTitle(): String = entries.find { it.name == this }?.title ?: "" + fun String.toIncheonAreaType(): IncheonAreaType? = entries.find { it.name == this } + fun String.fromTitleToIncheonAreaType(): IncheonAreaType? = entries.find { it.title == this } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/MoneyTagType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/MoneyTagType.kt new file mode 100644 index 000000000..f812171a7 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/MoneyTagType.kt @@ -0,0 +1,39 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Cost + +enum class MoneyTagType( + val title: String, + val threshold: Int, + val costParameter: Int +) { + LESS_THAN_30000( + title = Cost.LESS_THAN_30000_TITLE, + threshold = 30000, + costParameter = 3 + ), + LESS_THAN_50000( + title = Cost.LESS_THAN_50000_TITLE, + threshold = 50000, + costParameter = 5 + ), + LESS_THAN_100000( + title = Cost.LESS_THAN_100000_TITLE, + threshold = 100000, + costParameter = 10 + ), + EXCESS_100000( + title = Cost.EXCESS_100000_TITLE, + threshold = Int.MAX_VALUE, + costParameter = 11 + ); + + companion object { + fun Int.toCostTagTitle(): String = when { + this > LESS_THAN_100000.threshold -> EXCESS_100000.title + this > LESS_THAN_50000.threshold -> LESS_THAN_100000.title + this > LESS_THAN_30000.threshold -> LESS_THAN_50000.title + else -> LESS_THAN_30000.title + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/MonthType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/MonthType.kt new file mode 100644 index 000000000..44ce5ebce --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/MonthType.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.data.dataremote.util.Month + +enum class MonthType(val title: String) { + JANUARY(title = Month.JANUARY), + FEBRUARY(title = Month.FEBRUARY), + MARCH(title = Month.MARCH), + APRIL(title = Month.APRIL), + MAY(title = Month.MAY), + JUNE(title = Month.JUNE), + JULY(title = Month.JULY), + AUGUST(title = Month.AUGUST), + SEPTEMBER(title = Month.SEPTEMBER), + OCTOBER(title = Month.OCTOBER), + NOVEMBER(title = Month.NOVEMBER), + DECEMBER(title = Month.DECEMBER); + + companion object { + fun fromNumber(month: Int): String = entries.find { it.ordinal + 1 == month }?.title ?: month.toString() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/RegionType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/RegionType.kt new file mode 100644 index 000000000..17fcb5f1b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/RegionType.kt @@ -0,0 +1,17 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Region + +enum class RegionType( + val title: String +) { + SEOUL( + title = Region.SEOUL + ), + GYEONGGI( + title = Region.GYEONGGI + ), + INCHEON( + title = Region.INCHEON + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/SeoulAreaType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/SeoulAreaType.kt new file mode 100644 index 000000000..c45000de2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/SeoulAreaType.kt @@ -0,0 +1,56 @@ +package org.sopt.dateroad.domain.type + +import org.sopt.dateroad.domain.util.Seoul + +enum class SeoulAreaType( + val title: String +) { + SEOUL_ENTIRE( + title = Seoul.SEOUL_ENTIRE + ), + GANGNAM_SEOCHO( + title = Seoul.GANGNAM_SEOCHO + ), + JAMSIL_SONGPA_GANGDONG( + title = Seoul.JAMSIL_SONGPA_GANGDONG + ), + KONDAE_SEONGSU_SEONGDONG( + title = Seoul.KONDAE_SEONGSU_SEONGDONG + ), + GWANGIN_JUNGBANG( + title = Seoul.GWANGIN_JUNGBANG + ), + JONGNO_JUNGRO( + title = Seoul.JONGNO_JUNGRO + ), + EUNPYEONG_SEODAEMUN( + title = Seoul.EUNPYEONG_SEODAEMUN + ), + HONGDAE_HAPJEONG_MAPO( + title = Seoul.HONGDAE_HAPJEONG_MAPO + ), + YEONGDEUNGPO_YEOUIDO( + title = Seoul.YEONGDEUNGPO_YEOUIDO + ), + YONGSAN_ITAEWON_HANNAM( + title = Seoul.YONGSAN_ITAEWON_HANNAM + ), + YANGCHEON_GANGSEO_GURO( + title = Seoul.YANGCHEON_GANGSEO_GURO + ), + DONGDAEMUN_SEONGBUK( + title = Seoul.DONGDAEMUN_SEONGBUK + ), + NOWON_DOBONG_GANGBUK( + title = Seoul.NOWON_DOBONG_GANGBUK + ), + GWANAK_DONGJAK_GEUMCHEON( + title = Seoul.GWANAK_DONGJAK_GEUMCHEON + ); + + companion object { + fun String.toSeoulAreaTitle(): String = entries.find { it.name == this }?.title ?: "" + fun String.toSeoulAreaType(): SeoulAreaType? = entries.find { it.name == this } + fun String.fromTitleToSeoulAreaType(): SeoulAreaType? = entries.find { it.title == this } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/SortByType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/SortByType.kt new file mode 100644 index 000000000..3915a46fe --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/SortByType.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.type + +enum class SortByType { + POPULAR, + LATEST +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/TimelineTimeType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/TimelineTimeType.kt new file mode 100644 index 000000000..cd901aae7 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/TimelineTimeType.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.type + +enum class TimelineTimeType { + FUTURE, + PAST +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/type/TransactionType.kt b/app/src/main/java/org/sopt/dateroad/domain/type/TransactionType.kt new file mode 100644 index 000000000..e34f7129e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/type/TransactionType.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.domain.type + +enum class TransactionType { + POINT_GAINED, + POINT_USED +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/ClearUserInfoUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/ClearUserInfoUseCase.kt new file mode 100644 index 000000000..df7c3e148 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/ClearUserInfoUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class ClearUserInfoUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke() = userInfoRepository.clear() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseLikeUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseLikeUseCase.kt new file mode 100644 index 000000000..d2e3361d5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseLikeUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.CourseRepository + +@Singleton +class DeleteCourseLikeUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(courseId: Int): Result = courseRepository.deleteCourseLike(courseId = courseId) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseUseCase.kt new file mode 100644 index 000000000..6549d2bf5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteCourseUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.CourseRepository + +@Singleton +class DeleteCourseUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(courseId: Int): Result = courseRepository.deleteCourse(courseId = courseId) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteSignOutUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteSignOutUseCase.kt new file mode 100644 index 000000000..52080e688 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteSignOutUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class DeleteSignOutUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(): Result = authRepository.deleteSignOut() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteTimelineUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteTimelineUseCase.kt new file mode 100644 index 000000000..a45704842 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteTimelineUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.TimelineRepository + +@Singleton +class DeleteTimelineUseCase @Inject constructor( + private val timelineRepository: TimelineRepository +) { + suspend operator fun invoke(timelineId: Int): Result = runCatching { timelineRepository.deleteTimeline(timelineId = timelineId) } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteWithdrawUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteWithdrawUseCase.kt new file mode 100644 index 000000000..2681c713b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/DeleteWithdrawUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class DeleteWithdrawUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(authCode: String?): Result = authRepository.deleteWithdraw(authCode) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAccessTokenUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAccessTokenUseCase.kt new file mode 100644 index 000000000..6b86d00be --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAccessTokenUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class GetAccessTokenUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke() = userInfoRepository.getAccessToken() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementDetailUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementDetailUseCase.kt new file mode 100644 index 000000000..a0037af5b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementDetailUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.AdvertisementDetail +import org.sopt.dateroad.domain.repository.AdvertisementRepository + +@Singleton +class GetAdvertisementDetailUseCase @Inject constructor( + private val advertisementRepository: AdvertisementRepository +) { + suspend operator fun invoke(advertisementId: Int): Result = advertisementRepository.getAdvertisementDetail(advertisementId = advertisementId) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementsUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementsUseCase.kt new file mode 100644 index 000000000..2c90e0732 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetAdvertisementsUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Advertisement +import org.sopt.dateroad.domain.repository.AdvertisementRepository + +@Singleton +class GetAdvertisementsUseCase @Inject constructor( + private val advertisementRepository: AdvertisementRepository +) { + suspend operator fun invoke(): Result> = advertisementRepository.getHomeAdvertisements() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetCourseDetailUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetCourseDetailUseCase.kt new file mode 100644 index 000000000..afafc5015 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetCourseDetailUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.CourseRepository + +@Singleton +class GetCourseDetailUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(courseId: Int) = courseRepository.getCourseDetail(courseId = courseId) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetFilteredCourses.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetFilteredCourses.kt new file mode 100644 index 000000000..520dcd3e0 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetFilteredCourses.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.repository.CourseRepository +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType + +@Singleton +class GetFilteredCourses @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(country: RegionType?, city: Any?, cost: MoneyTagType?): Result> = courseRepository.getFilteredCourses(country = country, city = city, cost = cost) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseEnrollUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseEnrollUseCase.kt new file mode 100644 index 000000000..28b7bc99f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseEnrollUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.repository.MyCourseRepository + +@Singleton +class GetMyCourseEnrollUseCase @Inject constructor( + private val myCourseRepository: MyCourseRepository +) { + suspend operator fun invoke(): Result> = myCourseRepository.getMyCourseEnroll() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseReadUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseReadUseCase.kt new file mode 100644 index 000000000..9d08a4402 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetMyCourseReadUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.repository.MyCourseRepository + +@Singleton +class GetMyCourseReadUseCase @Inject constructor( + private val myCourseRepository: MyCourseRepository +) { + suspend operator fun invoke(): Result> = myCourseRepository.getMyCourseRead() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNearestTimelineUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNearestTimelineUseCase.kt new file mode 100644 index 000000000..1e2a4df28 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNearestTimelineUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.repository.TimelineRepository + +class GetNearestTimelineUseCase @Inject constructor( + private val timelineRepository: TimelineRepository +) { + suspend operator fun invoke(): Result = timelineRepository.getNearestTimeline() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameCheckUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameCheckUseCase.kt new file mode 100644 index 000000000..b83c58449 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameCheckUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class GetNicknameCheckUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(name: String): Result = authRepository.getNicknameCheck(name) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameUseCase.kt new file mode 100644 index 000000000..57809d2f7 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetNicknameUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class GetNicknameUseCase @Inject constructor( + private val authInfoRepository: UserInfoRepository +) { + operator fun invoke(): String = authInfoRepository.getNickname() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetPointHistoryUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetPointHistoryUseCase.kt new file mode 100644 index 000000000..509d13bc6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetPointHistoryUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.PointHistory +import org.sopt.dateroad.domain.repository.UserPointRepository + +@Singleton +class GetPointHistoryUseCase @Inject constructor( + private val userPointRepository: UserPointRepository +) { + suspend operator fun invoke(): Result = userPointRepository.getPointHistory() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetRefreshTokenUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetRefreshTokenUseCase.kt new file mode 100644 index 000000000..3a97408de --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetRefreshTokenUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class GetRefreshTokenUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke() = userInfoRepository.getRefreshToken() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetSortedCoursesUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetSortedCoursesUseCase.kt new file mode 100644 index 000000000..6df3b1413 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetSortedCoursesUseCase.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.repository.CourseRepository +import org.sopt.dateroad.domain.type.SortByType + +@Singleton +class GetSortedCoursesUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(sortedBy: SortByType): Result> = courseRepository.getSortedCourses(sortedBy = sortedBy) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelineDetailUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelineDetailUseCase.kt new file mode 100644 index 000000000..24f8cf8bd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelineDetailUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.domain.repository.TimelineRepository + +class GetTimelineDetailUseCase @Inject constructor( + private val timelineRepository: TimelineRepository +) { + suspend operator fun invoke(timelineId: Int): Result = timelineRepository.getTimelineDetail(timelineId = timelineId) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelinesUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelinesUseCase.kt new file mode 100644 index 000000000..7e4796225 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetTimelinesUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.domain.repository.TimelineRepository +import org.sopt.dateroad.domain.type.TimelineTimeType + +class GetTimelinesUseCase @Inject constructor( + private val timelineRepository: TimelineRepository +) { + suspend operator fun invoke(timelineTimeType: TimelineTimeType): Result> = timelineRepository.getTimelines(timelineTimeType = timelineTimeType) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserPointUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserPointUseCase.kt new file mode 100644 index 000000000..36124f117 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserPointUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.domain.repository.UserPointRepository + +class GetUserPointUseCase @Inject constructor( + private val userPointRepository: UserPointRepository +) { + suspend operator fun invoke(): Result = userPointRepository.getUserPoint() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserUseCase.kt new file mode 100644 index 000000000..87df2c3c9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/GetUserUseCase.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Profile +import org.sopt.dateroad.domain.repository.ProfileRepository + +@Singleton +class GetUserUseCase @Inject constructor( + private val profileRepository: ProfileRepository +) { + suspend operator fun invoke(): Result = + profileRepository.getUsers() +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PatchEditProfileUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PatchEditProfileUseCase.kt new file mode 100644 index 000000000..9f565bc02 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PatchEditProfileUseCase.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class PatchEditProfileUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(editProfile: EditProfile): Result = + authRepository.patchEditProfile(editProfile = editProfile) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseLikeUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseLikeUseCase.kt new file mode 100644 index 000000000..5f3d1d46a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseLikeUseCase.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.CourseRepository + +@Singleton +class PostCourseLikeUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(courseId: Int): Result = runCatching { + courseRepository.postCourseLike(courseId = courseId) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseUseCase.kt new file mode 100644 index 000000000..fa608a696 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostCourseUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.repository.CourseRepository + +@Singleton +class PostCourseUseCase @Inject constructor( + private val courseRepository: CourseRepository +) { + suspend operator fun invoke(enroll: Enroll): Result = courseRepository.postCourse(enroll = enroll) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignInUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignInUseCase.kt new file mode 100644 index 000000000..7db32724c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignInUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Auth +import org.sopt.dateroad.domain.model.SignIn +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class PostSignInUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(authorization: String, signIn: SignIn): Result = + authRepository.postSignIn(authorization = authorization, signIn = signIn) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignUpUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignUpUseCase.kt new file mode 100644 index 000000000..f5ba86e99 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostSignUpUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Auth +import org.sopt.dateroad.domain.model.SignUp +import org.sopt.dateroad.domain.repository.AuthRepository + +@Singleton +class PostSignUpUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(signUp: SignUp): Result = + authRepository.postSignUp(signUp = signUp) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostTimelineUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostTimelineUseCase.kt new file mode 100644 index 000000000..1d13755aa --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostTimelineUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.repository.TimelineRepository + +@Singleton +class PostTimelineUseCase @Inject constructor( + private val timelineRepository: TimelineRepository +) { + suspend operator fun invoke(enroll: Enroll): Result = timelineRepository.postTimeline(enroll = enroll) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/PostUsePointUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostUsePointUseCase.kt new file mode 100644 index 000000000..da2f2945c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/PostUsePointUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.model.UsePoint +import org.sopt.dateroad.domain.repository.UserPointRepository + +@Singleton +class PostUsePointUseCase @Inject constructor( + private val userPointRepository: UserPointRepository +) { + suspend operator fun invoke(courseId: Int, usePoint: UsePoint): Result = runCatching { + userPointRepository.postUsePoint(courseId = courseId, usePoint = usePoint) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/SetAccessTokenUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetAccessTokenUseCase.kt new file mode 100644 index 000000000..51ef3b2f4 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetAccessTokenUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class SetAccessTokenUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke(accessToken: String) = userInfoRepository.setAccessToken(accessToken = accessToken) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/SetNicknameUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetNicknameUseCase.kt new file mode 100644 index 000000000..7bf17e500 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetNicknameUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class SetNicknameUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke(nickname: String) = userInfoRepository.setNickname(nickname = nickname) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/usecase/SetRefreshTokenUseCase.kt b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetRefreshTokenUseCase.kt new file mode 100644 index 000000000..7fd376a0f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/usecase/SetRefreshTokenUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.dateroad.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton +import org.sopt.dateroad.domain.repository.UserInfoRepository + +@Singleton +class SetRefreshTokenUseCase @Inject constructor( + private val userInfoRepository: UserInfoRepository +) { + operator fun invoke(refreshToken: String) = userInfoRepository.setRefreshToken(refreshToken = refreshToken) +} diff --git a/app/src/main/java/org/sopt/dateroad/domain/util/Constraints.kt b/app/src/main/java/org/sopt/dateroad/domain/util/Constraints.kt new file mode 100644 index 000000000..c346cf10d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/domain/util/Constraints.kt @@ -0,0 +1,77 @@ +package org.sopt.dateroad.domain.util + +object Cost { + const val EXCESS_100000_TITLE = "10만원 초과" + const val LESS_THAN_100000_TITLE = "10만원 이하" + const val LESS_THAN_50000_TITLE = "5만원 이하" + const val LESS_THAN_30000_TITLE = "3만원 이하" +} + +object Region { + const val SEOUL = "서울" + const val GYEONGGI = "경기" + const val INCHEON = "인천" +} + +object Seoul { + const val SEOUL_ENTIRE = "서울 전체" + const val GANGNAM_SEOCHO = "강남/서초" + const val JAMSIL_SONGPA_GANGDONG = "잠실/송파/강동" + const val KONDAE_SEONGSU_SEONGDONG = "건대/성수/성동" + const val GWANGIN_JUNGBANG = "광진/중랑" + const val JONGNO_JUNGRO = "종로/중구" + const val EUNPYEONG_SEODAEMUN = "은평/서대문" + const val HONGDAE_HAPJEONG_MAPO = "홍대/합정/마포" + const val YEONGDEUNGPO_YEOUIDO = "영등포/여의도" + const val YONGSAN_ITAEWON_HANNAM = "용산/이태원/한남" + const val YANGCHEON_GANGSEO_GURO = "양천/강서/구로" + const val DONGDAEMUN_SEONGBUK = "동대문/성북" + const val NOWON_DOBONG_GANGBUK = "노원/도봉/강북" + const val GWANAK_DONGJAK_GEUMCHEON = "관악/동작/금천" +} + +object Gyeonggi { + const val GYEONGGI_ENTIRE = "경기 전체" + const val SEONGNAM = "성남" + const val SUWON = "수원" + const val GOYANG_PAJU = "고양/파주" + const val GIMPO = "김포" + const val YONGIN_HWASEONG = "용인/화성" + const val ANYANG_GWACHEON = "안양/과천" + const val POCHEON_YANGJU = "포천/양주" + const val NAMYANGJU_UIJEONGBU = "남양주/의정부" + const val GWANGJU_ICHEON_YEOJU = "광주/이천/여주" + const val GAPYEONG_YANGPYEONG = "가평/양평" + const val GUNPO_UIWANG = "군포/의왕" + const val HANAM_GURI = "하남/구리" + const val SIHEUNG_GWANGMYEONG = "시흥/광명" + const val BUCHEON_ANSHAN = "부천/안산" + const val DONGDUCHEON_YEONCHEON = "동두천/연천" + const val PYEONGTAEK_OSAN_ANSEONG = "평택/오산/안성" +} + +object Incheon { + const val INCHEON_ENTIRE = "인천 전체" +} + +object Advertisement { + const val EDITOR = "에디터 픽" + const val AD = "광고" + const val ABOUT = "ABOUT" + const val HOT = "이달의 HOT" +} + +object Month { + const val JANUARY = "January" + const val FEBRUARY = "February" + const val MARCH = "March" + const val APRIL = "April" + const val MAY = "May" + const val JUNE = "June" + const val JULY = "July" + const val AUGUST = "August" + const val SEPTEMBER = "September" + const val OCTOBER = "October" + const val NOVEMBER = "November" + const val DECEMBER = "December" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/model/RouteModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/model/RouteModel.kt new file mode 100644 index 000000000..ae3bf7df3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/model/RouteModel.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.presentation.model + +sealed interface Route + +sealed interface MainNavigationBarRoute : Route { + data object Dummy : MainNavigationBarRoute + data object Home : MainNavigationBarRoute + data object Look : MainNavigationBarRoute + data object Timeline : MainNavigationBarRoute + data object Read : MainNavigationBarRoute + data object MyPage : MainNavigationBarRoute +// TODO: data object Search : MainNavigationBarRoute +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/ChipType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/ChipType.kt new file mode 100644 index 000000000..eae9dd39d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/ChipType.kt @@ -0,0 +1,64 @@ +package org.sopt.dateroad.presentation.type + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.Black +import org.sopt.dateroad.ui.theme.Gray100 +import org.sopt.dateroad.ui.theme.Gray400 +import org.sopt.dateroad.ui.theme.Purple600 +import org.sopt.dateroad.ui.theme.White +import org.sopt.dateroad.ui.theme.defaultDateRoadTypography + +enum class ChipType( + val selectedBackgroundColor: Color, + val unselectedBackgroundColor: Color, + val selectedTextColor: Color, + val unselectedTextColor: Color, + val horizontalPadding: Dp, + val verticalPadding: Dp, + val cornerRadius: Dp, + val textStyle: TextStyle +) { + DATE( + selectedBackgroundColor = Purple600, + unselectedBackgroundColor = Gray100, + selectedTextColor = White, + unselectedTextColor = Black, + horizontalPadding = 10.dp, + verticalPadding = 5.dp, + cornerRadius = 20.dp, + textStyle = defaultDateRoadTypography.bodySemi13 + ), + MONEY( + selectedBackgroundColor = Purple600, + unselectedBackgroundColor = Gray100, + selectedTextColor = White, + unselectedTextColor = Gray400, + horizontalPadding = 8.dp, + verticalPadding = 6.dp, + cornerRadius = 20.dp, + textStyle = defaultDateRoadTypography.bodyMed13 + ), + AREA( + selectedBackgroundColor = Purple600, + unselectedBackgroundColor = Gray100, + selectedTextColor = White, + unselectedTextColor = Gray400, + horizontalPadding = 14.dp, + verticalPadding = 6.dp, + cornerRadius = 10.dp, + textStyle = defaultDateRoadTypography.bodyMed13 + ), + REGION( + selectedBackgroundColor = Purple600, + unselectedBackgroundColor = Gray100, + selectedTextColor = White, + unselectedTextColor = Gray400, + horizontalPadding = 11.dp, + verticalPadding = 6.dp, + cornerRadius = 10.dp, + textStyle = defaultDateRoadTypography.bodySemi15 + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/CourseDetailUnopenedDetailType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/CourseDetailUnopenedDetailType.kt new file mode 100644 index 000000000..d854a9404 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/CourseDetailUnopenedDetailType.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class CourseDetailUnopenedDetailType( + @StringRes val descriptionStringRes: Int, + @StringRes val buttonTextStringRes: Int +) { + POINT( + descriptionStringRes = R.string.course_detail_point_read_button_description, + buttonTextStringRes = R.string.course_detail_point_read_button + ), + FREE( + descriptionStringRes = R.string.course_detail_free_read_button_description, + buttonTextStringRes = R.string.course_detail_free_read_button + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/DateChipGroupType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/DateChipGroupType.kt new file mode 100644 index 000000000..d9465328f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/DateChipGroupType.kt @@ -0,0 +1,23 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import androidx.compose.ui.text.TextStyle +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.defaultDateRoadTypography + +enum class DateChipGroupType( + @StringRes val titleRes: Int, + val titleTextStyle: TextStyle, + val maxSize: Int +) { + PROFILE( + titleRes = R.string.date_chip_group_profile, + titleTextStyle = defaultDateRoadTypography.bodyBold15, + maxSize = 3 + ), + ENROLL( + titleRes = R.string.date_chip_group_enroll_course, + titleTextStyle = defaultDateRoadTypography.bodySemi15, + maxSize = 3 + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/DateRoadRegionBottomSheetType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/DateRoadRegionBottomSheetType.kt new file mode 100644 index 000000000..111163c8f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/DateRoadRegionBottomSheetType.kt @@ -0,0 +1,6 @@ +package org.sopt.dateroad.presentation.type + +enum class DateRoadRegionBottomSheetType { + ENROLL, + LOOK +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/DateTagType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/DateTagType.kt new file mode 100644 index 000000000..8b4df2dad --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/DateTagType.kt @@ -0,0 +1,59 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class DateTagType( + @StringRes val titleRes: Int, + @DrawableRes val imageRes: Int +) { + DRIVE( + titleRes = R.string.date_tag_drive, + imageRes = R.drawable.ic_all_drive + ), + SHOPPING( + titleRes = R.string.date_tag_shopping, + imageRes = R.drawable.ic_all_shopping + ), + INDOORS( + titleRes = R.string.date_tag_indoor, + imageRes = R.drawable.ic_all_indoor + ), + HEALING( + titleRes = R.string.date_tag_healing, + imageRes = R.drawable.ic_all_healing + ), + ALCOHOL( + titleRes = R.string.date_tag_alcohol, + imageRes = R.drawable.ic_all_alcohol + ), + FOOD( + titleRes = R.string.date_tag_epicurism, + imageRes = R.drawable.ic_all_epicurism + ), + WORKSHOP( + titleRes = R.string.date_tag_craft_shop, + imageRes = R.drawable.ic_all_craft_shop + ), + NATURE( + titleRes = R.string.date_tag_nature, + imageRes = R.drawable.ic_all_nature + ), + ACTIVITY( + titleRes = R.string.date_tag_activity, + imageRes = R.drawable.ic_all_activity + ), + PERFORMANCE_MUSIC( + titleRes = R.string.date_tag_performance_music, + imageRes = R.drawable.ic_all_performance_music + ), + EXHIBITION_POPUP( + titleRes = R.string.date_tag_exhibition_pop_up, + imageRes = R.drawable.ic_all_exhibition_pop_up + ); + + companion object { + fun String.getDateTagTypeByName(): DateTagType? = entries.find { it.name == this } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/EmptyViewType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/EmptyViewType.kt new file mode 100644 index 000000000..1e9be1de7 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/EmptyViewType.kt @@ -0,0 +1,43 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class EmptyViewType( + @StringRes val titleRes: Int, + @DrawableRes val imageRes: Int +) { + POINT_HISTORY_GAINED_HISTORY( + titleRes = R.string.empty_view_point_history_earn_history, + imageRes = R.drawable.img_empty_point_history_gained_history + ), + POINT_HISTORY_USED_HISTORY( + titleRes = R.string.empty_view_point_history_usage_history, + imageRes = R.drawable.img_empty_point_history_used_history + ), + READ( + titleRes = R.string.empty_view_read, + imageRes = R.drawable.img_empty_read + ), + LOOK( + titleRes = R.string.empty_view_look, + imageRes = R.drawable.img_empty_look + ), + TIMELINE( + titleRes = R.string.empty_view_timeline, + imageRes = R.drawable.img_empty_running + ), + PAST( + titleRes = R.string.empty_view_past, + imageRes = R.drawable.img_empty_envelope + ), + MY_COURSE_READ( + titleRes = R.string.empty_view_my_course_read, + imageRes = R.drawable.img_empty_envelope + ), + MY_COURSE_ENROLL( + titleRes = R.string.empty_view_my_course_enroll, + imageRes = R.drawable.img_empty_running + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollScreenType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollScreenType.kt new file mode 100644 index 000000000..e9e63bc7e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollScreenType.kt @@ -0,0 +1,17 @@ +package org.sopt.dateroad.presentation.type + +import org.sopt.dateroad.presentation.util.EnrollScreen + +enum class EnrollScreenType( + val position: Int +) { + FIRST( + position = EnrollScreen.FIRST + ), + SECOND( + position = EnrollScreen.SECOND + ), + THIRD( + position = EnrollScreen.THIRD + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollType.kt new file mode 100644 index 000000000..c4402e875 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/EnrollType.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class EnrollType( + @StringRes val topBarTitleRes: Int +) { + COURSE( + topBarTitleRes = R.string.top_bar_title_enroll_course + ), + TIMELINE( + topBarTitleRes = R.string.top_bar_title_enroll_timeline + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/MainNavigationBarItemType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/MainNavigationBarItemType.kt new file mode 100644 index 000000000..185b15872 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/MainNavigationBarItemType.kt @@ -0,0 +1,58 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.model.Route + +enum class MainNavigationBarItemType( + @DrawableRes val iconRes: Int, + @StringRes val label: Int, + val route: MainNavigationBarRoute +) { + HOME( + iconRes = R.drawable.ic_nav_home_selected, + label = R.string.main_navigation_bar_item_home, + route = MainNavigationBarRoute.Home + ), + LOOK( + iconRes = R.drawable.ic_nav_look_selected, + label = R.string.main_navigation_bar_item_look, + route = MainNavigationBarRoute.Look + ), + TIMELINE( + iconRes = R.drawable.ic_nav_timeline_selected, + label = R.string.main_navigation_bar_item_timeline, + route = MainNavigationBarRoute.Timeline + ), + READ( + iconRes = R.drawable.ic_nav_read_selected, + label = R.string.main_navigation_bar_item_read, + route = MainNavigationBarRoute.Read + ), + MY_PAGE( + iconRes = R.drawable.ic_nav_my_page_selected, + label = R.string.main_navigation_bar_item_my_page, + route = MainNavigationBarRoute.MyPage + ) +// TODO: SEARCH( +// iconRes = R.drawable.ic_nav_search_selected, +// label = R.string.main_navigation_bar_item_search, +// route = MainNavigationBarRoute.Search +// ) + ; + + companion object { + @Composable + fun find(predicate: @Composable (MainNavigationBarRoute) -> Boolean): MainNavigationBarItemType? { + return entries.find { predicate(it.route) } + } + + @Composable + fun contains(predicate: @Composable (Route) -> Boolean): Boolean { + return entries.map { it.route }.any { predicate(it) } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/MyCourseType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/MyCourseType.kt new file mode 100644 index 000000000..5008d21a0 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/MyCourseType.kt @@ -0,0 +1,15 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class MyCourseType( + @StringRes val topBarTitleRes: Int +) { + READ( + topBarTitleRes = R.string.top_bar_title_my_course_read + ), + ENROLL( + topBarTitleRes = R.string.top_bar_title_my_course_enroll + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/MyPageMenuType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/MyPageMenuType.kt new file mode 100644 index 000000000..744dd66fd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/MyPageMenuType.kt @@ -0,0 +1,23 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class MyPageMenuType( + @StringRes val titleRes: Int, + @DrawableRes val iconRes: Int = R.drawable.ic_my_page_arrow +) { + MY_COURSE_ENROLL( + titleRes = R.string.my_page_menu_my_enroll_course + ), + POINT_SYSTEM( + titleRes = R.string.my_page_menu_point_guide + ), + QUESTION( + titleRes = R.string.my_page_menu_question + ), + LOGOUT( + titleRes = R.string.my_page_menu_logout + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/MyPagePointInfoType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/MyPagePointInfoType.kt new file mode 100644 index 000000000..73ce9226b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/MyPagePointInfoType.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class MyPagePointInfoType( + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, + @DrawableRes val imageRes: Int = R.drawable.img_my_page_point_info_first + +) { + FIRST( + titleRes = R.string.point_system_first_title, + descriptionRes = R.string.point_system_first_description + ), + SECOND( + titleRes = R.string.point_system_second_title, + descriptionRes = R.string.point_system_second_description, + imageRes = R.drawable.img_my_page_point_info_second + ), + THIRD( + titleRes = R.string.point_system_third_title, + descriptionRes = R.string.point_system_third_description, + imageRes = R.drawable.img_my_page_point_info_third + ), + FOURTH( + titleRes = R.string.point_system_fourth_title, + descriptionRes = R.string.point_system_fourth_description, + imageRes = R.drawable.img_my_page_point_info_fourth + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/OnboardingType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/OnboardingType.kt new file mode 100644 index 000000000..aedc1cb3f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/OnboardingType.kt @@ -0,0 +1,32 @@ + +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class OnboardingType( + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, + @DrawableRes val imageRes: Int, + @StringRes val subDescriptionRes: Int +) { + FIRST( + titleRes = R.string.onboarding_first_title, + descriptionRes = R.string.onboarding_first_description, + imageRes = R.drawable.img_onboarding_background1, + subDescriptionRes = R.string.onboarding_first_sub_description + ), + SECOND( + titleRes = R.string.onboarding_second_title, + descriptionRes = R.string.onboarding_second_description, + imageRes = R.drawable.img_onboarding_background2, + subDescriptionRes = R.string.onboarding_second_sub_description + ), + THIRD( + titleRes = R.string.onboarding_third_title, + descriptionRes = R.string.onboarding_third_description, + imageRes = R.drawable.img_onboarding_background3, + subDescriptionRes = R.string.onboarding_third_sub_description + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogType.kt new file mode 100644 index 000000000..208a77263 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogType.kt @@ -0,0 +1,14 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class OneButtonDialogType( + @StringRes val titleRes: Int, + @StringRes val buttonTextRes: Int +) { + ENROLL_TIMELINE( + titleRes = R.string.one_button_dialog_with_description_enroll_timeline_title, + buttonTextRes = R.string.dialog_checked + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogWithDescriptionType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogWithDescriptionType.kt new file mode 100644 index 000000000..212578e54 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/OneButtonDialogWithDescriptionType.kt @@ -0,0 +1,21 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class OneButtonDialogWithDescriptionType( + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, + @StringRes val buttonTextRes: Int +) { + ENROLL_COURSE( + titleRes = R.string.one_button_dialog_with_description_enroll_course_title, + descriptionRes = R.string.one_button_dialog_with_description_enroll_course_description, + buttonTextRes = R.string.dialog_checked + ), + CANNOT_ENROLL_COURSE( + titleRes = R.string.one_button_dialog_with_description_cannot_enroll_course_title, + descriptionRes = R.string.one_button_dialog_with_description_cannot_enroll_course_description, + buttonTextRes = R.string.dialog_checked + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/PlaceCardType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/PlaceCardType.kt new file mode 100644 index 000000000..db8d9ee7a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/PlaceCardType.kt @@ -0,0 +1,34 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R + +enum class PlaceCardType( + @DrawableRes val iconRes: Int? = null, + val startPadding: Dp = 17.dp, + val endPadding: Dp = 4.dp, + val verticalPadding: Dp = 5.dp, + val iconStartPadding: Dp = 16.dp, + val iconTopPadding: Dp = 19.dp, + val iconEndPadding: Dp = 16.dp, + val iconBottomPadding: Dp = 20.dp + +) { + COURSE_NORMAL( + startPadding = 14.dp, + endPadding = 13.dp, + verticalPadding = 13.dp + ), + COURSE_EDIT( + iconRes = R.drawable.ic_date_schedule_move_course + ), + COURSE_DELETE( + iconRes = R.drawable.ic_date_schedule_delete_course, + iconStartPadding = 18.dp, + iconTopPadding = 18.dp, + iconEndPadding = 17.dp, + iconBottomPadding = 17.dp + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/PointHistoryTabType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/PointHistoryTabType.kt new file mode 100644 index 000000000..e96d753ba --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/PointHistoryTabType.kt @@ -0,0 +1,20 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.util.PointHistoryTab.GAINED_HISTORY_POSITION +import org.sopt.dateroad.presentation.util.PointHistoryTab.USED_HISTORY_POSITION + +enum class PointHistoryTabType( + val position: Int, + @StringRes val titleRes: Int +) { + GAINED_HISTORY( + position = GAINED_HISTORY_POSITION, + titleRes = R.string.point_history_tab_gained_history + ), + USED_HISTORY( + position = USED_HISTORY_POSITION, + titleRes = R.string.point_history_tab_used_history + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/PointSystemType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/PointSystemType.kt new file mode 100644 index 000000000..3957832d3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/PointSystemType.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class PointSystemType( + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, + @DrawableRes val imageRes: Int +) { + FIRST( + titleRes = R.string.point_system_first_title, + descriptionRes = R.string.point_system_first_description, + imageRes = R.drawable.ic_launcher_foreground + ), + SECOND( + titleRes = R.string.point_system_second_title, + descriptionRes = R.string.point_system_second_description, + imageRes = R.drawable.ic_launcher_foreground + ), + THIRD( + titleRes = R.string.point_system_third_title, + descriptionRes = R.string.point_system_third_description, + imageRes = R.drawable.ic_launcher_foreground + ), + FOURTH( + titleRes = R.string.point_system_fourth_title, + descriptionRes = R.string.point_system_fourth_description, + imageRes = R.drawable.ic_launcher_foreground + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/ProfileType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/ProfileType.kt new file mode 100644 index 000000000..bd11b143f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/ProfileType.kt @@ -0,0 +1,18 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class ProfileType( + @StringRes val topAppBarTitleRes: Int, + @StringRes val buttonTextRes: Int +) { + ENROLL( + topAppBarTitleRes = R.string.profile_enroll_top_bar_title, + buttonTextRes = R.string.enroll_profile_button + ), + EDIT( + topAppBarTitleRes = R.string.profile_edit_top_bar_title, + buttonTextRes = R.string.edit_profile_button + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/TagType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/TagType.kt new file mode 100644 index 000000000..15a74ea6e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/TagType.kt @@ -0,0 +1,136 @@ +package org.sopt.dateroad.presentation.type + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import org.sopt.dateroad.ui.theme.defaultDateRoadColors +import org.sopt.dateroad.ui.theme.defaultDateRoadTypography + +enum class TagType( + val backgroundColor: Color, + val contentColor: Color, + val paddingHorizontal: Int, + val paddingVertical: Int, + val textStyle: TextStyle, + val roundedCornerShape: Int +) { + ADVERTISEMENT_TITLE( + backgroundColor = defaultDateRoadColors.purple500, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 10, + paddingVertical = 2, + textStyle = defaultDateRoadTypography.bodySemi13, + roundedCornerShape = 12 + ), + COURSE_DETAIL_PHOTO_NUMBER( + backgroundColor = defaultDateRoadColors.gray400, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 14, + paddingVertical = 2, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 12 + ), + TIMELINE_D_DAY( + backgroundColor = defaultDateRoadColors.purple600, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 10, + paddingVertical = 2, + textStyle = defaultDateRoadTypography.capBold11, + roundedCornerShape = 20 + ), + ENROLL_PHOTO_NUMBER( + backgroundColor = defaultDateRoadColors.gray400, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.capReg11, + roundedCornerShape = 20 + ), + HEART( + backgroundColor = defaultDateRoadColors.purple600, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 10, + paddingVertical = 2, + textStyle = defaultDateRoadTypography.bodyBold13, + roundedCornerShape = 12 + ), + TIME( + backgroundColor = defaultDateRoadColors.gray100, + contentColor = defaultDateRoadColors.gray400, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + MONEY( + backgroundColor = defaultDateRoadColors.gray100, + contentColor = defaultDateRoadColors.gray400, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + MY_PAGE_DATE( + backgroundColor = defaultDateRoadColors.white, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 14, + paddingVertical = 6, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + ADVERTISEMENT_PAGE_NUMBER( + backgroundColor = defaultDateRoadColors.gray400, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 9, + paddingVertical = 1, + textStyle = defaultDateRoadTypography.capReg11, + roundedCornerShape = 20 + ), + PAST_DATE( + backgroundColor = defaultDateRoadColors.pink100, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 14, + paddingVertical = 6, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + PLACE_CARD_NUMBER( + backgroundColor = defaultDateRoadColors.purple600, + contentColor = defaultDateRoadColors.white, + paddingHorizontal = 9, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyBold13, + roundedCornerShape = 50 + ), + PLACE_CARD_TIME( + backgroundColor = defaultDateRoadColors.gray200, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 14, + paddingVertical = 5, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 10 + ), + TIMELINE_DATE_LIME( + backgroundColor = defaultDateRoadColors.lime100, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + TIMELINE_DATE_PINK( + backgroundColor = defaultDateRoadColors.pink100, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ), + TIMELINE_DATE_PURPLE( + backgroundColor = defaultDateRoadColors.purple100, + contentColor = defaultDateRoadColors.black, + paddingHorizontal = 10, + paddingVertical = 4, + textStyle = defaultDateRoadTypography.bodyMed13, + roundedCornerShape = 20 + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/TimelineType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/TimelineType.kt new file mode 100644 index 000000000..3ba2fd80a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/TimelineType.kt @@ -0,0 +1,35 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.ColorRes +import androidx.compose.ui.graphics.Color +import org.sopt.dateroad.ui.theme.defaultDateRoadColors + +enum class TimelineType( + val index: Int, + @ColorRes val backgroundColor: Color, + @ColorRes val lineColor: Color, + val tagType: TagType +) { + PINK( + index = 0, + backgroundColor = defaultDateRoadColors.pink200, + lineColor = defaultDateRoadColors.pink300, + tagType = TagType.TIMELINE_DATE_PINK + ), + PURPLE( + index = 1, + backgroundColor = defaultDateRoadColors.purple200, + lineColor = defaultDateRoadColors.purple300, + tagType = TagType.TIMELINE_DATE_PURPLE + ), + LIME( + index = 2, + backgroundColor = defaultDateRoadColors.lime200, + lineColor = defaultDateRoadColors.lime300, + tagType = TagType.TIMELINE_DATE_LIME + ); + + companion object { + fun getTimelineTypeByIndex(index: Int): TimelineType = entries.first { it.index == index % entries.size } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogType.kt new file mode 100644 index 000000000..b91457d5f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogType.kt @@ -0,0 +1,21 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class TwoButtonDialogType( + @StringRes val titleRes: Int, + @StringRes val confirmButtonTextRes: Int, + @StringRes val dismissButtonTextRes: Int +) { + OPEN_KAKAOTALK( + titleRes = R.string.two_button_dialog_open_kakaotalk_title, + confirmButtonTextRes = R.string.two_button_dialog_open_kakaotalk_confirm_button_text, + dismissButtonTextRes = R.string.dialog_cancel + ), + LOGOUT( + titleRes = R.string.two_button_dialog_logout_title, + confirmButtonTextRes = R.string.logout, + dismissButtonTextRes = R.string.dialog_cancel + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogWithDescriptionType.kt b/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogWithDescriptionType.kt new file mode 100644 index 000000000..0c135f678 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/type/TwoButtonDialogWithDescriptionType.kt @@ -0,0 +1,60 @@ +package org.sopt.dateroad.presentation.type + +import androidx.annotation.StringRes +import org.sopt.dateroad.R + +enum class TwoButtonDialogWithDescriptionType( + @StringRes val titleRes: Int, + @StringRes val descriptionRes: Int, + @StringRes val confirmButtonTextRes: Int, + @StringRes val dismissButtonTextRes: Int +) { + READ_COURSE( + titleRes = R.string.two_button_dialog_with_description_read_course_title, + descriptionRes = R.string.two_button_dialog_with_description_read_course_description, + confirmButtonTextRes = R.string.dialog_check, + dismissButtonTextRes = R.string.dialog_cancel + ), + POINT_LACK( + titleRes = R.string.two_button_dialog_with_description_point_lack_title, + descriptionRes = R.string.two_button_dialog_with_description_point_lack_description, + confirmButtonTextRes = R.string.two_button_dialog_with_description_point_lack_confirm_button_text, + dismissButtonTextRes = R.string.dialog_cancel + ), + FREE_READ( + titleRes = R.string.two_button_dialog_with_description_free_read_title, + descriptionRes = R.string.two_button_dialog_with_description_free_read_description, + confirmButtonTextRes = R.string.dialog_check, + dismissButtonTextRes = R.string.dialog_cancel + ), + DELETE_TIMELINE( + titleRes = R.string.two_button_dialog_with_description_delete_timeline_title, + descriptionRes = R.string.dialog_delete_schedule, + confirmButtonTextRes = R.string.dialog_delete, + dismissButtonTextRes = R.string.dialog_cancel + ), + DELETE_COURSE( + titleRes = R.string.two_button_dialog_with_description_delete_course_title, + descriptionRes = R.string.two_button_dialog_with_description_delete_course_description, + confirmButtonTextRes = R.string.dialog_delete, + dismissButtonTextRes = R.string.dialog_cancel + ), + DELETE_PAST( + titleRes = R.string.two_button_dialog_with_description_delete_past_title, + descriptionRes = R.string.dialog_delete_schedule, + confirmButtonTextRes = R.string.dialog_delete, + dismissButtonTextRes = R.string.dialog_cancel + ), + WITHDRAWAL( + titleRes = R.string.two_button_dialog_with_description_withdrawal_title, + descriptionRes = R.string.two_button_dialog_with_description_withdrawal_description, + confirmButtonTextRes = R.string.dialog_cancel, + dismissButtonTextRes = R.string.withdrawal + ), + REPORT_COURSE( + titleRes = R.string.two_button_dialog_with_description_report_course_title, + descriptionRes = R.string.two_button_dialog_with_description_report_course_description, + confirmButtonTextRes = R.string.dialog_report, + dismissButtonTextRes = R.string.dialog_cancel + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementContract.kt new file mode 100644 index 000000000..76196a92b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementContract.kt @@ -0,0 +1,22 @@ +package org.sopt.dateroad.presentation.ui.advertisement + +import org.sopt.dateroad.domain.model.AdvertisementDetail +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class AdvertisementContract { + data class AdvertisementUiState( + val loadState: LoadState = LoadState.Idle, + val advertisementDetail: AdvertisementDetail = AdvertisementDetail() + ) : UiState + + sealed interface AdvertisementSideEffect : UiSideEffect { + data object PopBackStack : AdvertisementSideEffect + } + + sealed class AdvertisementEvent : UiEvent { + data class FetchAdvertisementDetail(val loadState: LoadState, val advertisementDetail: AdvertisementDetail) : AdvertisementEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementScreen.kt new file mode 100644 index 000000000..acea7706c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementScreen.kt @@ -0,0 +1,120 @@ +package org.sopt.dateroad.presentation.ui.advertisement + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.PagerState +import org.sopt.dateroad.presentation.ui.advertisement.component.AdvertisementDetail +import org.sopt.dateroad.presentation.ui.component.pager.DateRoadImagePager +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadScrollResponsiveTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun AdvertisementRoute( + viewmodel: AdvertisementViewModel = hiltViewModel(), + popBackStack: () -> Unit, + advertisementId: Int +) { + val uiState by viewmodel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(viewmodel.sideEffect, lifecycleOwner) { + viewmodel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { advertisementSideEffect -> + when (advertisementSideEffect) { + is AdvertisementContract.AdvertisementSideEffect.PopBackStack -> popBackStack() + } + } + } + + LaunchedEffect(Unit) { + viewmodel.fetchAdvertisementDetail(advertisementId = advertisementId) + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> AdvertisementScreen( + advertisementUiState = uiState, + onTopBarIconClicked = { viewmodel.setSideEffect(AdvertisementContract.AdvertisementSideEffect.PopBackStack) } + ) + + LoadState.Error -> DateRoadErrorView() + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun AdvertisementScreen( + advertisementUiState: AdvertisementContract.AdvertisementUiState, + onTopBarIconClicked: () -> Unit +) { + var imageHeight by remember { mutableIntStateOf(0) } + + val scrollState = rememberLazyListState() + val isScrollResponsiveDefault by remember { + derivedStateOf { + scrollState.firstVisibleItemIndex == 0 && scrollState.firstVisibleItemScrollOffset < imageHeight + } + } + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = scrollState, + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + with(advertisementUiState.advertisementDetail) { + item { + DateRoadImagePager( + modifier = Modifier + .onGloballyPositioned { coordinates -> + imageHeight = coordinates.size.height + }, + pagerState = PagerState(), + images = images, + userScrollEnabled = true, + like = null + ) + } + + item { + AdvertisementDetail( + advertisementTagTitle = advertisementTagTitle, + createAt = createAt, + title = title, + description = description + ) + } + } + } + + DateRoadScrollResponsiveTopBar( + isDefault = isScrollResponsiveDefault, + onLeftIconClick = onTopBarIconClicked + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementViewModel.kt new file mode 100644 index 000000000..6a09f4d93 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/AdvertisementViewModel.kt @@ -0,0 +1,33 @@ +package org.sopt.dateroad.presentation.ui.advertisement + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.usecase.GetAdvertisementDetailUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class AdvertisementViewModel @Inject constructor( + private val getAdvertisementDetailUseCase: GetAdvertisementDetailUseCase +) : BaseViewModel() { + override fun createInitialState(): AdvertisementContract.AdvertisementUiState = AdvertisementContract.AdvertisementUiState() + + override suspend fun handleEvent(event: AdvertisementContract.AdvertisementEvent) { + when (event) { + is AdvertisementContract.AdvertisementEvent.FetchAdvertisementDetail -> setState { copy(loadState = event.loadState, advertisementDetail = event.advertisementDetail) } + } + } + + fun fetchAdvertisementDetail(advertisementId: Int) { + viewModelScope.launch { + setEvent(AdvertisementContract.AdvertisementEvent.FetchAdvertisementDetail(loadState = LoadState.Loading, advertisementDetail = currentState.advertisementDetail)) + getAdvertisementDetailUseCase(advertisementId = advertisementId).onSuccess { advertisementDetail -> + setEvent(AdvertisementContract.AdvertisementEvent.FetchAdvertisementDetail(loadState = LoadState.Success, advertisementDetail = advertisementDetail)) + }.onFailure { + setEvent(AdvertisementContract.AdvertisementEvent.FetchAdvertisementDetail(loadState = LoadState.Error, advertisementDetail = currentState.advertisementDetail)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/component/AdvertisementDetail.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/component/AdvertisementDetail.kt new file mode 100644 index 000000000..743c61994 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/component/AdvertisementDetail.kt @@ -0,0 +1,50 @@ +package org.sopt.dateroad.presentation.ui.advertisement.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun AdvertisementDetail( + advertisementTagTitle: String, + createAt: String, + title: String, + description: String +) { + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(20.dp)) + DateRoadTextTag( + textContent = advertisementTagTitle, + tagContentType = TagType.ADVERTISEMENT_TITLE + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = createAt, + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.gray400 + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = title, + style = DateRoadTheme.typography.titleExtra24, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = description, + style = DateRoadTheme.typography.bodyMed13Context, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(54.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/navigation/AdvertisementNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/navigation/AdvertisementNavigation.kt new file mode 100644 index 000000000..377ac9365 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/advertisement/navigation/AdvertisementNavigation.kt @@ -0,0 +1,37 @@ +package org.sopt.dateroad.presentation.ui.advertisement.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import org.sopt.dateroad.presentation.ui.advertisement.AdvertisementRoute + +fun NavController.navigationAdvertisement(advertisementId: Int) { + this.navigate(route = AdvertisementRoute.route(advertisementId = advertisementId)) +} + +fun NavGraphBuilder.advertisementGraph( + popBackStack: () -> Unit +) { + composable( + route = AdvertisementRoute.ROUTE_WITH_ARGUMENT, + arguments = listOf( + navArgument(AdvertisementRoute.ID) { + type = NavType.IntType + } + ) + ) { navBackStackEntry -> + AdvertisementRoute( + popBackStack = popBackStack, + advertisementId = navBackStackEntry.arguments?.getInt(AdvertisementRoute.ID) ?: 0 + ) + } +} + +object AdvertisementRoute { + private const val ROUTE = "advertisement" + const val ID = "id" + const val ROUTE_WITH_ARGUMENT = "$ROUTE/{$ID}" + fun route(advertisementId: Int) = "$ROUTE/$advertisementId" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBasicBottomSheet.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBasicBottomSheet.kt new file mode 100644 index 000000000..d03d95fba --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBasicBottomSheet.kt @@ -0,0 +1,117 @@ +package org.sopt.dateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRoadBasicBottomSheet( + isBottomSheetOpen: Boolean, + title: String, + isButtonEnabled: Boolean, + buttonText: String, + onButtonClick: () -> Unit = {}, + onDismissRequest: () -> Unit = {}, + itemList: List Unit>> +) { + DateRoadBottomSheet( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp), + sheetState = rememberModalBottomSheetState(), + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = isButtonEnabled, + buttonText = buttonText, + onButtonClick = onButtonClick, + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = title, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleBold18 + ) + Spacer(modifier = Modifier.height(16.dp)) + itemList.forEach { item -> + DateRoadBasicBottomSheetContent( + text = item.first, + onClick = { + item.second() + onDismissRequest() + } + ) + } + } + } +} + +@Composable +fun DateRoadBasicBottomSheetContent( + text: String, + onClick: () -> Unit = {} +) { + Box( + modifier = Modifier + .padding(vertical = 20.dp) + .noRippleClickable(onClick = onClick) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.purple500, + style = DateRoadTheme.typography.bodySemi15 + ) + } +} + +@Preview +@Composable +fun DateRoadBasicBottomSheetPreview() { + DATEROADTheme { + var text by rememberSaveable { mutableStateOf("BottomSheet") } + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = text, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + + DateRoadBasicBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + title = "프로필 사진 설정", + isButtonEnabled = true, + buttonText = "취소", + itemList = listOf( + "사진 등록" to { text = "사진 등록" }, + "사진 삭제" to { text = "사진 삭제" } + ), + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBottomSheet.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBottomSheet.kt new file mode 100644 index 000000000..b2708ae9a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadBottomSheet.kt @@ -0,0 +1,122 @@ +package org.sopt.dateroad.presentation.ui.component.bottomsheet + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@SuppressLint("CoroutineCreationDuringComposition") +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun DateRoadBottomSheet( + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + isBottomSheetOpen: Boolean, + isButtonEnabled: Boolean = true, + buttonText: String, + onButtonClick: () -> Unit = {}, + onDismissRequest: () -> Unit = {}, + content: @Composable () -> Unit +) { + val coroutineScope = rememberCoroutineScope() + if (isBottomSheetOpen) { + coroutineScope.launch { + sheetState.show() + } + ModalBottomSheet( + modifier = Modifier.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()), + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + onDismissRequest = onDismissRequest, + containerColor = DateRoadTheme.colors.white, + dragHandle = null + ) { + Column( + modifier = modifier + ) { + content() + DateRoadBasicButton( + isEnabled = true, + textContent = buttonText, + onClick = { + onButtonClick() + coroutineScope.launch { sheetState.hide() } + }, + enabledBackgroundColor = ( + if (isButtonEnabled) { + DateRoadTheme.colors.purple600 + } else { + DateRoadTheme.colors.gray200 + } + ), + enabledTextColor = ( + if (isButtonEnabled) { + DateRoadTheme.colors.white + } else { + (DateRoadTheme.colors.gray400) + } + ) + ) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun DateRoadBottomSheetPreview() { + DATEROADTheme { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "BottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + + DateRoadBottomSheet( + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + isBottomSheetOpen = isBottomSheetOpen, + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen }, + isButtonEnabled = false, + buttonText = "test", + content = { + Text( + text = "BottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadPickerBottomSheet.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadPickerBottomSheet.kt new file mode 100644 index 000000000..843d6adee --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadPickerBottomSheet.kt @@ -0,0 +1,88 @@ +package org.sopt.dateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.ui.component.bottomsheet.model.Picker +import org.sopt.dateroad.presentation.ui.component.numberpicker.DateRoadNumberPicker +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateRoadPickerBottomSheet( + isBottomSheetOpen: Boolean, + isButtonEnabled: Boolean, + buttonText: String, + onButtonClick: () -> Unit = {}, + onDismissRequest: () -> Unit = {}, + pickers: List +) { + DateRoadBottomSheet( + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = isButtonEnabled, + buttonText = buttonText, + onButtonClick = onButtonClick, + onDismissRequest = onDismissRequest + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth() + ) { + pickers.forEachIndexed { index, item -> + DateRoadNumberPicker( + modifier = Modifier + .weight(1f), + items = item.items, + startIndex = item.startIndex, + pickerState = item.pickerState + ) + if (index != pickers.size - 1) { + Spacer(modifier = Modifier.width(17.dp)) + } + } + } + Spacer(modifier = Modifier.height(19.dp)) + } + } +} + +@Preview +@Composable +fun DateRoadPickerBottomSheetPreview() { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "DateRoadPickerBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + + DateRoadPickerBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = true, + buttonText = "취소", + pickers = listOf( + Picker(items = (2000..2024).map { it.toString() }), + Picker(items = (1..12).map { it.toString() }), + Picker(items = (1..31).map { it.toString() }) + ) + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadRegionBottomSheet.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadRegionBottomSheet.kt new file mode 100644 index 000000000..a8a38b251 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/DateRoadRegionBottomSheet.kt @@ -0,0 +1,208 @@ +package org.sopt.dateroad.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.type.GyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.type.SeoulAreaType +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.type.DateRoadRegionBottomSheetType +import org.sopt.dateroad.presentation.ui.component.chip.DateRoadTextChip +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun DateRoadRegionBottomSheet( + isBottomSheetOpen: Boolean, + isButtonEnabled: Boolean, + dateRoadRegionBottomSheetType: DateRoadRegionBottomSheetType = DateRoadRegionBottomSheetType.LOOK, + titleText: String = stringResource(id = R.string.region_bottom_sheet_title), + buttonText: String = stringResource(id = R.string.apply), + selectedRegion: RegionType? = null, + onSelectedRegionChanged: (RegionType) -> Unit = {}, + selectedArea: Any? = null, + onSelectedAreaChanged: (Any) -> Unit = {}, + onButtonClick: (RegionType?, Any?) -> Unit = { _, _ -> }, + onDismissRequest: () -> Unit = {} +) { + var contentHeight by remember { mutableStateOf(0) } + val scrollState = rememberScrollState() + + DateRoadBottomSheet( + modifier = Modifier.padding(top = 15.dp, start = 16.dp, end = 16.dp, bottom = 16.dp), + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = isButtonEnabled, + buttonText = buttonText, + onButtonClick = { onButtonClick(selectedRegion, selectedArea) }, + onDismissRequest = onDismissRequest + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = titleText, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + Image( + modifier = Modifier + .padding(start = 15.dp, end = 5.dp, top = 15.dp, bottom = 15.dp) + .noRippleClickable(onClick = onDismissRequest), + painter = painterResource(id = R.drawable.ic_bottom_sheet_close), + contentDescription = null + ) + } + Spacer(modifier = Modifier.height(29.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 10.dp) + ) { + RegionType.entries.forEachIndexed { index, regionType -> + val isSelected = selectedRegion == regionType + DateRoadTextChip( + modifier = Modifier.weight(1f), + text = regionType.title, + chipType = ChipType.REGION, + isSelected = isSelected, + onSelectedChange = { + onSelectedRegionChanged(regionType) + } + ) + if (index != RegionType.entries.size - 1) { + Spacer(modifier = Modifier.width(10.dp)) + } + } + } + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(top = 18.dp, start = 2.dp, end = 2.dp, bottom = 21.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 180.dp, min = 180.dp) + .padding(end = 10.dp) + .let { if (contentHeight > 180) it.verticalScroll(scrollState) else it } + .onGloballyPositioned { coordinates -> + contentHeight = coordinates.size.height + } + ) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(9.dp), + verticalArrangement = Arrangement.spacedBy(11.dp), + modifier = Modifier.fillMaxWidth() + ) { + when (selectedRegion) { + RegionType.INCHEON -> IncheonAreaType.entries.forEach { areaType -> + DateRoadTextChip( + text = areaType.title, + chipType = ChipType.AREA, + isSelected = selectedArea == areaType, + onSelectedChange = { + onSelectedAreaChanged(areaType) + } + ) + } + + RegionType.GYEONGGI -> + GyeonggiAreaType.entries + .filterNot { gyeonggiAreaType -> dateRoadRegionBottomSheetType == DateRoadRegionBottomSheetType.ENROLL && gyeonggiAreaType == GyeonggiAreaType.GYEONGGI_ENTIRE } + .forEach { areaType -> + DateRoadTextChip( + text = areaType.title, + chipType = ChipType.AREA, + isSelected = selectedArea == areaType, + onSelectedChange = { + onSelectedAreaChanged(areaType) + } + ) + } + + else -> + SeoulAreaType.entries + .filterNot { seoulAreaType -> dateRoadRegionBottomSheetType == DateRoadRegionBottomSheetType.ENROLL && seoulAreaType == SeoulAreaType.SEOUL_ENTIRE } + .forEach { areaType -> + DateRoadTextChip( + text = areaType.title, + chipType = ChipType.AREA, + isSelected = selectedArea == areaType, + onSelectedChange = { + onSelectedAreaChanged(areaType) + } + ) + } + } + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + } +} + +@Preview +@Composable +fun DateRoadRegionBottomSheetPreview() { + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + var selectedRegion by rememberSaveable { mutableStateOf(null) } + var selectedArea by rememberSaveable { mutableStateOf(null) } + + Button(onClick = { isBottomSheetOpen = true }) { + Text( + text = "RegionBottomSheet", + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + } + + DateRoadRegionBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = selectedRegion != null && selectedArea != null, + selectedRegion = selectedRegion, + selectedArea = selectedArea, + onSelectedRegionChanged = { newSelectedRegion -> + selectedRegion = newSelectedRegion + selectedArea = null + }, + onSelectedAreaChanged = { newSelectedArea -> + selectedArea = newSelectedArea + }, + onDismissRequest = { isBottomSheetOpen = !isBottomSheetOpen } + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/model/Picker.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/model/Picker.kt new file mode 100644 index 000000000..f45054a5e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/bottomsheet/model/Picker.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.presentation.ui.component.bottomsheet.model + +import org.sopt.dateroad.presentation.ui.component.numberpicker.state.PickerState + +data class Picker( + val items: List, + val startIndex: Int = 0, + val pickerState: PickerState = PickerState() +) diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadAreaButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadAreaButton.kt new file mode 100644 index 000000000..31f21f391 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadAreaButton.kt @@ -0,0 +1,60 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadAreaButton( + modifier: Modifier = Modifier, + isSelected: Boolean, + textContent: String, + onClick: () -> Unit = {} +) { + DateRoadButton( + modifier = modifier + .fillMaxWidth(), + backgroundColor = DateRoadTheme.colors.gray100, + borderColor = if (isSelected) DateRoadTheme.colors.purple600 else Color.Unspecified, + borderWidth = 1.dp, + cornerRadius = 10.dp, + paddingVertical = 6.dp, + paddingHorizontal = 12.dp, + onClick = onClick + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = textContent, + color = if (isSelected) DateRoadTheme.colors.purple600 else DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.weight(1f)) + Icon(painter = painterResource(id = R.drawable.ic_area_dropdown), contentDescription = null, tint = if (isSelected) DateRoadTheme.colors.purple600 else DateRoadTheme.colors.gray400) + } + } +} + +@Preview +@Composable +fun DateRoadAreaButtonPreview() { + DATEROADTheme { + DateRoadAreaButton( + textContent = "건대/성수/왕십리", + isSelected = true + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadBasicButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadBasicButton.kt new file mode 100644 index 000000000..7d3c01def --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadBasicButton.kt @@ -0,0 +1,53 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadBasicButton( + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + enabledBackgroundColor: Color = DateRoadTheme.colors.purple600, + enabledTextColor: Color = DateRoadTheme.colors.white, + textContent: String, + onClick: () -> Unit = {} +) { + DateRoadFilledButton( + modifier = modifier.fillMaxWidth(), + isEnabled = isEnabled, + textContent = textContent, + textStyle = DateRoadTheme.typography.bodyBold15, + enabledBackgroundColor = enabledBackgroundColor, + enabledTextColor = enabledTextColor, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 14.dp, + paddingHorizontal = 0.dp, + paddingVertical = 16.dp, + onClick = onClick + ) +} + +@Preview +@Composable +fun DateRoadBasicButtonPreview() { + DATEROADTheme { + Column { + DateRoadBasicButton( + isEnabled = true, + textContent = "프로필 등록하기" + ) + DateRoadBasicButton( + isEnabled = false, + textContent = "프로필 등록하기" + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadButton.kt new file mode 100644 index 000000000..3e5cbccb2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadButton.kt @@ -0,0 +1,40 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.util.modifier.noRippleDebounceClickable + +@Composable +fun DateRoadButton( + modifier: Modifier = Modifier, + backgroundColor: Color, + borderColor: Color = Color.Unspecified, + borderWidth: Dp = 0.dp, + cornerRadius: Dp = 0.dp, + paddingHorizontal: Dp = 0.dp, + paddingVertical: Dp = 0.dp, + onClick: suspend () -> Unit, + content: @Composable () -> Unit +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(cornerRadius)) + .background(color = backgroundColor) + .noRippleDebounceClickable(onClick = onClick) + .border(width = borderWidth, color = borderColor, shape = RoundedCornerShape(cornerRadius)) + .padding(horizontal = paddingHorizontal, vertical = paddingVertical), + contentAlignment = Alignment.Center + ) { + content() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadFilledButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadFilledButton.kt new file mode 100644 index 000000000..a0cd64162 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadFilledButton.kt @@ -0,0 +1,107 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadFilledButton( + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + textContent: String, + textStyle: TextStyle, + enabledBackgroundColor: Color, + enabledTextColor: Color, + disabledBackgroundColor: Color, + disabledTextColor: Color, + cornerRadius: Dp, + paddingHorizontal: Dp, + paddingVertical: Dp, + onClick: () -> Unit +) { + DateRoadButton( + modifier = modifier, + backgroundColor = if (isEnabled) enabledBackgroundColor else disabledBackgroundColor, + cornerRadius = cornerRadius, + paddingHorizontal = paddingHorizontal, + paddingVertical = paddingVertical, + onClick = { if (isEnabled) onClick() } + ) { + Text( + text = textContent, + style = textStyle, + color = if (isEnabled) enabledTextColor else disabledTextColor, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +fun DateRoadFilledButtonPreview() { + DATEROADTheme { + Column { + DateRoadFilledButton( + isEnabled = true, + textContent = "중복확인", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 10.dp, + paddingHorizontal = 14.dp, + paddingVertical = 6.dp + ) + DateRoadFilledButton( + isEnabled = false, + textContent = "중복확인", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 10.dp, + paddingHorizontal = 14.dp, + paddingVertical = 6.dp + ) + DateRoadFilledButton( + isEnabled = true, + textContent = "불러오기", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 20.dp, + paddingHorizontal = 10.dp, + paddingVertical = 5.dp + ) + DateRoadFilledButton( + isEnabled = true, + textContent = "데이트 코스 받고 100P 받기", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 10.dp, + paddingHorizontal = 20.dp, + paddingVertical = 10.dp + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadImageButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadImageButton.kt new file mode 100644 index 000000000..f45a94264 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadImageButton.kt @@ -0,0 +1,81 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadImageButton( + modifier: Modifier = Modifier, + isEnabled: Boolean = false, + enabledBackgroundColor: Color = DateRoadTheme.colors.purple600, + enabledContentColor: Color = DateRoadTheme.colors.white, + disabledBackgroundColor: Color = DateRoadTheme.colors.gray200, + disabledContentColor: Color = DateRoadTheme.colors.gray400, + iconResId: Int = R.drawable.ic_all_plus_white, + cornerRadius: Dp, + paddingHorizontal: Dp, + paddingVertical: Dp, + onClick: () -> Unit +) { + DateRoadButton( + modifier = modifier, + backgroundColor = if (isEnabled) enabledBackgroundColor else disabledBackgroundColor, + cornerRadius = cornerRadius, + paddingHorizontal = paddingHorizontal, + paddingVertical = paddingVertical, + onClick = onClick + ) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + tint = if (isEnabled) enabledContentColor else disabledContentColor + ) + } +} + +@Preview +@Composable +fun DateRoadImageButtonPreview() { + DATEROADTheme { + Column { + DateRoadImageButton( + isEnabled = true, + onClick = {}, + cornerRadius = 14.dp, + paddingHorizontal = 16.dp, + paddingVertical = 8.dp + ) + DateRoadImageButton( + isEnabled = true, + onClick = {}, + cornerRadius = 14.dp, + paddingHorizontal = 12.dp, + paddingVertical = 12.dp + ) + DateRoadImageButton( + isEnabled = false, + onClick = {}, + cornerRadius = 14.dp, + paddingHorizontal = 12.dp, + paddingVertical = 12.dp + ) + DateRoadImageButton( + isEnabled = true, + onClick = {}, + cornerRadius = 44.dp, + paddingHorizontal = 12.dp, + paddingVertical = 12.dp + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt new file mode 100644 index 000000000..9b92a774b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadKakaoLoginButton.kt @@ -0,0 +1,73 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadKakaoLoginButton( + modifier: Modifier = Modifier, + backgroundColor: Color = DateRoadTheme.colors.kakaoYellow, + contentColor: Color = DateRoadTheme.colors.black, + onClick: () -> Unit = {} +) { + DateRoadButton( + modifier = modifier.fillMaxWidth(), + backgroundColor = backgroundColor, + cornerRadius = 6.dp, + paddingVertical = 11.dp, + paddingHorizontal = 14.dp, + onClick = onClick + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_kakao_logo), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + ) + Spacer(modifier = Modifier.size(5.dp)) + Text( + text = stringResource(id = R.string.kakao_login), + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 1.5.em, + color = contentColor, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterVertically) + .fillMaxWidth() + ) + } + } +} + +@Preview +@Composable +fun DateRoadKakaoLoginButtonPreview() { + DATEROADTheme { + DateRoadKakaoLoginButton() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadOutlinedButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadOutlinedButton.kt new file mode 100644 index 000000000..13a005d44 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadOutlinedButton.kt @@ -0,0 +1,64 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadOutlinedButton( + modifier: Modifier = Modifier, + textContent: String, + textStyle: TextStyle, + backgroundColor: Color, + contentColor: Color, + borderWidth: Dp, + cornerRadius: Dp, + paddingHorizontal: Dp = 0.dp, + paddingVertical: Dp = 0.dp, + onClick: () -> Unit +) { + DateRoadButton( + modifier = modifier.fillMaxWidth(), + backgroundColor = backgroundColor, + borderColor = contentColor, + borderWidth = borderWidth, + cornerRadius = cornerRadius, + paddingVertical = paddingVertical, + paddingHorizontal = paddingHorizontal, + onClick = onClick + ) { + Text( + modifier = modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = textContent, + style = textStyle, + color = contentColor + ) + } +} + +@Preview +@Composable +fun DateRoadOutlinedButtonPreview() { + DATEROADTheme { + DateRoadOutlinedButton( + textContent = "다음", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyBold15, + contentColor = DateRoadTheme.colors.purple600, + backgroundColor = DateRoadTheme.colors.white, + cornerRadius = 29.dp, + borderWidth = 1.dp, + paddingVertical = 16.dp + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadTextButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadTextButton.kt new file mode 100644 index 000000000..02fb905c2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/button/DateRoadTextButton.kt @@ -0,0 +1,79 @@ +package org.sopt.dateroad.presentation.ui.component.button + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadTextButton( + modifier: Modifier = Modifier, + textContent: String, + textStyle: TextStyle, + textColor: Color, + paddingHorizontal: Dp, + paddingVertical: Dp, + onClick: () -> Unit +) { + DateRoadButton( + modifier = modifier, + backgroundColor = Color.Transparent, + paddingHorizontal = paddingHorizontal, + paddingVertical = paddingVertical, + onClick = onClick + ) { + Text( + text = textContent, + style = textStyle, + color = textColor + ) + } +} + +@Preview +@Composable +fun DateRoadTextButtonPreview() { + DATEROADTheme { + Column { + DateRoadTextButton( + textContent = "편집", + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = DateRoadTheme.colors.gray400, + paddingHorizontal = 18.dp, + paddingVertical = 6.dp, + onClick = {} + ) + DateRoadTextButton( + textContent = "완료", + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = DateRoadTheme.colors.purple600, + paddingHorizontal = 18.dp, + paddingVertical = 6.dp, + onClick = {} + ) + DateRoadTextButton( + textContent = "더보기", + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = DateRoadTheme.colors.purple600, + paddingHorizontal = 20.dp, + paddingVertical = 8.dp, + onClick = {} + ) + DateRoadTextButton( + textContent = "삭제", + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = DateRoadTheme.colors.gray400, + paddingHorizontal = 18.dp, + paddingVertical = 6.dp, + onClick = {} + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadCourseCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadCourseCard.kt new file mode 100644 index 000000000..53f8de4e8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadCourseCard.kt @@ -0,0 +1,132 @@ +package org.sopt.dateroad.presentation.ui.component.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadCourseCard( + modifier: Modifier = Modifier, + course: Course, + onClick: (Int) -> Unit = {} +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(130.dp) + .background(DateRoadTheme.colors.white) + .noRippleClickable(onClick = { onClick(course.courseId) }) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(top = 10.dp, bottom = 10.dp, start = 16.dp) + .aspectRatio(1f) + ) { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(course.thumbnail) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(14.dp)) + ) + DateRoadImageTag( + textContent = course.like, + imageContent = R.drawable.ic_tag_heart, + tagContentType = TagType.HEART, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 5.dp, bottom = 5.dp, end = 5.dp) + ) + } + Column( + modifier = Modifier + .padding(start = 14.dp) + .fillMaxHeight() + ) { + Spacer(modifier = Modifier.height(15.dp)) + Text( + text = course.city, + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.gray400, + modifier = Modifier + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = course.title, + style = DateRoadTheme.typography.bodyBold15, + color = DateRoadTheme.colors.black, + modifier = Modifier + .fillMaxWidth(), + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(10.dp)) + Row { + DateRoadImageTag( + textContent = course.cost, + imageContent = R.drawable.ic_all_money_12, + tagContentType = TagType.MONEY + ) + Spacer(modifier = Modifier.size(6.dp)) + DateRoadImageTag( + textContent = course.duration, + imageContent = R.drawable.ic_all_clock_12, + tagContentType = TagType.TIME + ) + } + } + } +} + +@Preview +@Composable +fun DateRoadCourseCardPreview() { + Column { + DateRoadCourseCard( + course = Course( + courseId = 1, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "여기 야키니쿠 꼭 먹으러 가세요\n하지만 일본에 있습니다.", + cost = "10만원 초과", + duration = "10시간", + like = "999" + ) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadPlaceCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadPlaceCard.kt new file mode 100644 index 000000000..ba0028ef4 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/card/DateRoadPlaceCard.kt @@ -0,0 +1,113 @@ +package org.sopt.dateroad.presentation.ui.component.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.presentation.type.PlaceCardType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadPlaceCard( + modifier: Modifier = Modifier, + placeCardType: PlaceCardType, + sequence: Int? = null, + place: Place, + onIconClick: (() -> Unit)? = null +) { + val paddingValues = Modifier.padding(start = placeCardType.startPadding, end = placeCardType.endPadding) + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.gray100) + .then(paddingValues) + .padding(vertical = placeCardType.verticalPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (placeCardType == PlaceCardType.COURSE_NORMAL) { + sequence?.let { + DateRoadTextTag( + textContent = (sequence + 1).toString(), + tagContentType = TagType.PLACE_CARD_NUMBER + ) + } + Spacer(modifier = Modifier.width(14.dp)) + } + Text( + text = place.title, + modifier = Modifier.weight(1f), + style = DateRoadTheme.typography.bodyBold15, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(10.dp)) + + DateRoadTextTag( + textContent = place.duration, + tagContentType = TagType.PLACE_CARD_TIME + ) + placeCardType.iconRes?.let { + Icon( + painter = painterResource(id = it), + contentDescription = null, + modifier = Modifier + .padding( + start = placeCardType.iconStartPadding, + top = placeCardType.iconTopPadding, + end = placeCardType.iconEndPadding, + bottom = placeCardType.iconBottomPadding + ) + .noRippleClickable { + onIconClick?.invoke() + } + ) + } + } +} + +@Preview +@Composable +fun DateRoadPlaceCardPreview() { + Column { + DateRoadPlaceCard( + placeCardType = PlaceCardType.COURSE_NORMAL, + sequence = 0, + place = Place(title = "성수미술관 성수점성수미술관 성수점성수미술관 성수점성수미술관 성수점성수미술관 성수점", duration = "2.5시간") + ) + Spacer(modifier = Modifier.height(8.dp)) + DateRoadPlaceCard( + placeCardType = PlaceCardType.COURSE_EDIT, + place = Place(title = "성수미술관 성수점", duration = "1시간"), + onIconClick = { } + ) + Spacer(modifier = Modifier.height(8.dp)) + DateRoadPlaceCard( + placeCardType = PlaceCardType.COURSE_DELETE, + place = Place(title = "성수미술관 성수점", duration = "0.5시간"), + onIconClick = { } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadChip.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadChip.kt new file mode 100644 index 000000000..301a3bc96 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadChip.kt @@ -0,0 +1,34 @@ +package org.sopt.dateroad.presentation.ui.component.chip + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable + +@Composable +fun DateRoadChip( + modifier: Modifier = Modifier, + chipType: ChipType, + isSelected: Boolean = false, + onSelectedChange: (Boolean) -> Unit = {}, + content: @Composable (Boolean) -> Unit +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(chipType.cornerRadius)) + .background(color = if (isSelected) chipType.selectedBackgroundColor else chipType.unselectedBackgroundColor) + .noRippleClickable { + onSelectedChange(isSelected) + } + .padding(horizontal = chipType.horizontalPadding, vertical = chipType.verticalPadding), + contentAlignment = Alignment.Center + ) { + content(isSelected) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadImageChip.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadImageChip.kt new file mode 100644 index 000000000..3bd5426b6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadImageChip.kt @@ -0,0 +1,62 @@ +package org.sopt.dateroad.presentation.ui.component.chip + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.type.DateTagType + +@Composable +fun DateRoadImageChip( + modifier: Modifier = Modifier, + @StringRes textId: Int, + @DrawableRes imageRes: Int, + chipType: ChipType, + spaceValue: Dp = 2.dp, + isSelected: Boolean = false, + onSelectedChange: (Boolean) -> Unit = {} +) { + DateRoadChip( + modifier = modifier, + chipType = chipType, + isSelected = isSelected, + onSelectedChange = onSelectedChange + ) { selected -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = imageRes), + contentDescription = null + ) + Spacer(modifier = Modifier.size(spaceValue)) + Text( + text = stringResource(id = textId), + style = chipType.textStyle, + color = if (selected) chipType.selectedTextColor else chipType.unselectedTextColor + ) + } + } +} + +@Preview +@Composable +fun DateRoadImageChipPreview() { + DateRoadImageChip( + textId = DateTagType.EXHIBITION_POPUP.titleRes, + imageRes = DateTagType.EXHIBITION_POPUP.imageRes, + chipType = ChipType.DATE + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadTextChip.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadTextChip.kt new file mode 100644 index 000000000..625ce0239 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chip/DateRoadTextChip.kt @@ -0,0 +1,50 @@ +package org.sopt.dateroad.presentation.ui.component.chip + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.ChipType + +@Composable +fun DateRoadTextChip( + modifier: Modifier = Modifier, + text: String, + chipType: ChipType, + isSelected: Boolean = false, + onSelectedChange: (Boolean) -> Unit = {} +) { + DateRoadChip( + modifier = modifier, + chipType = chipType, + isSelected = isSelected, + onSelectedChange = onSelectedChange + ) { selected -> + Text( + text = text, + style = chipType.textStyle, + color = if (selected) chipType.selectedTextColor else chipType.unselectedTextColor + ) + } +} + +@Preview +@Composable +fun DateRoadTextChipPreview() { + Column { + DateRoadTextChip(text = "10만원 이상", chipType = ChipType.MONEY) + Spacer(modifier = Modifier.height(10.dp)) + DateRoadTextChip(text = "식도락", chipType = ChipType.DATE) + Spacer(modifier = Modifier.height(10.dp)) + DateRoadTextChip(text = "식도락", chipType = ChipType.DATE) + Spacer(modifier = Modifier.height(10.dp)) + DateRoadTextChip(modifier = Modifier.fillMaxWidth(), text = "서울", chipType = ChipType.REGION) + Spacer(modifier = Modifier.height(10.dp)) + DateRoadTextChip(modifier = Modifier.fillMaxWidth(), text = "수원", chipType = ChipType.AREA) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chipgroup/DateRoadDateChipGroup.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chipgroup/DateRoadDateChipGroup.kt new file mode 100644 index 000000000..32967c01f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/chipgroup/DateRoadDateChipGroup.kt @@ -0,0 +1,82 @@ +package org.sopt.dateroad.presentation.ui.component.chipgroup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.type.DateChipGroupType +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.ui.component.chip.DateRoadImageChip +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DateRoadDateChipGroup( + modifier: Modifier = Modifier, + dateChipGroupType: DateChipGroupType, + selectedDateTags: List, + onSelectedDateTagsChanged: (DateTagType) -> Unit = {} +) { + Column( + modifier = modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(id = dateChipGroupType.titleRes, selectedDateTags.size, dateChipGroupType.maxSize), + color = DateRoadTheme.colors.black, + style = dateChipGroupType.titleTextStyle + ) + Spacer(modifier = Modifier.height(10.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(7.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + DateTagType.entries.forEach { dateTagType: DateTagType -> + DateRoadImageChip( + textId = dateTagType.titleRes, + imageRes = dateTagType.imageRes, + chipType = ChipType.DATE, + isSelected = selectedDateTags.contains(dateTagType), + onSelectedChange = { + onSelectedDateTagsChanged(dateTagType) + } + ) + } + } + } +} + +@Preview +@Composable +fun DateRoadDateChipGroupPreview() { + DATEROADTheme { + var selectedDateTags by rememberSaveable { mutableStateOf>(emptyList()) } + + DateRoadDateChipGroup( + dateChipGroupType = DateChipGroupType.PROFILE, + selectedDateTags = selectedDateTags, + onSelectedDateTagsChanged = { selectedDateTag -> + when { + selectedDateTags.contains(selectedDateTag) -> selectedDateTags -= selectedDateTag + selectedDateTags.size < DateChipGroupType.PROFILE.maxSize -> selectedDateTags += selectedDateTag + } + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadDialog.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadDialog.kt new file mode 100644 index 000000000..f693d21d1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadDialog.kt @@ -0,0 +1,99 @@ +package org.sopt.dateroad.presentation.ui.component.dialog + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.sopt.dateroad.presentation.type.OneButtonDialogType +import org.sopt.dateroad.presentation.type.OneButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.type.TwoButtonDialogType +import org.sopt.dateroad.presentation.type.TwoButtonDialogWithDescriptionType + +@Composable +fun DateRoadDialog( + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest + ) { + content() + } +} + +@Preview +@Composable +fun DateRoadDialogPreview(modifier: Modifier = Modifier) { + val showOneButtonDialog = remember { mutableStateOf(false) } + val showOneButtonDialogWithDescription = remember { mutableStateOf(false) } + val showTwoButtonDialog = remember { mutableStateOf(false) } + val showTwoButtonDialogWithDescription = remember { mutableStateOf(false) } + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button(onClick = { showOneButtonDialog.value = true }) { + Text(text = "Show One Button Dialog") + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { showOneButtonDialogWithDescription.value = true }) { + Text(text = "Show One Button Dialog With Description") + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { showTwoButtonDialog.value = true }) { + Text(text = "Show Two Button Dialog") + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { showTwoButtonDialogWithDescription.value = true }) { + Text(text = "Show Two Button Dialog With Description") + } + } + + if (showOneButtonDialog.value) { + DateRoadOneButtonDialog( + oneButtonDialogType = OneButtonDialogType.ENROLL_TIMELINE, + onDismissRequest = { showOneButtonDialog.value = false }, + onClickConfirm = { showOneButtonDialog.value = false } + ) + } + + if (showOneButtonDialogWithDescription.value) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.ENROLL_COURSE, + onDismissRequest = { showOneButtonDialogWithDescription.value = false }, + onClickConfirm = { showOneButtonDialogWithDescription.value = false } + ) + } + + if (showTwoButtonDialog.value) { + DateRoadTwoButtonDialog( + twoButtonDialogType = TwoButtonDialogType.LOGOUT, + onDismissRequest = { showTwoButtonDialog.value = false }, + onClickConfirm = { showTwoButtonDialog.value = false }, + onClickDismiss = { showTwoButtonDialog.value = false } + ) + } + + if (showTwoButtonDialogWithDescription.value) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.POINT_LACK, + onDismissRequest = { showTwoButtonDialogWithDescription.value = false }, + onClickConfirm = { showTwoButtonDialogWithDescription.value = false }, + onClickDismiss = { showTwoButtonDialogWithDescription.value = false } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialog.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialog.kt new file mode 100644 index 000000000..c5b3e6488 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialog.kt @@ -0,0 +1,58 @@ +package org.sopt.dateroad.presentation.ui.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.OneButtonDialogType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadOneButtonDialog( + oneButtonDialogType: OneButtonDialogType, + onDismissRequest: () -> Unit, + onClickConfirm: () -> Unit +) { + DateRoadDialog( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(color = DateRoadTheme.colors.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.black, + text = stringResource(id = oneButtonDialogType.titleRes), + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer(modifier = Modifier.height(36.dp)) + DateRoadBasicButton( + modifier = Modifier.padding(horizontal = 16.dp), + isEnabled = true, + textContent = stringResource(id = oneButtonDialogType.buttonTextRes), + onClick = onClickConfirm + ) + Spacer(modifier = Modifier.height(14.dp)) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialogWithDescription.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialogWithDescription.kt new file mode 100644 index 000000000..33682f027 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadOneButtonDialogWithDescription.kt @@ -0,0 +1,68 @@ +package org.sopt.dateroad.presentation.ui.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.OneButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType: OneButtonDialogWithDescriptionType, + onDismissRequest: () -> Unit, + onClickConfirm: () -> Unit +) { + DateRoadDialog( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(color = DateRoadTheme.colors.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(23.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.black, + text = stringResource(id = oneButtonDialogWithDescriptionType.titleRes), + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.black, + text = stringResource(id = oneButtonDialogWithDescriptionType.descriptionRes), + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(30.dp)) + DateRoadBasicButton( + modifier = Modifier.padding(horizontal = 16.dp), + isEnabled = true, + textContent = stringResource(id = oneButtonDialogWithDescriptionType.buttonTextRes), + onClick = onClickConfirm + ) + Spacer(modifier = Modifier.height(14.dp)) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialog.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialog.kt new file mode 100644 index 000000000..6ea966795 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialog.kt @@ -0,0 +1,74 @@ +package org.sopt.dateroad.presentation.ui.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.TwoButtonDialogType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadTwoButtonDialog( + twoButtonDialogType: TwoButtonDialogType, + onDismissRequest: () -> Unit, + onClickConfirm: () -> Unit, + onClickDismiss: () -> Unit +) { + DateRoadDialog( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(color = DateRoadTheme.colors.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(27.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = DateRoadTheme.colors.black, + textAlign = TextAlign.Center, + text = stringResource(id = twoButtonDialogType.titleRes), + style = DateRoadTheme.typography.bodyMed15 + ) + Spacer(modifier = Modifier.height(29.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + DateRoadBasicButton( + modifier = Modifier.weight(1f), + enabledBackgroundColor = DateRoadTheme.colors.gray200, + enabledTextColor = DateRoadTheme.colors.gray400, + textContent = stringResource(id = twoButtonDialogType.dismissButtonTextRes), + onClick = onClickDismiss + ) + DateRoadBasicButton( + modifier = Modifier.weight(1f), + textContent = stringResource(id = twoButtonDialogType.confirmButtonTextRes), + onClick = onClickConfirm + ) + } + Spacer(modifier = Modifier.height(14.dp)) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialogWithDescription.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialogWithDescription.kt new file mode 100644 index 000000000..d003950b5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dialog/DateRoadTwoButtonDialogWithDescription.kt @@ -0,0 +1,86 @@ +package org.sopt.dateroad.presentation.ui.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.TwoButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType: TwoButtonDialogWithDescriptionType, + onDismissRequest: () -> Unit = {}, + onClickConfirm: () -> Unit, + onClickDismiss: () -> Unit = {} + +) { + DateRoadDialog( + onDismissRequest = onDismissRequest + + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(color = DateRoadTheme.colors.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(23.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.black, + text = stringResource(id = twoButtonDialogWithDescriptionType.titleRes), + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + color = DateRoadTheme.colors.black, + text = stringResource(id = twoButtonDialogWithDescriptionType.descriptionRes), + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(30.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + DateRoadBasicButton( + modifier = Modifier.weight(1f), + enabledBackgroundColor = DateRoadTheme.colors.gray200, + enabledTextColor = DateRoadTheme.colors.gray400, + textContent = stringResource(id = twoButtonDialogWithDescriptionType.dismissButtonTextRes), + onClick = onClickDismiss + ) + DateRoadBasicButton( + modifier = Modifier.weight(1f), + textContent = stringResource(id = twoButtonDialogWithDescriptionType.confirmButtonTextRes), + onClick = onClickConfirm + ) + } + Spacer(modifier = Modifier.height(14.dp)) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dotsindicator/DotsIndicator.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dotsindicator/DotsIndicator.kt new file mode 100644 index 000000000..d0ecbca77 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/dotsindicator/DotsIndicator.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.ui.component.dotsindicator + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DotsIndicator(totalDots: Int, selectedIndex: Int, modifier: Modifier = Modifier, indicatorSize: Dp) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.Bottom + ) { + for (i in 0 until totalDots) { + val color = if (i == selectedIndex) DateRoadTheme.colors.purple600 else DateRoadTheme.colors.gray200 + Box( + modifier = Modifier + .size(indicatorSize) + .background(color, CircleShape) + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/DateRoadNumberPicker.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/DateRoadNumberPicker.kt new file mode 100644 index 000000000..979a8fc18 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/DateRoadNumberPicker.kt @@ -0,0 +1,139 @@ +package org.sopt.dateroad.presentation.ui.component.numberpicker + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import org.sopt.dateroad.presentation.ui.component.numberpicker.state.PickerState +import org.sopt.dateroad.presentation.ui.component.numberpicker.state.rememberPickerState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DateRoadNumberPicker( + modifier: Modifier = Modifier, + pickerState: PickerState = rememberPickerState(), + items: List, + startIndex: Int = 0, + visibleItemCount: Int = 3, + dividerColor: Color = DateRoadTheme.colors.gray400 +) { + var itemHeightPixel by remember { mutableStateOf(0) } + val itemHeightDp = with(LocalDensity.current) { itemHeightPixel.toDp() } + + val visibleItemsMiddle = visibleItemCount / 2 + val scrollState = rememberLazyListState(initialFirstVisibleItemIndex = startIndex) + val flingBehavior = rememberSnapFlingBehavior(lazyListState = scrollState) + + LaunchedEffect(itemHeightPixel) { + if (itemHeightPixel > 0) scrollState.scrollToItem(startIndex) + } + + LaunchedEffect(scrollState) { + snapshotFlow { scrollState.firstVisibleItemIndex } + .map { index -> items[index] } + .distinctUntilChanged() + .collect { item -> + pickerState.selectedItem = item + } + } + + Box( + modifier = modifier + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .height(itemHeightDp * visibleItemCount), + flingBehavior = flingBehavior, + state = scrollState, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(visibleItemsMiddle) { + Spacer(modifier = Modifier.height(itemHeightDp)) + } + items(items.size) { index -> + DateRoadNumberPickerContent( + modifier = Modifier + .onSizeChanged { intSize: IntSize -> itemHeightPixel = intSize.height }, + text = items[index], + color = if (pickerState.selectedItem == items[index]) DateRoadTheme.colors.black else DateRoadTheme.colors.gray200 + ) + } + items(visibleItemsMiddle) { + Spacer(modifier = Modifier.height(itemHeightDp)) + } + } + HorizontalDivider( + modifier = Modifier + .offset(y = itemHeightDp * visibleItemsMiddle), + color = dividerColor, + thickness = 1.dp + ) + HorizontalDivider( + modifier = Modifier + .offset(y = itemHeightDp * (visibleItemsMiddle + 1)), + color = dividerColor, + thickness = 1.dp + ) + } +} + +@Composable +fun DateRoadNumberPickerContent( + modifier: Modifier = Modifier, + color: Color, + text: String +) { + Box( + modifier = modifier + .padding(vertical = 13.dp) + ) { + Text( + text = text, + textAlign = TextAlign.Center, + color = color, + style = DateRoadTheme.typography.bodySemi15, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Preview +@Composable +fun DateRoadNumberPickerPreview() { + DATEROADTheme { + DateRoadNumberPicker( + items = (0..11).map { it.toString() } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/state/PickerState.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/state/PickerState.kt new file mode 100644 index 000000000..7c3fc7ed6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/numberpicker/state/PickerState.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad.presentation.ui.component.numberpicker.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +class PickerState { + var selectedItem by mutableStateOf("") +} + +@Composable +fun rememberPickerState() = remember { + PickerState() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/pager/DateRoadImagePager.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/pager/DateRoadImagePager.kt new file mode 100644 index 000000000..de6c7eccd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/pager/DateRoadImagePager.kt @@ -0,0 +1,72 @@ +package org.sopt.dateroad.presentation.ui.component.pager + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun DateRoadImagePager( + modifier: Modifier = Modifier, + pagerState: PagerState, + images: List, + userScrollEnabled: Boolean, + like: String? +) { + Box(modifier = Modifier.fillMaxWidth()) { + HorizontalPager( + count = images.size, + state = pagerState, + modifier = Modifier.fillMaxWidth(), + userScrollEnabled = userScrollEnabled + ) { page -> + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(images[page]) + .crossfade(true) + .build(), + contentDescription = null, + modifier = modifier + .fillMaxWidth() + .aspectRatio(1f), + contentScale = ContentScale.Crop + ) + } + + if (like != null) { + DateRoadImageTag( + textContent = like, + imageContent = R.drawable.ic_tag_heart, + tagContentType = TagType.HEART, + modifier = Modifier + .padding(start = 10.dp, bottom = 10.dp) + .align(Alignment.BottomStart) + ) + } + + DateRoadTextTag( + textContent = stringResource(id = R.string.fraction_format, pagerState.currentPage + 1, pagerState.pageCount), + tagContentType = TagType.COURSE_DETAIL_PHOTO_NUMBER, + modifier = Modifier + .padding(end = 10.dp, bottom = 10.dp) + .align(Alignment.BottomEnd) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/partialcolortext/PartialColorText.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/partialcolortext/PartialColorText.kt new file mode 100644 index 000000000..549358653 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/partialcolortext/PartialColorText.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.ui.component.partialcolortext + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle + +@Composable +fun PartialColorText(text: String, keywords: List, color: Color): AnnotatedString { + return buildAnnotatedString { + var currentIndex = 0 + while (currentIndex < text.length) { + val keywordIndex = keywords + .map { keyword -> text.indexOf(keyword, currentIndex, ignoreCase = true) to keyword } + .filter { it.first >= 0 } + .minByOrNull { it.first } + + if (keywordIndex != null && keywordIndex.first >= 0) { + append(text.substring(currentIndex, keywordIndex.first)) + withStyle(style = SpanStyle(color)) { + append(keywordIndex.second) + } + currentIndex = keywordIndex.first + keywordIndex.second.length + } else { + append(text.substring(currentIndex)) + break + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tabbar/DateRoadTabBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tabbar/DateRoadTabBar.kt new file mode 100644 index 000000000..d806a4aac --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tabbar/DateRoadTabBar.kt @@ -0,0 +1,181 @@ +package org.sopt.dateroad.presentation.ui.component.tabbar + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +enum class SubComposeID { + HEIGHT, + WIDTH, + TAB, + INDICATOR, + DIVIDER +} + +data class TabPosition( + val x: Dp, + val width: Dp +) + +@Composable +fun DateRoadTabBar( + dividerColor: Color = DateRoadTheme.colors.gray300, + indicatorColor: Color = DateRoadTheme.colors.black, + animationSpec: AnimationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), + selectedTabPosition: Int = 0, + tabItem: @Composable () -> Unit +) { + SubcomposeLayout( + modifier = + Modifier + .selectableGroup() + ) { constraints -> + val maxItemHeight = + subcompose(SubComposeID.HEIGHT, tabItem) + .map { it.measure(constraints) }.maxOf { it.height } + + val itemWidth = + constraints.maxWidth / subcompose(SubComposeID.WIDTH, tabItem) + .map { it.measure(constraints) }.size + + val tabs = + subcompose(SubComposeID.TAB, tabItem).map { + it.measure(Constraints.fixed(itemWidth, maxItemHeight)) + } + + val tabPositions = + List(tabs.size) { index -> + TabPosition(x = (itemWidth * index).toDp(), width = itemWidth.toDp()) + } + + layout(constraints.maxWidth, maxItemHeight) { + subcompose(SubComposeID.DIVIDER) { + Box( + modifier = Modifier + .background(color = dividerColor) + .height(1.dp) + ) + }.forEach { + val height = 1.dp.toPx().toInt() + it.measure(Constraints.fixed(constraints.maxWidth, height)).placeRelative(0, maxItemHeight - height) + } + + subcompose(SubComposeID.INDICATOR) { + Box( + modifier = Modifier + .tabIndicator(tabPositions[selectedTabPosition], animationSpec) + .background(color = indicatorColor) + ) + }.forEach { + it.measure(Constraints.fixed(itemWidth, maxItemHeight)).placeRelative(0, 0) + } + + tabs.forEachIndexed { index, placeable -> + placeable.placeRelative(itemWidth * index, 0) + } + } + } +} + +@Composable +private fun Modifier.tabIndicator( + tabPosition: TabPosition, + animationSpec: AnimationSpec +): Modifier { + val animatedTabWidth by animateDpAsState( + targetValue = tabPosition.width, + animationSpec = animationSpec, + label = "" + ) + val animatedIndicatorOffset by animateDpAsState( + targetValue = tabPosition.x, + animationSpec = animationSpec, + label = "" + ) + + return this + .fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) + .offset(x = animatedIndicatorOffset) + .width(animatedTabWidth) + .height(2.dp) +} + +@Composable +fun DateRoadTabTitle( + title: String, + selected: Boolean, + selectedTextColor: Color = DateRoadTheme.colors.black, + unselectedTextColor: Color = DateRoadTheme.colors.gray300, + textStyle: TextStyle = DateRoadTheme.typography.bodyBold17, + position: Int, + padding: PaddingValues = PaddingValues(vertical = 15.dp), + onClick: (Int) -> Unit = {} +) { + Text( + text = title, + color = if (selected) selectedTextColor else unselectedTextColor, + style = textStyle, + modifier = + Modifier + .padding(paddingValues = padding) + .noRippleClickable( + onClick = { onClick(position) } + ), + textAlign = TextAlign.Center + ) +} + +@Preview +@Composable +fun DateRoadTabBarPreview() { + DATEROADTheme { + var selectedTabPosition by remember { mutableStateOf(0) } + + val items = listOf("획득 내역", "사용 내역") + + DateRoadTabBar( + selectedTabPosition = selectedTabPosition + ) { + items.forEachIndexed { index, title -> + DateRoadTabTitle( + title = title, + selected = index == selectedTabPosition, + position = index + ) { + selectedTabPosition = index + } + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadImageTag.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadImageTag.kt new file mode 100644 index 000000000..0847a5142 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadImageTag.kt @@ -0,0 +1,88 @@ +package org.sopt.dateroad.presentation.ui.component.tag + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@Composable +fun DateRoadImageTag( + modifier: Modifier = Modifier, + textContent: String, + @DrawableRes imageContent: Int, + spaceValue: Int = 5, + tagContentType: TagType +) { + DateRoadTag( + modifier = modifier, + tagType = tagContentType + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = imageContent), + contentDescription = null + ) + Spacer(modifier = Modifier.size(spaceValue.dp)) + Text( + text = textContent, + style = tagContentType.textStyle, + color = tagContentType.contentColor + ) + } + } +} + +@Preview +@Composable +fun DateRoadImageTagPreview() { + DATEROADTheme { + Column { + DateRoadImageTag( + textContent = "10만원 이상", + imageContent = R.drawable.ic_all_money_12, + tagContentType = TagType.MONEY + ) + DateRoadImageTag( + textContent = "5", + imageContent = R.drawable.ic_tag_heart, + tagContentType = TagType.HEART + ) + DateRoadImageTag( + textContent = "10시간", + imageContent = R.drawable.ic_all_clock_12, + tagContentType = TagType.TIME + ) + DateRoadImageTag( + textContent = stringResource(id = DateTagType.DRIVE.titleRes), + imageContent = DateTagType.DRIVE.imageRes, + tagContentType = TagType.MY_PAGE_DATE + ) + DateRoadImageTag( + textContent = stringResource(id = DateTagType.DRIVE.titleRes), + imageContent = DateTagType.DRIVE.imageRes, + tagContentType = TagType.PAST_DATE + ) + DateRoadImageTag( + textContent = stringResource(id = DateTagType.EXHIBITION_POPUP.titleRes), + imageContent = DateTagType.EXHIBITION_POPUP.imageRes, + tagContentType = TagType.TIMELINE_DATE_PINK + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadPointTag.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadPointTag.kt new file mode 100644 index 000000000..1fbdb95ee --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadPointTag.kt @@ -0,0 +1,90 @@ +package org.sopt.dateroad.presentation.ui.component.tag + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadPointTag( + modifier: Modifier = Modifier, + text: String, + profileImage: String? = null, + backgroundColor: Color = DateRoadTheme.colors.purple500, + contentColor: Color = DateRoadTheme.colors.white, + onClick: () -> Unit = {} +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .background(backgroundColor) + .noRippleClickable(onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = DateRoadTheme.typography.bodyBold13, + color = contentColor, + maxLines = 1, + modifier = modifier + .padding(start = 14.dp, end = 7.dp) + ) + if (profileImage.isNullOrEmpty()) { + Image( + modifier = Modifier + .width(33.dp) + .aspectRatio(1f) + .clip(CircleShape), + painter = painterResource(id = R.drawable.img_profile_small), + contentDescription = null, + contentScale = ContentScale.Crop + ) + } else { + AsyncImage( + modifier = Modifier + .width(33.dp) + .aspectRatio(1f) + .clip(CircleShape), + model = ImageRequest.Builder(context = LocalContext.current) + .data(profileImage) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + } +} + +@Preview +@Composable +fun DateRoadPointTagPreview() { + DATEROADTheme { + DateRoadPointTag( + text = "5000 P", + profileImage = null + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTag.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTag.kt new file mode 100644 index 000000000..a0d9e888c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTag.kt @@ -0,0 +1,27 @@ +package org.sopt.dateroad.presentation.ui.component.tag + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.TagType + +@Composable +fun DateRoadTag( + modifier: Modifier = Modifier, + tagType: TagType, + content: @Composable () -> Unit +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(tagType.roundedCornerShape.dp)) + .background(color = tagType.backgroundColor) + .padding(horizontal = tagType.paddingHorizontal.dp, vertical = tagType.paddingVertical.dp) + ) { + content() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTextTag.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTextTag.kt new file mode 100644 index 000000000..02356d1a4 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/tag/DateRoadTextTag.kt @@ -0,0 +1,64 @@ +package org.sopt.dateroad.presentation.ui.component.tag + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@Composable +fun DateRoadTextTag( + modifier: Modifier = Modifier, + textContent: String, + tagContentType: TagType +) { + DateRoadTag( + modifier = modifier, + tagType = tagContentType + ) { + Text( + text = textContent, + style = tagContentType.textStyle, + color = tagContentType.contentColor + ) + } +} + +@Preview +@Composable +fun DateRoadTextTagPreview() { + DATEROADTheme { + Column { + DateRoadTextTag( + textContent = "2시간", + tagContentType = TagType.PLACE_CARD_TIME + ) + DateRoadTextTag( + textContent = "에디터 픽", + tagContentType = TagType.ADVERTISEMENT_TITLE + ) + DateRoadTextTag( + textContent = "1/3", + tagContentType = TagType.COURSE_DETAIL_PHOTO_NUMBER + ) + DateRoadTextTag( + textContent = "1/5", + tagContentType = TagType.ENROLL_PHOTO_NUMBER + ) + DateRoadTextTag( + textContent = "1/5", + tagContentType = TagType.ADVERTISEMENT_PAGE_NUMBER + ) + DateRoadTextTag( + textContent = "1", + tagContentType = TagType.PLACE_CARD_NUMBER + ) + DateRoadTextTag( + textContent = "D-Day", + tagContentType = TagType.TIMELINE_D_DAY + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadBasicTextField.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadBasicTextField.kt new file mode 100644 index 000000000..164d68086 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadBasicTextField.kt @@ -0,0 +1,172 @@ +package org.sopt.dateroad.presentation.ui.component.textfield + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadBasicTextField( + modifier: Modifier = Modifier, + validateState: TextFieldValidateResult = TextFieldValidateResult.Basic, + title: String? = null, + placeholder: String = "", + @DrawableRes iconResourceId: Int? = null, + iconContentDescription: String = "", + successDescription: String = "", + errorDescription: String = "", + readOnly: Boolean = false, + value: String = "", + onClick: () -> Unit = {}, + visualTransformation: VisualTransformation = VisualTransformation.None, + onValueChange: (String) -> Unit = { _ -> }, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Default), + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column( + modifier = modifier + .fillMaxWidth() + ) { + title?.let { + Text( + text = it, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer( + modifier = Modifier.height(12.dp) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(14.dp)) + .border(width = 1.dp, color = if (validateState == TextFieldValidateResult.ValidationError) DateRoadTheme.colors.alertRed else Color.Transparent, shape = RoundedCornerShape(14.dp)) + .padding(horizontal = 15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (readOnly) { + Text( + modifier = Modifier + .weight(1f) + .padding(vertical = 16.dp) + .noRippleClickable(onClick = onClick), + text = value.ifEmpty { placeholder }, + color = if (value.isEmpty()) DateRoadTheme.colors.gray300 else DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } else { + BasicTextField( + modifier = Modifier + .weight(1f) + .padding(vertical = 16.dp) + .noRippleClickable(onClick = onClick), + value = value, + onValueChange = onValueChange, + cursorBrush = SolidColor(DateRoadTheme.colors.purple600), + singleLine = true, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, + textStyle = DateRoadTheme.typography.bodySemi13, + decorationBox = { innerTextField -> + innerTextField() + if (value.isEmpty()) { + Text( + text = placeholder, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + ) + } + iconResourceId?.let { + Spacer( + modifier = Modifier.padding(start = 14.dp) + ) + Icon(painter = painterResource(id = it), contentDescription = iconContentDescription, tint = DateRoadTheme.colors.gray200) + } + } + } + if (errorDescription.isNotEmpty() || successDescription.isNotEmpty()) { + Spacer(modifier = Modifier.height(1.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 9.dp), + text = when (validateState) { + is TextFieldValidateResult.Success -> successDescription + is TextFieldValidateResult.ValidationError -> errorDescription + else -> "" + }, + color = if (validateState == TextFieldValidateResult.ValidationError) DateRoadTheme.colors.alertRed else DateRoadTheme.colors.purple600, + style = DateRoadTheme.typography.capReg11 + ) + } +} + +@Preview +@Composable +fun DateRoadBasicTextFieldPreview() { + DATEROADTheme { + var text by remember { mutableStateOf("") } + var validationState by remember { mutableStateOf(TextFieldValidateResult.Basic) } + + fun validateTest(text: String) { + validationState = when { + text.isEmpty() -> TextFieldValidateResult.Basic + text.length < 5 -> TextFieldValidateResult.ValidationError + else -> TextFieldValidateResult.Success + } + } + + DateRoadBasicTextField( + validateState = validationState, + title = "타이틀", + placeholder = "힌트", + errorDescription = "최소 5글자 이상 입력해 주세요", + value = text, + onValueChange = { newValue -> + text = newValue + validateTest(text = newValue) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextArea.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextArea.kt new file mode 100644 index 000000000..bb49dfe59 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextArea.kt @@ -0,0 +1,108 @@ +package org.sopt.dateroad.presentation.ui.component.textfield + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadTextArea( + modifier: Modifier = Modifier, + title: String? = null, + placeholder: String = "", + minLength: Int = 200, + value: String = "", + visualTransformation: VisualTransformation = VisualTransformation.None, + onValueChange: (String) -> Unit = { _ -> }, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Default), + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column( + modifier = modifier + .fillMaxWidth() + ) { + title?.let { + Text( + text = it, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Start, + text = stringResource(id = R.string.text_area_length_content, value.length, minLength), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer( + modifier = Modifier.padding(top = 8.dp) + ) + } + BasicTextField( + modifier = Modifier + .fillMaxWidth() + .height(279.dp) + .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(14.dp)) + .padding(vertical = 16.dp, horizontal = 14.dp), + value = value, + onValueChange = onValueChange, + cursorBrush = SolidColor(DateRoadTheme.colors.purple500), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + textStyle = DateRoadTheme.typography.bodyMed13Context, + decorationBox = { innerTextField -> + innerTextField() + if (value.isEmpty()) { + Text( + text = placeholder, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodyMed13Context + ) + } + } + ) + Spacer(modifier = Modifier.height(6.dp)) + } +} + +@Preview +@Composable +fun DateRoadBasicTextAreaPreview() { + DATEROADTheme { + var text by remember { mutableStateOf("") } + + DateRoadTextArea( + title = "코스에 대한 설명을 적어주세요", + placeholder = "데이트 내용을 입력해 주세요\n예약 정보, 웨이팅 정보, 꿀팁 등을 작성해 주세요.\n(최소 200자)", + value = text, + onValueChange = { newValue -> + text = newValue + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextFieldWithButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextFieldWithButton.kt new file mode 100644 index 000000000..bf7289e18 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/DateRoadTextFieldWithButton.kt @@ -0,0 +1,183 @@ +package org.sopt.dateroad.presentation.ui.component.textfield + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadTextFieldWithButton( + modifier: Modifier = Modifier, + validateState: TextFieldValidateResult = TextFieldValidateResult.Basic, + title: String? = null, + titleDescription: String? = null, + placeholder: String = "", + successDescription: String = "", + validationErrorDescription: String = "", + conflictErrorDescription: String = "", + buttonText: String, + isButtonEnabled: Boolean, + maxLength: Int = 5, + value: String = "", + visualTransformation: VisualTransformation = VisualTransformation.None, + onValueChange: (String) -> Unit = { _ -> }, + onButtonClick: () -> Unit = {}, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Default), + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column( + modifier = modifier + .fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + title?.let { title -> + Text( + text = title, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold15 + ) + } + titleDescription?.let { titleDescription -> + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = titleDescription, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodyMed13 + ) + } + } + Spacer( + modifier = Modifier.padding(top = 10.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(14.dp)) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + modifier = Modifier + .weight(1f) + .padding(vertical = 16.dp), + value = value, + onValueChange = { + if (it.length <= maxLength) onValueChange(it) + }, + cursorBrush = SolidColor(DateRoadTheme.colors.purple600), + singleLine = true, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, + textStyle = DateRoadTheme.typography.bodySemi15, + decorationBox = { innerTextField -> + innerTextField() + if (value.isEmpty()) { + Text( + text = placeholder, + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.bodySemi15 + ) + } + } + ) + Spacer(modifier = Modifier.width(12.dp)) + DateRoadFilledButton( + isEnabled = isButtonEnabled, + textContent = buttonText, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 10.dp, + paddingHorizontal = 14.dp, + paddingVertical = 6.dp, + onClick = onButtonClick + ) + } + Spacer(modifier = Modifier.height(7.dp)) + Row { + Text( + modifier = Modifier + .weight(1f), + text = when (validateState) { + is TextFieldValidateResult.Success -> successDescription + is TextFieldValidateResult.ValidationError -> validationErrorDescription + is TextFieldValidateResult.ConflictError -> conflictErrorDescription + else -> "" + }, + color = if (validateState == TextFieldValidateResult.Success) DateRoadTheme.colors.purple600 else DateRoadTheme.colors.alertRed, + style = DateRoadTheme.typography.capReg11 + ) + Text( + text = stringResource(id = R.string.fraction_format, value.length, maxLength), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.capReg11 + ) + } + } +} + +@Preview +@Composable +fun DateRoadTextFieldWithButtonPreview() { + DATEROADTheme { + var text by remember { mutableStateOf("") } + var validationState by remember { mutableStateOf(TextFieldValidateResult.Basic) } + var isButtonEnabled by remember { mutableStateOf(false) } + + fun validateTest(text: String) { + validationState = when { + text.isEmpty() -> TextFieldValidateResult.Basic + text.length < 2 -> TextFieldValidateResult.ValidationError + else -> TextFieldValidateResult.Success + } + } + + DateRoadTextFieldWithButton( + validateState = validationState, + title = "닉네임", + titleDescription = "(한글, 영문, 숫자만 가능)", + placeholder = "닉네임을 입력해 주세요", + successDescription = "사용가능한 닉네임입니다.", + validationErrorDescription = "최소 2글자를 입력해주세요.", + conflictErrorDescription = "이미 사용중인 닉네임입니다.", + buttonText = "중복확인", + isButtonEnabled = isButtonEnabled, + value = text, + onValueChange = { newValue -> + text = newValue + validateTest(text = newValue) + isButtonEnabled = text.isNotEmpty() + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/model/TextFieldValidateResult.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/model/TextFieldValidateResult.kt new file mode 100644 index 000000000..153372e1f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/textfield/model/TextFieldValidateResult.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.presentation.ui.component.textfield.model + +sealed class TextFieldValidateResult { + data object Basic : TextFieldValidateResult() + data object ValidationError : TextFieldValidateResult() + data object ConflictError : TextFieldValidateResult() + data object Success : TextFieldValidateResult() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadBasicTopBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadBasicTopBar.kt new file mode 100644 index 000000000..c81f763f9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadBasicTopBar.kt @@ -0,0 +1,142 @@ +package org.sopt.dateroad.presentation.ui.component.topbar + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadBasicTopBar( + title: String = "", + backGroundColor: Color = Color.Transparent, + onLeftIconClick: () -> Unit = {}, + @DrawableRes leftIconResource: Int? = null, + buttonContent: (@Composable () -> Unit)? = null, + leftIconTint: Color = DateRoadTheme.colors.black +) { + var iconWidth by remember { mutableStateOf(0) } + var contentWidth by remember { mutableStateOf(0) } + var paddingValue = maxOf(iconWidth, contentWidth) + LaunchedEffect(iconWidth, contentWidth) { + paddingValue = maxOf(iconWidth, contentWidth) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .background(backGroundColor) + .padding(vertical = 11.dp) + ) { + if (leftIconResource != null) { + Column( + Modifier + .align(Alignment.CenterStart) + .onGloballyPositioned { coordinates -> + iconWidth = coordinates.size.width + } + .noRippleClickable(onClick = onLeftIconClick) + ) { + Icon( + painter = painterResource(id = leftIconResource), + contentDescription = null, + tint = leftIconTint, + modifier = Modifier + .padding(16.dp) + ) + } + } + + if (buttonContent != null) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .onGloballyPositioned { coordinates -> + contentWidth = coordinates.size.width + } + ) { + buttonContent() + } + } + + Text( + text = title, + style = DateRoadTheme.typography.titleBold18, + color = DateRoadTheme.colors.black, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center) + .padding(horizontal = paddingValue.dp / 2) + ) + } +} + +@Preview +@Composable +fun DateRoadBasicTopBarPreview() { + Column { + DateRoadBasicTopBar( + title = "포인트 내역포인트 내역포인트내역내역포인트포인트내역포인트 내역", + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white + ) + DateRoadBasicTopBar( + title = "내 프로필", + backGroundColor = DateRoadTheme.colors.white + ) + DateRoadBasicTopBar( + title = "데이트 일정", + leftIconResource = R.drawable.ic_top_bar_back_white, + buttonContent = { + Icon( + painterResource(id = R.drawable.ic_top_bar_share), + contentDescription = null, + tint = DateRoadTheme.colors.black + ) + } + ) + DateRoadBasicTopBar( + title = "데이트 일정데이트 일정데이트 일정데이트 일정일정데이트 일정데이트 일정", + leftIconResource = R.drawable.ic_top_bar_back_white, + buttonContent = { + DateRoadFilledButton( + isEnabled = true, + textContent = "불러오기", + onClick = {}, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 20.dp, + paddingHorizontal = 10.dp, + paddingVertical = 5.dp + ) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadLeftTitleTopBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadLeftTitleTopBar.kt new file mode 100644 index 000000000..b5208f8f2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadLeftTitleTopBar.kt @@ -0,0 +1,76 @@ +package org.sopt.dateroad.presentation.ui.component.topbar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadLeftTitleTopBar( + title: String, + buttonContent: (@Composable () -> Unit)? = null +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(vertical = 13.dp, horizontal = 16.dp) + ) { + Text( + text = title, + style = DateRoadTheme.typography.titleBold20, + color = DateRoadTheme.colors.black, + textAlign = TextAlign.Center + ) + if (buttonContent != null) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + ) { + buttonContent() + } + } + } +} + +@Preview +@Composable +fun DateRoadLeftTitlePreview() { + Column { + DateRoadLeftTitleTopBar( + title = "코스 둘러보기", + buttonContent = { + DateRoadImageButton( + isEnabled = true, + onClick = {}, + cornerRadius = 14.dp, + paddingHorizontal = 16.dp, + paddingVertical = 8.dp + ) + } + ) + DateRoadLeftTitleTopBar( + title = "데이트 일정", + buttonContent = { + DateRoadImageButton( + isEnabled = true, + onClick = {}, + cornerRadius = 14.dp, + paddingHorizontal = 16.dp, + paddingVertical = 8.dp + ) + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadScrollResponsiveTopBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadScrollResponsiveTopBar.kt new file mode 100644 index 000000000..afdd33eca --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/topbar/DateRoadScrollResponsiveTopBar.kt @@ -0,0 +1,61 @@ +package org.sopt.dateroad.presentation.ui.component.topbar + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadScrollResponsiveTopBar( + isDefault: Boolean, + defaultBackgroundColor: Color = Color.Transparent, + scrolledBackgroundColor: Color = DateRoadTheme.colors.white, + defaultIconTintColor: Color = DateRoadTheme.colors.white, + scrolledIconTintColor: Color = DateRoadTheme.colors.black, + @DrawableRes leftIconResource: Int = R.drawable.ic_top_bar_back_white, + onLeftIconClick: () -> Unit = {}, + @DrawableRes rightIconResource: Int? = null, + onRightIconClick: () -> Unit = {} +) { + Box { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + DateRoadTheme.colors.black.copy(alpha = 0.6f), + Color.Transparent + ) + ) + ) + .matchParentSize() + ) + + DateRoadBasicTopBar( + backGroundColor = if (isDefault) defaultBackgroundColor else scrolledBackgroundColor, + leftIconResource = leftIconResource, + onLeftIconClick = onLeftIconClick, + leftIconTint = if (isDefault) defaultIconTintColor else scrolledIconTintColor, + buttonContent = { + if (rightIconResource != null) { + Icon( + painter = painterResource(id = rightIconResource), + contentDescription = null, + tint = if (isDefault) defaultIconTintColor else scrolledIconTintColor, + modifier = Modifier.noRippleClickable(onRightIconClick) + ) + } + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DataRoadIdeView.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DataRoadIdeView.kt new file mode 100644 index 000000000..aa87d367a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DataRoadIdeView.kt @@ -0,0 +1,19 @@ +package org.sopt.dateroad.presentation.ui.component.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadIdleView( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + .background(color = DateRoadTheme.colors.white) + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadEmptyView.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadEmptyView.kt new file mode 100644 index 000000000..8238daa2b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadEmptyView.kt @@ -0,0 +1,56 @@ +package org.sopt.dateroad.presentation.ui.component.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadEmptyView( + modifier: Modifier = Modifier, + emptyViewType: EmptyViewType +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResource(id = emptyViewType.imageRes), + contentDescription = null, + contentScale = ContentScale.FillWidth + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResource(id = emptyViewType.titleRes), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.titleBold18, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +fun DateRoadEmptyViewPreview() { + DATEROADTheme { + DateRoadEmptyView(emptyViewType = EmptyViewType.LOOK) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadErrorView.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadErrorView.kt new file mode 100644 index 000000000..e01d78e6c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadErrorView.kt @@ -0,0 +1,71 @@ +package org.sopt.dateroad.presentation.ui.component.view + +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadErrorView() { + val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + Column( + modifier = Modifier + .fillMaxSize() + .background(color = DateRoadTheme.colors.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DateRoadBasicTopBar( + title = "", + onLeftIconClick = { backDispatcher?.onBackPressed() }, + leftIconResource = R.drawable.ic_top_bar_back_white + ) + Spacer(modifier = Modifier.weight(131f)) + Image( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(id = R.drawable.img_error_server), + contentDescription = null + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier + .fillMaxWidth(), + text = stringResource(id = R.string.error_view_server_title), + color = DateRoadTheme.colors.gray500, + style = DateRoadTheme.typography.titleExtra20, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier + .fillMaxWidth(), + text = stringResource(id = R.string.error_view_server_description), + color = DateRoadTheme.colors.gray500, + style = DateRoadTheme.typography.bodyMed15, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.weight(186f)) + } +} + +@Preview +@Composable +fun ErrorPreview() { + DateRoadErrorView() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadLoadingView.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadLoadingView.kt new file mode 100644 index 000000000..a44f24255 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadLoadingView.kt @@ -0,0 +1,59 @@ +package org.sopt.dateroad.presentation.ui.component.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieClipSpec +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieAnimatable +import com.airbnb.lottie.compose.rememberLottieComposition +import org.sopt.dateroad.presentation.util.LoadingView.CLIPMAX +import org.sopt.dateroad.presentation.util.LoadingView.CLIPMIN +import org.sopt.dateroad.presentation.util.LoadingView.LOTTIE +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadLoadingView() { + val composition by rememberLottieComposition( + LottieCompositionSpec.Asset(LOTTIE) + ) + val lottieAnimatable = rememberLottieAnimatable() + + LaunchedEffect(composition) { + lottieAnimatable.animate( + composition = composition, + clipSpec = LottieClipSpec.Frame(CLIPMIN, CLIPMAX), + initialProgress = 0f, + iteration = LottieConstants.IterateForever + ) + } + + Box( + modifier = Modifier.fillMaxSize().background(color = DateRoadTheme.colors.white), + contentAlignment = Alignment.Center + ) { + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + modifier = Modifier + .fillMaxSize() + .align(Alignment.BottomCenter), + contentScale = ContentScale.FillWidth + ) + } +} + +@Preview +@Composable +fun DateLoadLoadingViewPreview() { + DateRoadLoadingView() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadWebView.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadWebView.kt new file mode 100644 index 000000000..3364e6d20 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/component/view/DateRoadWebView.kt @@ -0,0 +1,49 @@ +package org.sopt.dateroad.presentation.ui.component.view + +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +@Composable +fun DateRoadWebView(url: String, onClose: () -> Unit) { + val webViewState = rememberWebViewState(url) + var webView: android.webkit.WebView? by remember { mutableStateOf(null) } + + BackHandler(enabled = true) { + if (webView?.canGoBack() == true) { + webView?.goBack() + } else { + onClose() + } + } + + Column(modifier = Modifier.fillMaxSize()) { + WebView( + state = webViewState, + modifier = Modifier.weight(1f), + onCreated = { webViewInstance -> + with(webViewInstance) { + settings.run { + javaScriptEnabled = true + domStorageEnabled = true + javaScriptCanOpenWindowsAutomatically = false + } + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: android.webkit.WebView?, url: String?) { + super.onPageFinished(view, url) + } + } + } + webView = webViewInstance + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailContract.kt new file mode 100644 index 000000000..b7dc018d2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailContract.kt @@ -0,0 +1,58 @@ +package org.sopt.dateroad.presentation.ui.coursedetail + +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class CourseDetailContract { + data class CourseDetailUiState( + val loadState: LoadState = LoadState.Idle, + val isDeleteCourseBottomSheetOpen: Boolean = false, + val isRegionBottomSheetOpen: Boolean = false, + val isReportCourseBottomSheetOpen: Boolean = false, + val isPointReadDialogOpen: Boolean = false, + val isPointLackDialogOpen: Boolean = false, + val isFreeReadDialogOpen: Boolean = false, + val isDeleteCourseDialogOpen: Boolean = false, + val isReportCourseDialogOpen: Boolean = false, + val isLikedButtonChecked: Boolean = false, + val courseDetail: CourseDetail = CourseDetail(), + val currentImagePage: Int = 0, + val usePointLoadState: LoadState = LoadState.Idle, + val deleteLoadState: LoadState = LoadState.Idle, + var isWebViewOpened: Boolean = false + ) : UiState + + sealed interface CourseDetailSideEffect : UiSideEffect { + data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : CourseDetailSideEffect + data object PopBackStack : CourseDetailSideEffect + } + + sealed class CourseDetailEvent : UiEvent { + data object OnLikeButtonClicked : CourseDetailEvent() + data object OnDialogLookedByPoint : CourseDetailEvent() + data object DismissDialogLookedByPoint : CourseDetailEvent() + data object OnDialogLookedForFree : CourseDetailEvent() + data object DismissDialogLookedForFree : CourseDetailEvent() + data object OnDialogPointLack : CourseDetailEvent() + data object DismissDialogPointLack : CourseDetailEvent() + data object OnDialogDeleteCourse : CourseDetailEvent() + data object DismissDialogDeleteCourse : CourseDetailEvent() + data object OnDialogReportCourse : CourseDetailEvent() + data object DismissDialogReportCourse : CourseDetailEvent() + data object OnDeleteCourseBottomSheet : CourseDetailEvent() + data object DismissDeleteCourseBottomSheet : CourseDetailEvent() + data object OnReportCourseBottomSheet : CourseDetailEvent() + data object DismissReportCourseBottomSheet : CourseDetailEvent() + data class FetchCourseDetail(val loadState: LoadState, val courseDetail: CourseDetail) : CourseDetailEvent() + data class PostUsePoint(val usePointLoadState: LoadState, val isAccess: Boolean) : CourseDetailEvent() + data class DeleteCourseLike(val courseDetail: CourseDetail) : CourseDetailEvent() + data class PostCourseLike(val courseDetail: CourseDetail) : CourseDetailEvent() + data class DeleteCourse(val deleteLoadState: LoadState) : CourseDetailEvent() + data object OnReportWebViewClicked : CourseDetailEvent() + data object DismissReportWebView : CourseDetailEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailScreen.kt new file mode 100644 index 000000000..351966d45 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailScreen.kt @@ -0,0 +1,426 @@ +package org.sopt.dateroad.presentation.ui.coursedetail + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.PagerState +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.presentation.type.CourseDetailUnopenedDetailType +import org.sopt.dateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.TwoButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription +import org.sopt.dateroad.presentation.ui.component.pager.DateRoadImagePager +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadScrollResponsiveTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadWebView +import org.sopt.dateroad.presentation.ui.coursedetail.component.CourseDetailBasicInfo +import org.sopt.dateroad.presentation.ui.coursedetail.component.CourseDetailBottomBar +import org.sopt.dateroad.presentation.ui.coursedetail.component.CourseDetailUnopenedDetail +import org.sopt.dateroad.presentation.ui.coursedetail.component.courseDetailOpenedDetail +import org.sopt.dateroad.presentation.util.ViewPath.COURSE_DETAIL +import org.sopt.dateroad.presentation.util.WebViewUrl.REPORT_URL +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailRoute( + viewModel: CourseDetailViewModel = hiltViewModel(), + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + courseId: Int +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { courseDetailSideEffect -> + when (courseDetailSideEffect) { + is CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll -> navigateToEnroll(courseDetailSideEffect.enrollType, courseDetailSideEffect.viewPath, courseDetailSideEffect.id) + is CourseDetailContract.CourseDetailSideEffect.PopBackStack -> popBackStack() + } + } + } + + LaunchedEffect(Unit) { + viewModel.fetchCourseDetail(courseId = courseId) + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + CourseDetailScreen( + courseDetailUiState = uiState, + onDialogPointLack = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogPointLack) }, + onDialogPointLackConfirm = { + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) + viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = COURSE_DETAIL, id = null)) + }, + dismissDialogPointLack = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogPointLack) }, + onDialogLookedForFree = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogLookedForFree) }, + dismissDialogLookedForFree = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogLookedForFree) }, + onDialogLookedByPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogLookedByPoint) }, + onDialogDeleteCourse = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogDeleteCourse) }, + onDialogReportCourse = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDialogReportCourse) }, + dismissDialogDeleteCourse = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogDeleteCourse) }, + dismissDialogReportCourse = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogReportCourse) }, + dismissDialogLookedByPoint = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDialogLookedByPoint) }, + onLikeButtonClicked = { + when (uiState.courseDetail.isUserLiked) { + true -> viewModel.deleteCourseLike(courseId = courseId) + false -> viewModel.postCourseLike(courseId = courseId) + } + }, + onDeleteButtonClicked = { + viewModel.deleteCourse(courseId = courseId) + }, + onDeleteCourseBottomSheet = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnDeleteCourseBottomSheet) }, + dismissDeleteCourseBottomSheet = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissDeleteCourseBottomSheet) }, + onReportCourseBottomSheet = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportCourseBottomSheet) }, + dismissReportCourseBottomSheet = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportCourseBottomSheet) }, + enrollSchedule = { viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.NavigateToEnroll(enrollType = EnrollType.TIMELINE, viewPath = COURSE_DETAIL, id = courseId)) }, + onTopBarIconClicked = { viewModel.setSideEffect(CourseDetailContract.CourseDetailSideEffect.PopBackStack) }, + openCourseDetail = { viewModel.postUsePoint(courseId = courseId) }, + onReportButtonClicked = { + viewModel.setEvent(CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked) + }, + onReportWebViewClose = { viewModel.setEvent(CourseDetailContract.CourseDetailEvent.DismissReportWebView) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } + + when (uiState.usePointLoadState) { + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Error -> DateRoadErrorView() + + else -> Unit + } + + when (uiState.deleteLoadState) { + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> popBackStack() + + LoadState.Error -> DateRoadErrorView() + + else -> Unit + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun CourseDetailScreen( + courseDetailUiState: CourseDetailContract.CourseDetailUiState, + onDialogPointLack: () -> Unit, + onDialogPointLackConfirm: () -> Unit, + dismissDialogPointLack: () -> Unit, + onDialogLookedForFree: () -> Unit, + dismissDialogLookedForFree: () -> Unit, + onDialogLookedByPoint: () -> Unit, + dismissDialogLookedByPoint: () -> Unit, + onDialogDeleteCourse: () -> Unit, + dismissDialogDeleteCourse: () -> Unit, + onDialogReportCourse: () -> Unit, + dismissDialogReportCourse: () -> Unit, + onLikeButtonClicked: () -> Unit, + onDeleteButtonClicked: () -> Unit, + onDeleteCourseBottomSheet: () -> Unit, + dismissDeleteCourseBottomSheet: () -> Unit, + onReportCourseBottomSheet: () -> Unit, + onReportButtonClicked: () -> Unit, + onReportWebViewClose: () -> Unit, + dismissReportCourseBottomSheet: () -> Unit, + enrollSchedule: () -> Unit, + onTopBarIconClicked: () -> Unit, + openCourseDetail: () -> Unit +) { + var imageHeight by remember { mutableIntStateOf(0) } + + val scrollState = rememberLazyListState() + val isScrollResponsiveDefault by remember { + derivedStateOf { + scrollState.firstVisibleItemIndex == 0 && scrollState.firstVisibleItemScrollOffset < imageHeight + } + } + + val isViewable = courseDetailUiState.courseDetail.isAccess || courseDetailUiState.courseDetail.isCourseMine + val courseDetailUnopenedType = if (courseDetailUiState.courseDetail.free > 0) CourseDetailUnopenedDetailType.FREE else CourseDetailUnopenedDetailType.POINT + + if (courseDetailUiState.isWebViewOpened) { + DateRoadWebView(url = REPORT_URL, onClose = onReportWebViewClose) + } else { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = scrollState, + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + with(courseDetailUiState.courseDetail) { + item { + DateRoadImagePager( + modifier = Modifier + .onGloballyPositioned { coordinates -> + imageHeight = coordinates.size.height + }, + pagerState = PagerState(), + images = courseDetailUiState.courseDetail.images, + userScrollEnabled = isViewable, + like = courseDetailUiState.courseDetail.like.toString() + ) + } + + item { + CourseDetailBasicInfo( + date = date, + title = title, + totalTime = totalTime, + totalCostTag = totalCostTag, + city = city + ) + } + + when (isViewable) { + true -> { + courseDetailOpenedDetail( + description = description, + startAt = startAt, + places = places, + totalCost = totalCost, + tags = tags.mapNotNull { tag -> tag.getDateTagTypeByName() } + ) + if (!isCourseMine) { + item { + Spacer(modifier = Modifier.height(86.dp)) + } + } + } + + false -> { + item { + CourseDetailUnopenedDetail( + text = description, + free = free, + courseDetailUnopenedDetailType = courseDetailUnopenedType, + onButtonClicked = when (courseDetailUnopenedType) { + CourseDetailUnopenedDetailType.FREE -> onDialogLookedForFree + CourseDetailUnopenedDetailType.POINT -> onDialogLookedByPoint + } + ) + } + } + } + } + } + + DateRoadScrollResponsiveTopBar( + isDefault = isScrollResponsiveDefault, + onLeftIconClick = onTopBarIconClicked, + onRightIconClick = if (courseDetailUiState.courseDetail.isCourseMine) onDeleteCourseBottomSheet else onReportCourseBottomSheet, + rightIconResource = if (isViewable) R.drawable.btn_course_detail_more_white else null + ) + + if (isViewable && !courseDetailUiState.courseDetail.isCourseMine) { + CourseDetailBottomBar( + modifier = Modifier.align(Alignment.BottomCenter), + isUserLiked = courseDetailUiState.courseDetail.isUserLiked, + onLikeButtonClicked = onLikeButtonClicked, + onEnrollButtonClicked = enrollSchedule + ) + } + + if (courseDetailUiState.isPointReadDialogOpen) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.READ_COURSE, + onDismissRequest = { dismissDialogLookedByPoint() }, + onClickConfirm = { + dismissDialogLookedByPoint() + if (courseDetailUiState.courseDetail.totalPoint < 50) { + onDialogPointLack() + } else { + openCourseDetail() + } + }, + onClickDismiss = { dismissDialogLookedByPoint() } + ) + } + + if (courseDetailUiState.isPointLackDialogOpen) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.POINT_LACK, + onDismissRequest = dismissDialogPointLack, + onClickConfirm = onDialogPointLackConfirm, + onClickDismiss = dismissDialogPointLack + ) + } + + if (courseDetailUiState.isFreeReadDialogOpen) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.FREE_READ, + onDismissRequest = { dismissDialogLookedForFree() }, + onClickConfirm = { + dismissDialogLookedForFree() + openCourseDetail() + }, + onClickDismiss = { dismissDialogLookedForFree() } + ) + } + + if (courseDetailUiState.isDeleteCourseDialogOpen) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.DELETE_COURSE, + onDismissRequest = { dismissDialogDeleteCourse() }, + onClickConfirm = { + dismissDialogDeleteCourse() + onDeleteButtonClicked() + }, + onClickDismiss = { dismissDialogDeleteCourse() } + ) + } + + if (courseDetailUiState.isReportCourseDialogOpen) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.REPORT_COURSE, + onDismissRequest = { dismissDialogReportCourse() }, + onClickConfirm = { + dismissDialogReportCourse() + onReportButtonClicked() + }, + onClickDismiss = { dismissDialogReportCourse() } + ) + } + + DateRoadBasicBottomSheet( + isBottomSheetOpen = courseDetailUiState.isDeleteCourseBottomSheetOpen, + title = stringResource(id = R.string.course_detail_bottom_sheet_title), + isButtonEnabled = false, + buttonText = stringResource(id = R.string.course_detail_bottom_sheet_delete), + itemList = listOf( + stringResource(id = R.string.course_detail_bottom_sheet_confirm) to { + onDialogDeleteCourse() + } + ), + onDismissRequest = { dismissDeleteCourseBottomSheet() }, + onButtonClick = { + dismissDeleteCourseBottomSheet() + } + ) + + DateRoadBasicBottomSheet( + isBottomSheetOpen = courseDetailUiState.isReportCourseBottomSheetOpen, + title = stringResource(id = R.string.course_detail_bottom_sheet_title), + isButtonEnabled = false, + buttonText = stringResource(id = R.string.course_detail_bottom_sheet_delete), + itemList = listOf( + stringResource(id = R.string.course_detail_bottom_sheet_report) to { + onDialogReportCourse() + } + ), + onDismissRequest = { dismissReportCourseBottomSheet() }, + onButtonClick = { + dismissReportCourseBottomSheet() + } + ) + } + } +} + +@Preview +@Composable +fun CourseDetailScreenPreview() { + DATEROADTheme { + val dummyCourseDetail = CourseDetailContract.CourseDetailUiState( + loadState = LoadState.Success, + courseDetail = CourseDetail( + courseId = 1, + title = "Sample Course", + description = "This is a sample course description.", + totalTime = "4 hours", + totalCost = "$100", + city = "Seoul", + images = listOf( + "https://via.placeholder.com/300", + "https://via.placeholder.com/300" + ), + tags = listOf("TAG1", "TAG2"), + places = listOf( + Place( + title = "Place 1", + duration = "1" + ), + Place( + title = "Place 2", + duration = "2" + ) + ), + isCourseMine = true, + isAccess = true, + isUserLiked = false, + like = 10, + free = 2, + totalPoint = 30 + ) + ) + + CourseDetailScreen( + courseDetailUiState = dummyCourseDetail, + onDialogPointLack = {}, + dismissDialogPointLack = {}, + onDialogPointLackConfirm = {}, + onDialogLookedForFree = {}, + dismissDialogLookedForFree = {}, + onDialogLookedByPoint = {}, + dismissDialogLookedByPoint = {}, + onLikeButtonClicked = {}, + onDeleteButtonClicked = {}, + onDeleteCourseBottomSheet = {}, + dismissDeleteCourseBottomSheet = {}, + onReportCourseBottomSheet = {}, + dismissReportCourseBottomSheet = {}, + enrollSchedule = {}, + onTopBarIconClicked = {}, + openCourseDetail = {}, + onReportButtonClicked = {}, + onReportWebViewClose = {}, + onDialogDeleteCourse = {}, + onDialogReportCourse = {}, + dismissDialogDeleteCourse = {}, + dismissDialogReportCourse = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt new file mode 100644 index 000000000..ecb9a3cf5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/CourseDetailViewModel.kt @@ -0,0 +1,116 @@ +package org.sopt.dateroad.presentation.ui.coursedetail + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.model.UsePoint +import org.sopt.dateroad.domain.usecase.DeleteCourseLikeUseCase +import org.sopt.dateroad.domain.usecase.DeleteCourseUseCase +import org.sopt.dateroad.domain.usecase.GetCourseDetailUseCase +import org.sopt.dateroad.domain.usecase.PostCourseLikeUseCase +import org.sopt.dateroad.domain.usecase.PostUsePointUseCase +import org.sopt.dateroad.presentation.util.Point +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class CourseDetailViewModel @Inject constructor( + private val deleteCourseUseCase: DeleteCourseUseCase, + private val deleteCourseLikeUseCase: DeleteCourseLikeUseCase, + private val getCourseDetailUseCase: GetCourseDetailUseCase, + private val postCourseLikeUseCase: PostCourseLikeUseCase, + private val postUsePointUseCase: PostUsePointUseCase +) : BaseViewModel() { + override fun createInitialState(): CourseDetailContract.CourseDetailUiState = CourseDetailContract.CourseDetailUiState() + + override suspend fun handleEvent(event: CourseDetailContract.CourseDetailEvent) { + when (event) { + is CourseDetailContract.CourseDetailEvent.OnDialogPointLack -> setState { copy(isPointLackDialogOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogPointLack -> setState { copy(isPointLackDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnDialogLookedForFree -> setState { copy(isFreeReadDialogOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogLookedForFree -> setState { copy(isFreeReadDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnDialogDeleteCourse -> setState { copy(isDeleteCourseDialogOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogDeleteCourse -> setState { copy(isDeleteCourseDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnDialogReportCourse -> setState { copy(isReportCourseDialogOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogReportCourse -> setState { copy(isReportCourseDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnDialogLookedByPoint -> setState { copy(isPointReadDialogOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDialogLookedByPoint -> setState { copy(isPointReadDialogOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnLikeButtonClicked -> setState { copy(isLikedButtonChecked = !isLikedButtonChecked) } + is CourseDetailContract.CourseDetailEvent.OnDeleteCourseBottomSheet -> setState { copy(isDeleteCourseBottomSheetOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissDeleteCourseBottomSheet -> setState { copy(isDeleteCourseBottomSheetOpen = false) } + is CourseDetailContract.CourseDetailEvent.OnReportCourseBottomSheet -> setState { copy(isReportCourseBottomSheetOpen = true) } + is CourseDetailContract.CourseDetailEvent.DismissReportCourseBottomSheet -> setState { copy(isReportCourseBottomSheetOpen = false) } + is CourseDetailContract.CourseDetailEvent.FetchCourseDetail -> setState { copy(loadState = event.loadState, courseDetail = event.courseDetail) } + is CourseDetailContract.CourseDetailEvent.DeleteCourseLike -> setState { copy(courseDetail = event.courseDetail) } + is CourseDetailContract.CourseDetailEvent.PostCourseLike -> setState { copy(courseDetail = event.courseDetail) } + is CourseDetailContract.CourseDetailEvent.DeleteCourse -> setState { copy(deleteLoadState = event.deleteLoadState) } + is CourseDetailContract.CourseDetailEvent.PostUsePoint -> setState { copy(usePointLoadState = event.usePointLoadState, courseDetail = courseDetail.copy(isAccess = event.isAccess)) } + is CourseDetailContract.CourseDetailEvent.OnReportWebViewClicked -> setState { copy(isWebViewOpened = true) } + is CourseDetailContract.CourseDetailEvent.DismissReportWebView -> setState { copy(isWebViewOpened = false) } + } + } + + fun deleteCourseLike(courseId: Int) { + viewModelScope.launch { + setEvent(CourseDetailContract.CourseDetailEvent.DeleteCourseLike(courseDetail = currentState.courseDetail)) + deleteCourseLikeUseCase(courseId = courseId).onSuccess { + setEvent(CourseDetailContract.CourseDetailEvent.DeleteCourseLike(courseDetail = currentState.courseDetail.copy(isUserLiked = false, like = currentState.courseDetail.like - 1))) + }.onFailure { + setEvent(CourseDetailContract.CourseDetailEvent.DeleteCourseLike(courseDetail = currentState.courseDetail)) + } + } + } + + fun fetchCourseDetail(courseId: Int) { + viewModelScope.launch { + setEvent(CourseDetailContract.CourseDetailEvent.FetchCourseDetail(loadState = LoadState.Loading, courseDetail = currentState.courseDetail)) + getCourseDetailUseCase(courseId = courseId).onSuccess { courseDetail -> + setEvent(CourseDetailContract.CourseDetailEvent.FetchCourseDetail(loadState = LoadState.Success, courseDetail = courseDetail)) + }.onFailure { + setEvent(CourseDetailContract.CourseDetailEvent.FetchCourseDetail(loadState = LoadState.Error, courseDetail = currentState.courseDetail)) + } + } + } + + fun postCourseLike(courseId: Int) { + viewModelScope.launch { + setEvent( + CourseDetailContract.CourseDetailEvent.PostCourseLike(courseDetail = currentState.courseDetail) + ) + postCourseLikeUseCase(courseId = courseId).onSuccess { + setEvent(CourseDetailContract.CourseDetailEvent.PostCourseLike(courseDetail = currentState.courseDetail.copy(isUserLiked = true, like = currentState.courseDetail.like + 1))) + }.onFailure { + setEvent(CourseDetailContract.CourseDetailEvent.PostCourseLike(courseDetail = currentState.courseDetail)) + } + } + } + + fun postUsePoint(courseId: Int) { + viewModelScope.launch { + setEvent(CourseDetailContract.CourseDetailEvent.PostUsePoint(usePointLoadState = LoadState.Loading, isAccess = currentState.courseDetail.isAccess)) + postUsePointUseCase(courseId = courseId, usePoint = UsePoint(Point.POINT, Point.POINT_USED, Point.POINT_USED_DESCRIPTION)).onSuccess { + setEvent(CourseDetailContract.CourseDetailEvent.PostUsePoint(usePointLoadState = LoadState.Success, isAccess = true)) + }.onFailure { + setEvent(CourseDetailContract.CourseDetailEvent.PostUsePoint(usePointLoadState = LoadState.Error, isAccess = currentState.courseDetail.isAccess)) + } + } + } + + fun deleteCourse(courseId: Int) { + viewModelScope.launch { + setEvent( + CourseDetailContract.CourseDetailEvent.DeleteCourse(deleteLoadState = LoadState.Loading) + ) + deleteCourseUseCase(courseId = courseId).onSuccess { + setEvent( + CourseDetailContract.CourseDetailEvent.DeleteCourse(deleteLoadState = LoadState.Success) + ) + }.onFailure { + setEvent( + CourseDetailContract.CourseDetailEvent.DeleteCourse(deleteLoadState = LoadState.Error) + ) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBasicInfo.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBasicInfo.kt new file mode 100644 index 000000000..adb335cc3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBasicInfo.kt @@ -0,0 +1,44 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailBasicInfo( + date: String, + title: String, + totalTime: String, + totalCostTag: String, + city: String +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = date, + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.gray400 + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = title, + style = DateRoadTheme.typography.titleExtra24, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(16.dp)) + CourseDetailInfoBar( + totalTime = totalTime, + totalCostTag = totalCostTag, + city = city + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBottomBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBottomBar.kt new file mode 100644 index 000000000..d5750d661 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailBottomBar.kt @@ -0,0 +1,80 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailBottomBar( + modifier: Modifier = Modifier, + isUserLiked: Boolean, + onLikeButtonClicked: () -> Unit, + onEnrollButtonClicked: () -> Unit +) { + var buttonHeight by remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + Row( + modifier = modifier + .fillMaxWidth() + .background(color = DateRoadTheme.colors.white) + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + DateRoadImageButton( + modifier = Modifier.height(buttonHeight), + iconResId = R.drawable.ic_coures_detail_heart_default, + enabledContentColor = DateRoadTheme.colors.purple600, + disabledContentColor = DateRoadTheme.colors.gray200, + enabledBackgroundColor = DateRoadTheme.colors.gray100, + disabledBackgroundColor = DateRoadTheme.colors.gray100, + isEnabled = isUserLiked, + onClick = onLikeButtonClicked, + cornerRadius = 14.dp, + paddingHorizontal = 23.dp, + paddingVertical = 0.dp + ) + Spacer(modifier = Modifier.width(12.dp)) + DateRoadBasicButton( + modifier = Modifier + .weight(1f) + .onSizeChanged { size -> + buttonHeight = with(density) { size.height.toDp() } + }, + textContent = stringResource(id = R.string.course_detail_get_course), + onClick = onEnrollButtonClicked + ) + } +} + +@Preview +@Composable +fun ButtonPreview(modifier: Modifier = Modifier) { + Box(modifier = Modifier) { + CourseDetailBottomBar( + modifier = Modifier.align(Alignment.BottomCenter), + isUserLiked = true, + onLikeButtonClicked = { }, + onEnrollButtonClicked = { } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailCost.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailCost.kt new file mode 100644 index 000000000..254bd7478 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailCost.kt @@ -0,0 +1,41 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailCost( + totalCost: String +) { + Spacer(modifier = Modifier.height(14.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(id = R.string.course_total_cost_string), + style = DateRoadTheme.typography.titleBold18, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = totalCost, + style = DateRoadTheme.typography.bodyBold15, + color = DateRoadTheme.colors.black, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(10.dp)) + .background(DateRoadTheme.colors.gray100) + .padding(start = 20.dp, top = 15.dp, end = 5.dp, bottom = 17.dp) + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailInfoBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailInfoBar.kt new file mode 100644 index 000000000..b5b7430fc --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailInfoBar.kt @@ -0,0 +1,86 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailInfoBar( + totalCostTag: String, + totalTime: String, + city: String +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier.weight(116f), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_all_money_14), + contentDescription = null + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = totalCostTag, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodySemi15 + ) + } + + Row( + modifier = Modifier.weight(86f), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_all_clock_14), + contentDescription = null + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = totalTime, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodySemi15 + ) + } + + Row( + modifier = Modifier.weight(122f), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_all_location_14), + contentDescription = null + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = city, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodySemi15 + ) + } + } +} + +@Preview +@Composable +fun CourseDetailInfoBarPreview() { + CourseDetailInfoBar( + totalTime = "10", + city = "건대/성수/왕십리", + totalCostTag = "50000" + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailOpenedDetail.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailOpenedDetail.kt new file mode 100644 index 000000000..53d808d52 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailOpenedDetail.kt @@ -0,0 +1,40 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.ui.theme.DateRoadTheme + +fun LazyListScope.courseDetailOpenedDetail( + description: String, + startAt: String, + places: List, + totalCost: String, + tags: List +) { + item { + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = description, + style = DateRoadTheme.typography.bodyMed13Context, + color = DateRoadTheme.colors.black + ) + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + courseDetailTimeline(startAt = startAt, places = places) + item { + CourseDetailCost(totalCost = totalCost) + } + item { + CourseDetailTag(tags = tags) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTag.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTag.kt new file mode 100644 index 000000000..f0b01c965 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTag.kt @@ -0,0 +1,50 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.ui.component.chip.DateRoadImageChip +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailTag( + tags: List +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(id = R.string.course_detail_tag), + style = DateRoadTheme.typography.titleBold18, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(12.dp)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) { + items(tags) { tag -> + DateRoadImageChip( + textId = tag.titleRes, + imageRes = tag.imageRes, + chipType = ChipType.DATE, + isSelected = false + ) + } + } + Spacer(modifier = Modifier.height(30.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTimeline.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTimeline.kt new file mode 100644 index 000000000..0cbdb6b1d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailTimeline.kt @@ -0,0 +1,47 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.presentation.type.PlaceCardType +import org.sopt.dateroad.presentation.ui.component.card.DateRoadPlaceCard +import org.sopt.dateroad.ui.theme.DateRoadTheme + +fun LazyListScope.courseDetailTimeline( + startAt: String, + places: List +) { + item { + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(id = R.string.course_detail_timeline_title), + style = DateRoadTheme.typography.titleBold18, + color = DateRoadTheme.colors.black, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(7.dp)) + Text( + text = startAt, + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.gray400, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(14.dp)) + } + items(places.size) { index -> + DateRoadPlaceCard( + modifier = Modifier.padding(horizontal = 16.dp), + placeCardType = PlaceCardType.COURSE_NORMAL, + sequence = index, + place = places[index] + ) + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailUnopenedDetail.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailUnopenedDetail.kt new file mode 100644 index 000000000..1c288ea63 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/component/CourseDetailUnopenedDetail.kt @@ -0,0 +1,106 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.CourseDetailUnopenedDetailType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun CourseDetailUnopenedDetail( + text: String, + free: Int, + courseDetailUnopenedDetailType: CourseDetailUnopenedDetailType, + onButtonClicked: () -> Unit +) { + Column { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = text, + style = DateRoadTheme.typography.bodyMed13Context, + color = DateRoadTheme.colors.black, + maxLines = 3, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.White.copy(alpha = 0.6f), + Color.White.copy(alpha = 1f) + ) + ) + ) + .matchParentSize() + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Image( + painter = painterResource(id = R.drawable.img_course_detail_is_not_access), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.course_detail_unopened_title), + style = DateRoadTheme.typography.bodyBold17, + color = DateRoadTheme.colors.black, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = courseDetailUnopenedDetailType.descriptionStringRes), + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.purple600, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(24.dp)) + DateRoadFilledButton( + modifier = Modifier + .align(Alignment.CenterHorizontally), + isEnabled = true, + textContent = when (courseDetailUnopenedDetailType) { + CourseDetailUnopenedDetailType.FREE -> stringResource(id = courseDetailUnopenedDetailType.buttonTextStringRes, free) + CourseDetailUnopenedDetailType.POINT -> stringResource(id = courseDetailUnopenedDetailType.buttonTextStringRes) + }, + onClick = onButtonClicked, + textStyle = DateRoadTheme.typography.bodyBold15, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 14.dp, + paddingHorizontal = 52.dp, + paddingVertical = 16.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/navigation/CourseDetailNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/navigation/CourseDetailNavigation.kt new file mode 100644 index 000000000..a01666ff6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/coursedetail/navigation/CourseDetailNavigation.kt @@ -0,0 +1,40 @@ +package org.sopt.dateroad.presentation.ui.coursedetail.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.coursedetail.CourseDetailRoute + +fun NavController.navigationCourseDetail(courseId: Int) { + this.navigate(route = CourseDetailRoute.route(courseId = courseId)) +} + +fun NavGraphBuilder.courseDetailGraph( + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit +) { + composable( + route = CourseDetailRoute.ROUTE_WITH_ARGUMENT, + arguments = listOf( + navArgument(CourseDetailRoute.ID) { + type = NavType.IntType + } + ) + ) { navBackStackEntry -> + CourseDetailRoute( + popBackStack = popBackStack, + navigateToEnroll = navigateToEnroll, + courseId = navBackStackEntry.arguments?.getInt(CourseDetailRoute.ID) ?: 0 + ) + } +} + +object CourseDetailRoute { + private const val ROUTE = "courseDetail" + const val ID = "id" + const val ROUTE_WITH_ARGUMENT = "$ROUTE/{$ID}" + fun route(courseId: Int) = "$ROUTE/$courseId" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollContract.kt new file mode 100644 index 000000000..c9e34e5dd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollContract.kt @@ -0,0 +1,92 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import org.sopt.dateroad.domain.model.CourseDetail +import org.sopt.dateroad.domain.model.Enroll +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.presentation.type.EnrollScreenType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.model.Picker +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.util.TimePicker +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class EnrollContract { + data class EnrollUiState( + val loadState: LoadState = LoadState.Idle, + val fetchEnrollState: LoadState = LoadState.Idle, + val enrollType: EnrollType = EnrollType.COURSE, + val page: EnrollScreenType = EnrollScreenType.FIRST, + val enroll: Enroll = Enroll(), + val isEnrollButtonEnabled: Boolean = false, + val titleValidateState: TextFieldValidateResult = TextFieldValidateResult.Basic, + val dateValidateState: TextFieldValidateResult = TextFieldValidateResult.Basic, + val isDatePickerBottomSheetOpen: Boolean = false, + val datePickers: List = listOf( + Picker(items = (2000..2024).map { it.toString() }, startIndex = 24), + Picker(items = (1..12).map { it.toString() }), + Picker(items = (1..31).map { it.toString() }) + ), + val isTimePickerBottomSheetOpen: Boolean = false, + val timePickers: List = listOf( + Picker(items = listOf(TimePicker.AM, TimePicker.PM)), + Picker(items = (1..12).map { it.toString() }), + Picker(items = (0..59).map { it.toString().padStart(2, '0') }) + ), + val isRegionBottomSheetOpen: Boolean = false, + val onRegionBottomSheetRegionSelected: RegionType? = RegionType.SEOUL, + val onRegionBottomSheetAreaSelected: Any? = null, + val place: Place = Place(), + val isPlaceEditable: Boolean = true, + val isDurationBottomSheetOpen: Boolean = false, + val durationPicker: List = listOf(Picker(items = (1..12).map { (it * 0.5).toString() })), + val isEnrollSuccessDialogOpen: Boolean = false + ) : UiState + + sealed interface EnrollSideEffect : UiSideEffect { + data object PopBackStack : EnrollSideEffect + data object NavigateToMyCourseRead : EnrollSideEffect + } + + sealed class EnrollEvent : UiEvent { + data object OnTopBarBackButtonClick : EnrollEvent() + data object OnEnrollButtonClick : EnrollEvent() + data object OnDateTextFieldClick : EnrollEvent() + data object OnSelectedPlaceCourseTimeClick : EnrollEvent() + data object OnDatePickerBottomSheetDismissRequest : EnrollEvent() + data object OnTimePickerBottomSheetDismissRequest : EnrollEvent() + data object OnRegionBottomSheetDismissRequest : EnrollEvent() + data object OnDurationBottomSheetDismissRequest : EnrollEvent() + data object OnTimeTextFieldClick : EnrollEvent() + data object OnRegionTextFieldClick : EnrollEvent() + data class FetchEnrollCourseType(val enrollType: EnrollType) : EnrollEvent() + data class FetchCourseDetail(val fetchEnrollState: LoadState, val courseDetail: CourseDetail?) : EnrollEvent() + data class FetchTimelineDetail(val fetchEnrollState: LoadState, val timelineDetail: TimelineDetail?) : EnrollEvent() + data class SetEnrollButtonEnabled(val isEnrollButtonEnabled: Boolean) : EnrollEvent() + data class SetImage(val images: List) : EnrollEvent() + data class OnImageDeleteButtonClick(val index: Int) : EnrollEvent() + data class OnTitleValueChange(val title: String) : EnrollEvent() + data class OnDatePickerBottomSheetButtonClick(val date: String) : EnrollEvent() + data class OnTimePickerBottomSheetButtonClick(val startAt: String) : EnrollEvent() + data class OnDateChipClicked(val tag: String) : EnrollEvent() + data class OnRegionBottomSheetRegionChipClick(val country: RegionType) : EnrollEvent() + data class OnRegionBottomSheetAreaChipClick(val city: Any?) : EnrollEvent() + data class OnRegionBottomSheetButtonClick(val region: RegionType?, val area: Any?) : EnrollEvent() + data class OnAddPlaceButtonClick(val place: Place) : EnrollEvent() + data class OnPlaceCardDragAndDrop(val places: List) : EnrollEvent() + data class OnPlaceTitleValueChange(val placeTitle: String) : EnrollEvent() + data class OnDurationBottomSheetButtonClick(val placeDuration: String) : EnrollEvent() + data class OnEditableValueChange(val editable: Boolean) : EnrollEvent() + data class OnPlaceCardDeleteButtonClick(val index: Int) : EnrollEvent() + data class OnDescriptionValueChange(val description: String) : EnrollEvent() + data class OnCostValueChange(val cost: String) : EnrollEvent() + data class Enroll(val loadState: LoadState) : EnrollEvent() + data class SetTitleValidationState(val titleValidationState: TextFieldValidateResult) : EnrollEvent() + data class SetDateValidationState(val dateValidationState: TextFieldValidateResult) : EnrollEvent() + data class SetIsEnrollSuccessDialogOpen(val isEnrollSuccessDialogOpen: Boolean) : EnrollEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollFirstScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollFirstScreen.kt new file mode 100644 index 000000000..ab73e0405 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollFirstScreen.kt @@ -0,0 +1,105 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.type.GyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType +import org.sopt.dateroad.domain.type.SeoulAreaType +import org.sopt.dateroad.presentation.type.DateChipGroupType +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.dateroad.presentation.ui.component.chipgroup.DateRoadDateChipGroup +import org.sopt.dateroad.presentation.ui.component.textfield.DateRoadBasicTextField +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@Composable +fun EnrollFirstScreen( + enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + onDateTextFieldClick: () -> Unit, + onTimeTextFieldClick: () -> Unit, + onRegionTextFieldClick: () -> Unit, + onTitleValueChange: (String) -> Unit, + onDateChipClicked: (DateTagType) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(14.dp)) + DateRoadBasicTextField( + placeholder = stringResource(id = R.string.enroll_title_placeholder), + validateState = enrollUiState.titleValidateState, + errorDescription = stringResource(id = R.string.enroll_title_error_description), + value = enrollUiState.enroll.title, + onValueChange = onTitleValueChange + ) + Spacer(modifier = Modifier.height(2.dp)) + DateRoadBasicTextField( + placeholder = stringResource(id = R.string.enroll_date_placeholder), + validateState = enrollUiState.dateValidateState, + errorDescription = stringResource(id = R.string.enroll_date_error_description), + value = enrollUiState.enroll.date, + readOnly = true, + iconResourceId = R.drawable.ic_enroll_calendar, + onClick = onDateTextFieldClick + ) + Spacer(modifier = Modifier.height(2.dp)) + DateRoadBasicTextField( + placeholder = stringResource(id = R.string.enroll_date_start_at), + value = enrollUiState.enroll.startAt, + readOnly = true, + iconResourceId = R.drawable.ic_enroll_time, + onClick = onTimeTextFieldClick + ) + Spacer(modifier = Modifier.height(20.dp)) + DateRoadDateChipGroup( + dateChipGroupType = DateChipGroupType.ENROLL, + selectedDateTags = enrollUiState.enroll.tags.mapNotNull { tag -> tag.getDateTagTypeByName() }, + onSelectedDateTagsChanged = onDateChipClicked + ) + Spacer(modifier = Modifier.height(20.dp)) + DateRoadBasicTextField( + placeholder = stringResource(id = R.string.enroll_city_placeholder), + value = when (enrollUiState.enroll.city) { + is SeoulAreaType -> enrollUiState.enroll.city.title + is GyeonggiAreaType -> enrollUiState.enroll.city.title + is IncheonAreaType -> enrollUiState.enroll.city.title + else -> "" + }, + readOnly = true, + onClick = onRegionTextFieldClick + ) + Spacer(modifier = Modifier.height(23.dp)) + } +} + +@Preview +@Composable +fun EnrollFirstScreenPreview() { + DATEROADTheme { + EnrollFirstScreen( + enrollUiState = EnrollContract.EnrollUiState( + loadState = LoadState.Success + ), + onDateTextFieldClick = {}, + onTimeTextFieldClick = {}, + onRegionTextFieldClick = {}, + onTitleValueChange = {}, + onDateChipClicked = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollScreen.kt new file mode 100644 index 000000000..52dda626a --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollScreen.kt @@ -0,0 +1,583 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import android.net.Uri +import android.os.Build +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.presentation.type.DateRoadRegionBottomSheetType +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.EnrollScreenType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.type.OneButtonDialogType +import org.sopt.dateroad.presentation.type.OneButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadPickerBottomSheet +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadRegionBottomSheet +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadOneButtonDialog +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.enroll.component.EnrollPhotos +import org.sopt.dateroad.presentation.util.DatePicker +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_BRING_COURSE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_COURSE1_BACK +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_COURSE2_BACK +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_COURSE3_BACK +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_SCHEDULE1_BACK +import org.sopt.dateroad.presentation.util.EnrollAmplitude.CLICK_SCHEDULE2_BACK +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_CONTENT_BOOL +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_CONTENT_NUM +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_COST +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_DATE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_IMAGE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_LOCATION +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_START_TIME +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_TAGS +import org.sopt.dateroad.presentation.util.EnrollAmplitude.COURSE_TITLE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_AREA +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_COURSE_NUM +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_DATE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_DETAIL_LOCATION +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_DETAIL_TIME +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_LOCATION +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_SPEND_TIME +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_TAG_NUM +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_TIME +import org.sopt.dateroad.presentation.util.EnrollAmplitude.DATE_TITLE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.LOCATION_NUM +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_ADD_BRING_COURSE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_ADD_BRING_COURSE2 +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_ADD_SCHEDULE +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_ADD_SCHEDULE2 +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_COURSE1 +import org.sopt.dateroad.presentation.util.EnrollAmplitude.VIEW_PATH +import org.sopt.dateroad.presentation.util.EnrollScreen.MAX_ITEMS +import org.sopt.dateroad.presentation.util.EnrollScreen.TITLE_MIN_LENGTH +import org.sopt.dateroad.presentation.util.TimePicker +import org.sopt.dateroad.presentation.util.TimelineAmplitude.CLICK_ADD_SCHEDULE +import org.sopt.dateroad.presentation.util.amplitude.AmplitudeUtils +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollRoute( + padding: PaddingValues, + viewModel: EnrollViewModel = hiltViewModel(), + popBackStack: () -> Unit, + navigateToMyCourse: (MyCourseType) -> Unit, + enrollType: EnrollType, + viewPath: String, + timelineId: Int? +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + if (uri != null) viewModel.setEvent(EnrollContract.EnrollEvent.SetImage(images = listOf(uri.toString()))) + } + + val getPhotoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(maxItems = MAX_ITEMS)) { uris: List -> + viewModel.setEvent(EnrollContract.EnrollEvent.SetImage(images = uris.map { it.toString() })) + } + + BackHandler { + viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) + } + + LaunchedEffect(Unit) { + viewModel.setEvent(EnrollContract.EnrollEvent.FetchEnrollCourseType(enrollType = enrollType)) + + if (timelineId != null) { + when (enrollType) { + EnrollType.COURSE -> { + viewModel.fetchTimelineDetail(timelineId = timelineId) + } + + EnrollType.TIMELINE -> { + viewModel.fetchCourseDetail(courseId = timelineId) + } + } + } + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { enrollSideEffect -> + when (enrollSideEffect) { + is EnrollContract.EnrollSideEffect.PopBackStack -> popBackStack() + is EnrollContract.EnrollSideEffect.NavigateToMyCourseRead -> navigateToMyCourse(MyCourseType.READ) + } + } + } + + LaunchedEffect(uiState.enroll.title) { + viewModel.setEvent( + EnrollContract.EnrollEvent.SetTitleValidationState( + titleValidationState = when { + uiState.enroll.title.isEmpty() -> TextFieldValidateResult.Basic + uiState.enroll.title.length >= TITLE_MIN_LENGTH -> TextFieldValidateResult.Success + else -> TextFieldValidateResult.ValidationError + } + ) + ) + } + + LaunchedEffect(uiState.enroll.date) { + viewModel.setEvent( + EnrollContract.EnrollEvent.SetDateValidationState( + dateValidationState = when { + uiState.enroll.date.isEmpty() -> TextFieldValidateResult.Basic + uiState.enrollType == EnrollType.COURSE && LocalDate.parse(uiState.enroll.date, DateTimeFormatter.ofPattern(DatePicker.DATE_PATTERN)).isAfter(LocalDate.now()) -> TextFieldValidateResult.ValidationError + else -> TextFieldValidateResult.Success + } + ) + ) + } + + LaunchedEffect(uiState.page) { + when (enrollType) { + EnrollType.COURSE -> { + when (uiState.page) { + EnrollScreenType.FIRST -> AmplitudeUtils.trackEventWithProperty(eventName = VIEW_COURSE1, propertyName = VIEW_PATH, propertyValue = viewPath) + EnrollScreenType.SECOND -> Unit + EnrollScreenType.THIRD -> Unit + } + } + + EnrollType.TIMELINE -> { + (timelineId != null).let { isBringCourse -> + when (uiState.page) { + EnrollScreenType.FIRST -> AmplitudeUtils.trackEventWithProperty(eventName = if (isBringCourse) VIEW_ADD_BRING_COURSE else VIEW_ADD_SCHEDULE, propertyName = VIEW_PATH, propertyValue = viewPath) + EnrollScreenType.SECOND -> AmplitudeUtils.trackEvent(eventName = if (isBringCourse) VIEW_ADD_BRING_COURSE2 else VIEW_ADD_SCHEDULE2) + EnrollScreenType.THIRD -> Unit + } + } + } + } + } + + LaunchedEffect(uiState.loadState) { + if (uiState.loadState == LoadState.Success) { + when (uiState.enrollType) { + EnrollType.TIMELINE -> AmplitudeUtils.trackEvent(CLICK_ADD_SCHEDULE) + else -> Unit + } + } + } + + EnrollScreen( + padding = padding, + enrollUiState = uiState, + onTopBarBackButtonClick = { + viewModel.setEvent(EnrollContract.EnrollEvent.OnTopBarBackButtonClick) + + when (enrollType) { + EnrollType.COURSE -> { + when (uiState.page) { + EnrollScreenType.FIRST -> AmplitudeUtils.trackEventWithProperties( + eventName = CLICK_COURSE1_BACK, + properties = with(uiState.enroll) { mapOf(COURSE_IMAGE to images.isNotEmpty(), COURSE_TITLE to title.isNotEmpty(), COURSE_DATE to date.isNotEmpty(), COURSE_START_TIME to startAt.isNotEmpty(), COURSE_TAGS to tags.isNotEmpty(), COURSE_LOCATION to (city != null)) } + ) + + EnrollScreenType.SECOND -> AmplitudeUtils.trackEventWithProperties( + eventName = CLICK_COURSE2_BACK, + properties = with(uiState.place) { mapOf(DATE_LOCATION to title.isNotEmpty(), DATE_SPEND_TIME to duration.isNotEmpty(), LOCATION_NUM to uiState.enroll.places.size) } + ) + + EnrollScreenType.THIRD -> AmplitudeUtils.trackEventWithProperties( + eventName = CLICK_COURSE3_BACK, + properties = with(uiState.enroll) { mapOf(COURSE_CONTENT_BOOL to description.isNotEmpty(), COURSE_CONTENT_NUM to description.length, COURSE_COST to cost.isNotEmpty()) } + ) + } + } + + EnrollType.TIMELINE -> { + when (uiState.page) { + EnrollScreenType.FIRST -> AmplitudeUtils.trackEventWithProperties( + eventName = CLICK_SCHEDULE1_BACK, + properties = with(uiState.enroll) { mapOf(DATE_TITLE to title.isNotEmpty(), DATE_DATE to date.isNotEmpty(), DATE_TIME to startAt.isNotEmpty(), DATE_TAG_NUM to tags.size, DATE_AREA to (city != null)) } + ) + + EnrollScreenType.SECOND -> AmplitudeUtils.trackEventWithProperties( + eventName = CLICK_SCHEDULE2_BACK, + properties = with(uiState.place) { mapOf(DATE_DETAIL_LOCATION to title.isNotEmpty(), DATE_DETAIL_TIME to duration.isNotEmpty(), DATE_COURSE_NUM to uiState.enroll.places.size) } + ) + + EnrollScreenType.THIRD -> Unit + } + } + } + }, + onTopBarLoadButtonClick = { + viewModel.setSideEffect(EnrollContract.EnrollSideEffect.NavigateToMyCourseRead) + AmplitudeUtils.trackEvent(eventName = CLICK_BRING_COURSE) + }, + onEnrollButtonClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnEnrollButtonClick) }, + onDateTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDateTextFieldClick) }, + onTimeTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimeTextFieldClick) }, + onRegionTextFieldClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionTextFieldClick) }, + onSelectedPlaceCourseTimeClick = { viewModel.setEvent(EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick) }, + onDatePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest) }, + onTimePickerBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest) }, + onRegionBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetDismissRequest) }, + onDurationBottomSheetDismissRequest = { viewModel.setEvent(EnrollContract.EnrollEvent.OnDurationBottomSheetDismissRequest) }, + onPhotoButtonClick = { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + getGalleryLauncher.launch("image/*") + } else { + getPhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + } + }, + onImageDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnImageDeleteButtonClick(index = index)) }, + onTitleValueChange = { title -> viewModel.setEvent(EnrollContract.EnrollEvent.OnTitleValueChange(title = title)) }, + onDatePickerBottomSheetButtonClick = { date -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick(date = date)) }, + onTimePickerBottomSheetButtonClick = { startAt -> viewModel.setEvent(EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick(startAt = startAt)) }, + onDateChipClicked = { tag -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDateChipClicked(tag = tag.name)) }, + onRegionBottomSheetRegionChipClick = { country -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetRegionChipClick(country = country)) }, + onRegionBottomSheetAreaChipClick = { city -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetAreaChipClick(city = city)) }, + onRegionBottomSheetButtonClick = { region: RegionType?, area: Any? -> viewModel.setEvent(EnrollContract.EnrollEvent.OnRegionBottomSheetButtonClick(region = region, area = area)) }, + onAddPlaceButtonClick = { place -> viewModel.setEvent(EnrollContract.EnrollEvent.OnAddPlaceButtonClick(place = place)) }, + onPlaceCardDragAndDrop = { places -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDragAndDrop(places = places)) }, + onPlaceTitleValueChange = { placeTitle -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceTitleValueChange(placeTitle = placeTitle)) }, + onDurationBottomSheetButtonClick = { placeDuration -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDurationBottomSheetButtonClick(placeDuration = placeDuration)) }, + onPlaceEditButtonClick = { editable -> viewModel.setEvent(EnrollContract.EnrollEvent.OnEditableValueChange(editable = editable)) }, + onPlaceCardDeleteButtonClick = { index -> viewModel.setEvent(EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick(index = index)) }, + onDescriptionValueChange = { description -> viewModel.setEvent(EnrollContract.EnrollEvent.OnDescriptionValueChange(description = description)) }, + onCostValueChange = { cost -> viewModel.setEvent(EnrollContract.EnrollEvent.OnCostValueChange(cost = cost)) }, + onEnrollSuccessDialogButtonClick = { + viewModel.setSideEffect(EnrollContract.EnrollSideEffect.PopBackStack) + } + ) + + when (uiState.loadState) { + LoadState.Success -> { + viewModel.setEvent(EnrollContract.EnrollEvent.SetIsEnrollSuccessDialogOpen(isEnrollSuccessDialogOpen = true)) + } + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Error -> DateRoadErrorView() + + else -> Unit + } + + with(uiState) { + viewModel.setEvent( + EnrollContract.EnrollEvent.SetEnrollButtonEnabled( + when (page) { + EnrollScreenType.FIRST -> { + when (enrollType) { + EnrollType.COURSE -> enroll.images.isNotEmpty() && titleValidateState == TextFieldValidateResult.Success && dateValidateState == TextFieldValidateResult.Success && enroll.startAt.isNotEmpty() && enroll.tags.isNotEmpty() && enroll.country != null && enroll.city != null + EnrollType.TIMELINE -> titleValidateState == TextFieldValidateResult.Success && enroll.date.isNotEmpty() && enroll.startAt.isNotEmpty() && enroll.tags.isNotEmpty() && enroll.country != null && enroll.city != null + } + } + + EnrollScreenType.SECOND -> enroll.places.size >= 2 + EnrollScreenType.THIRD -> enroll.description.length >= 200 && enroll.cost.isNotEmpty() + } + ) + ) + + if (enroll.places.isEmpty()) viewModel.setEvent(EnrollContract.EnrollEvent.OnEditableValueChange(editable = true)) + } +} + +@Composable +fun EnrollScreen( + padding: PaddingValues, + enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + onTopBarBackButtonClick: () -> Unit, + onTopBarLoadButtonClick: () -> Unit, + onEnrollButtonClick: () -> Unit, + onDateTextFieldClick: () -> Unit, + onTimeTextFieldClick: () -> Unit, + onRegionTextFieldClick: () -> Unit, + onSelectedPlaceCourseTimeClick: () -> Unit, + onDatePickerBottomSheetDismissRequest: () -> Unit, + onTimePickerBottomSheetDismissRequest: () -> Unit, + onRegionBottomSheetDismissRequest: () -> Unit, + onDurationBottomSheetDismissRequest: () -> Unit, + onPhotoButtonClick: () -> Unit, + onImageDeleteButtonClick: (Int) -> Unit, + onTitleValueChange: (String) -> Unit, + onDatePickerBottomSheetButtonClick: (String) -> Unit, + onTimePickerBottomSheetButtonClick: (String) -> Unit, + onDateChipClicked: (DateTagType) -> Unit, + onRegionBottomSheetRegionChipClick: (RegionType) -> Unit, + onRegionBottomSheetAreaChipClick: (Any?) -> Unit, + onRegionBottomSheetButtonClick: (RegionType?, Any?) -> Unit, + onAddPlaceButtonClick: (Place) -> Unit, + onPlaceCardDragAndDrop: (List) -> Unit, + onPlaceTitleValueChange: (String) -> Unit, + onDurationBottomSheetButtonClick: (String) -> Unit, + onPlaceEditButtonClick: (Boolean) -> Unit, + onPlaceCardDeleteButtonClick: (Int) -> Unit, + onDescriptionValueChange: (String) -> Unit, + onCostValueChange: (String) -> Unit, + onEnrollSuccessDialogButtonClick: () -> Unit +) { + val focusManager = LocalFocusManager.current + + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .background(color = DateRoadTheme.colors.white) + .pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + } + ) { + when (enrollUiState.enrollType) { + EnrollType.COURSE -> { + DateRoadBasicTopBar( + title = stringResource(id = R.string.top_bar_title_enroll_course), + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white, + onLeftIconClick = onTopBarBackButtonClick + ) + Spacer(modifier = Modifier.height(8.dp)) + EnrollPhotos( + isDeletable = enrollUiState.page == EnrollScreenType.FIRST, + images = enrollUiState.enroll.images, + onPhotoButtonClick = onPhotoButtonClick, + onDeleteButtonClick = onImageDeleteButtonClick + ) + } + + EnrollType.TIMELINE -> { + DateRoadBasicTopBar( + title = stringResource(id = R.string.top_bar_title_enroll_timeline), + leftIconResource = R.drawable.ic_top_bar_back_white, + onLeftIconClick = onTopBarBackButtonClick, + buttonContent = { + Row { + DateRoadFilledButton( + isEnabled = true, + textContent = stringResource(id = R.string.top_bar_button_text_load), + onClick = onTopBarLoadButtonClick, + textStyle = DateRoadTheme.typography.bodyMed13, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 20.dp, + paddingHorizontal = 10.dp, + paddingVertical = 5.dp + ) + Spacer(modifier = Modifier.width(16.dp)) + } + } + ) + Spacer(modifier = Modifier.height(2.dp)) + } + } + Column( + modifier = Modifier.weight(1f) + ) { + when (enrollUiState.page) { + EnrollScreenType.FIRST -> EnrollFirstScreen( + enrollUiState = enrollUiState, + onDateTextFieldClick = onDateTextFieldClick, + onTimeTextFieldClick = onTimeTextFieldClick, + onRegionTextFieldClick = onRegionTextFieldClick, + onTitleValueChange = onTitleValueChange, + onDateChipClicked = onDateChipClicked + ) + + EnrollScreenType.SECOND -> EnrollSecondScreen( + enrollUiState = enrollUiState, + onSelectedPlaceCourseTimeClick = onSelectedPlaceCourseTimeClick, + onAddPlaceButtonClick = onAddPlaceButtonClick, + onPlaceTitleValueChange = onPlaceTitleValueChange, + onPlaceEditButtonClick = onPlaceEditButtonClick, + onPlaceCardDeleteButtonClick = onPlaceCardDeleteButtonClick, + onPlaceCardDragAndDrop = onPlaceCardDragAndDrop + ) + + EnrollScreenType.THIRD -> EnrollThirdScreen( + enrollUiState = enrollUiState, + onDescriptionValueChange = onDescriptionValueChange, + onCostValueChange = onCostValueChange + ) + } + } + Spacer( + modifier = Modifier + .height(16.dp) + .padding(horizontal = 16.dp) + ) + DateRoadBasicButton( + modifier = Modifier.padding(horizontal = 16.dp), + isEnabled = enrollUiState.isEnrollButtonEnabled, + textContent = when (enrollUiState.enrollType) { + EnrollType.COURSE -> if (enrollUiState.page != EnrollScreenType.THIRD) stringResource(id = R.string.enroll_button_text_next_with_page, enrollUiState.page.position, 3) else stringResource(id = R.string.complete) + EnrollType.TIMELINE -> if (enrollUiState.page == EnrollScreenType.FIRST) stringResource(id = R.string.enroll_button_text_next) else stringResource(id = R.string.complete) + }, + onClick = onEnrollButtonClick + ) + Spacer(modifier = Modifier.height(16.dp)) + } + + DateRoadPickerBottomSheet( + isBottomSheetOpen = enrollUiState.isDatePickerBottomSheetOpen, + isButtonEnabled = true, + buttonText = stringResource(id = R.string.apply), + onButtonClick = { + onDatePickerBottomSheetButtonClick( + enrollUiState.datePickers.joinToString(separator = ".") { it.pickerState.selectedItem.padStart(2, '0') } + ) + }, + onDismissRequest = onDatePickerBottomSheetDismissRequest, + pickers = enrollUiState.datePickers + ) + + DateRoadPickerBottomSheet( + isBottomSheetOpen = enrollUiState.isTimePickerBottomSheetOpen, + isButtonEnabled = true, + buttonText = stringResource(id = R.string.apply), + onButtonClick = { + onTimePickerBottomSheetButtonClick( + formatTime(enrollUiState.timePickers.map { it.pickerState.selectedItem }) + ) + }, + onDismissRequest = onTimePickerBottomSheetDismissRequest, + pickers = enrollUiState.timePickers + ) + + DateRoadRegionBottomSheet( + isBottomSheetOpen = enrollUiState.isRegionBottomSheetOpen, + isButtonEnabled = enrollUiState.onRegionBottomSheetRegionSelected != null && enrollUiState.onRegionBottomSheetAreaSelected != null, + dateRoadRegionBottomSheetType = DateRoadRegionBottomSheetType.ENROLL, + selectedRegion = enrollUiState.onRegionBottomSheetRegionSelected, + onSelectedRegionChanged = { regionType -> + onRegionBottomSheetRegionChipClick(regionType) + }, + selectedArea = enrollUiState.onRegionBottomSheetAreaSelected, + onSelectedAreaChanged = { area -> + onRegionBottomSheetAreaChipClick(area) + }, + titleText = stringResource(id = R.string.region_bottom_sheet_title), + buttonText = stringResource(id = R.string.apply), + onButtonClick = { regoion, area -> onRegionBottomSheetButtonClick(regoion, area) }, + onDismissRequest = onRegionBottomSheetDismissRequest + ) + + DateRoadPickerBottomSheet( + isBottomSheetOpen = enrollUiState.isDurationBottomSheetOpen, + isButtonEnabled = true, + buttonText = stringResource(id = R.string.apply), + onButtonClick = { + onDurationBottomSheetButtonClick(enrollUiState.durationPicker.first().pickerState.selectedItem) + }, + onDismissRequest = onDurationBottomSheetDismissRequest, + pickers = enrollUiState.durationPicker + ) + + if (enrollUiState.isEnrollSuccessDialogOpen) { + when (enrollUiState.enrollType) { + EnrollType.TIMELINE -> { + DateRoadOneButtonDialog( + oneButtonDialogType = OneButtonDialogType.ENROLL_TIMELINE, + onDismissRequest = onEnrollSuccessDialogButtonClick, + onClickConfirm = onEnrollSuccessDialogButtonClick + ) + } + + EnrollType.COURSE -> { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.ENROLL_COURSE, + onDismissRequest = onEnrollSuccessDialogButtonClick, + onClickConfirm = onEnrollSuccessDialogButtonClick + ) + } + } + } +} + +fun formatTime(time: List): String { + val period = if (time[0] == TimePicker.AM) "AM" else "PM" + val hour = time[1].padStart(2, '0') + val minute = time[2].padStart(2, '0') + return "$hour:$minute $period" +} + +@Preview +@Composable +fun EnrollScreenPreview() { + DATEROADTheme { + EnrollScreen( + padding = PaddingValues(0.dp), + enrollUiState = EnrollContract.EnrollUiState( + loadState = LoadState.Success + ), + onTopBarBackButtonClick = {}, + onTopBarLoadButtonClick = {}, + onEnrollButtonClick = {}, + onDateTextFieldClick = {}, + onTimeTextFieldClick = {}, + onRegionTextFieldClick = {}, + onSelectedPlaceCourseTimeClick = {}, + onDatePickerBottomSheetDismissRequest = {}, + onTimePickerBottomSheetDismissRequest = {}, + onRegionBottomSheetDismissRequest = {}, + onDurationBottomSheetDismissRequest = {}, + onPhotoButtonClick = {}, + onImageDeleteButtonClick = {}, + onTitleValueChange = {}, + onDatePickerBottomSheetButtonClick = {}, + onTimePickerBottomSheetButtonClick = {}, + onDateChipClicked = {}, + onRegionBottomSheetRegionChipClick = {}, + onRegionBottomSheetAreaChipClick = {}, + onRegionBottomSheetButtonClick = { _, _ -> }, + onAddPlaceButtonClick = {}, + onPlaceTitleValueChange = {}, + onDurationBottomSheetButtonClick = {}, + onPlaceEditButtonClick = {}, + onPlaceCardDeleteButtonClick = {}, + onPlaceCardDragAndDrop = {}, + onDescriptionValueChange = {}, + onCostValueChange = {}, + onEnrollSuccessDialogButtonClick = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollSecondScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollSecondScreen.kt new file mode 100644 index 000000000..348bfb0a4 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollSecondScreen.kt @@ -0,0 +1,185 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import android.annotation.SuppressLint +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.presentation.type.PlaceCardType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadTextButton +import org.sopt.dateroad.presentation.ui.component.card.DateRoadPlaceCard +import org.sopt.dateroad.presentation.ui.enroll.component.EnrollPlaceInsertBar +import org.sopt.dateroad.presentation.util.Time +import org.sopt.dateroad.presentation.util.draganddrop.rememberDragAndDropListState +import org.sopt.dateroad.presentation.util.mutablelist.move +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@SuppressLint("UnrememberedMutableState", "UnnecessaryComposedModifier") +@Composable +fun EnrollSecondScreen( + enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + onSelectedPlaceCourseTimeClick: () -> Unit, + onAddPlaceButtonClick: (Place) -> Unit, + onPlaceTitleValueChange: (String) -> Unit, + onPlaceEditButtonClick: (Boolean) -> Unit, + onPlaceCardDeleteButtonClick: (Int) -> Unit, + onPlaceCardDragAndDrop: (List) -> Unit +) { + val scope = rememberCoroutineScope() + var overScrollJob by remember { mutableStateOf(null) } + + val placeLists = rememberUpdatedState(enrollUiState.enroll.places.toMutableStateList()) + val dragDropListState = rememberDragAndDropListState(onMove = { from, to -> + placeLists.value.move(from, to) + onPlaceCardDragAndDrop(placeLists.value.toList()) + }) + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.height(11.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(id = R.string.enroll_place_title), + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold17 + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(id = R.string.enroll_place_description), + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(13.dp)) + EnrollPlaceInsertBar( + modifier = Modifier.padding(horizontal = 16.dp), + title = enrollUiState.place.title, + duration = enrollUiState.place.duration, + onTitleChange = onPlaceTitleValueChange, + onSelectedCourseTimeClick = onSelectedPlaceCourseTimeClick, + onAddCourseButtonClick = { + onAddPlaceButtonClick(Place(title = enrollUiState.place.title, duration = enrollUiState.place.duration + Time.TIME)) + } + ) + Spacer(modifier = Modifier.height(22.dp)) + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + thickness = 1.dp, + color = DateRoadTheme.colors.gray200 + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.enroll_place_guide), + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyMed13 + ) + DateRoadTextButton( + textContent = stringResource(id = if (enrollUiState.isPlaceEditable) R.string.edit else R.string.complete), + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = if (enrollUiState.isPlaceEditable) DateRoadTheme.colors.gray400 else DateRoadTheme.colors.purple600, + paddingHorizontal = 18.dp, + paddingVertical = 6.dp, + onClick = { + onPlaceEditButtonClick(!enrollUiState.isPlaceEditable) + } + ) + } + Spacer(modifier = Modifier.height(12.dp)) + LazyColumn( + state = dragDropListState.lazyListState, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxHeight() + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropListState.onDrag(offset = offset) + + if (overScrollJob?.isActive == true) return@detectDragGesturesAfterLongPress + + dragDropListState + .checkForOverScroll() + .takeIf { it != 0f } + ?.let { + overScrollJob = scope.launch { dragDropListState.lazyListState.scrollBy(it) } + } ?: run { overScrollJob?.cancel() } + }, + onDragStart = { offset -> dragDropListState.onDragStart(offset = offset) }, + onDragEnd = { dragDropListState.onDragInterrupted() }, + onDragCancel = { dragDropListState.onDragInterrupted() } + ) + }, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(enrollUiState.enroll.places.size) { index -> + DateRoadPlaceCard( + modifier = Modifier + .zIndex(if (index == dragDropListState.currentIndexOfDraggedItem) 1f else 0f) + .graphicsLayer( + scaleX = animateFloatAsState(if (dragDropListState.currentIndexOfDraggedItem == index) 1.1f else 1.0f, label = "").value, + scaleY = animateFloatAsState(if (dragDropListState.currentIndexOfDraggedItem == index) 1.1f else 1.0f, label = "").value + ), + placeCardType = if (enrollUiState.isPlaceEditable) PlaceCardType.COURSE_EDIT else PlaceCardType.COURSE_DELETE, + place = enrollUiState.enroll.places[index], + onIconClick = { if (!enrollUiState.isPlaceEditable) onPlaceCardDeleteButtonClick(index) } + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Preview +@Composable +fun EnrollSecondScreenPreview() { + DATEROADTheme { + EnrollSecondScreen( + onAddPlaceButtonClick = {}, + onSelectedPlaceCourseTimeClick = {}, + onPlaceTitleValueChange = {}, + onPlaceEditButtonClick = {}, + onPlaceCardDeleteButtonClick = {}, + onPlaceCardDragAndDrop = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollThirdScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollThirdScreen.kt new file mode 100644 index 000000000..55ba14310 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollThirdScreen.kt @@ -0,0 +1,66 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.textfield.DateRoadBasicTextField +import org.sopt.dateroad.presentation.ui.component.textfield.DateRoadTextArea +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@Composable +fun EnrollThirdScreen( + enrollUiState: EnrollContract.EnrollUiState = EnrollContract.EnrollUiState(), + onDescriptionValueChange: (String) -> Unit, + onCostValueChange: (String) -> Unit +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(11.dp)) + DateRoadTextArea( + title = stringResource(id = R.string.enroll_description_title), + placeholder = stringResource(id = R.string.enroll_description_placeholder), + value = enrollUiState.enroll.description, + onValueChange = onDescriptionValueChange + ) + Spacer(modifier = Modifier.height(15.dp)) + DateRoadBasicTextField( + title = stringResource(id = R.string.enroll_cost_title), + placeholder = stringResource(id = R.string.enroll_cost_placeholder), + value = enrollUiState.enroll.cost, + onValueChange = { newValue -> + if (newValue.all { it.isDigit() }) onCostValueChange(newValue) + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Spacer(modifier = Modifier.height(6.dp)) + } +} + +@Preview +@Composable +fun EnrollSecondThirdPreview() { + DATEROADTheme { + EnrollThirdScreen( + enrollUiState = EnrollContract.EnrollUiState( + loadState = LoadState.Success + ), + onDescriptionValueChange = {}, + onCostValueChange = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollViewModel.kt new file mode 100644 index 000000000..29cb0e857 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/EnrollViewModel.kt @@ -0,0 +1,163 @@ +package org.sopt.dateroad.presentation.ui.enroll + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.data.dataremote.util.Date.NEAREST_DATE_START_OUTPUT_FORMAT +import org.sopt.dateroad.data.mapper.toEntity.toEnroll +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.usecase.GetCourseDetailUseCase +import org.sopt.dateroad.domain.usecase.GetTimelineDetailUseCase +import org.sopt.dateroad.domain.usecase.PostCourseUseCase +import org.sopt.dateroad.domain.usecase.PostTimelineUseCase +import org.sopt.dateroad.presentation.type.EnrollScreenType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class EnrollViewModel @Inject constructor( + private val getCourseDetailUseCase: GetCourseDetailUseCase, + private val getTimelineDetailUseCase: GetTimelineDetailUseCase, + private val postCourseUseCase: PostCourseUseCase, + private val postTimelineUseCase: PostTimelineUseCase +) : BaseViewModel() { + override fun createInitialState(): EnrollContract.EnrollUiState = EnrollContract.EnrollUiState() + + override suspend fun handleEvent(event: EnrollContract.EnrollEvent) { + when (event) { + is EnrollContract.EnrollEvent.OnTopBarBackButtonClick -> { + when (currentState.enrollType) { + EnrollType.COURSE -> { + when (currentState.page) { + EnrollScreenType.FIRST -> setSideEffect(EnrollContract.EnrollSideEffect.PopBackStack) + EnrollScreenType.SECOND -> setState { copy(page = EnrollScreenType.FIRST) } + EnrollScreenType.THIRD -> setState { copy(page = EnrollScreenType.SECOND) } + } + } + + EnrollType.TIMELINE -> { + when (currentState.page) { + EnrollScreenType.FIRST -> setSideEffect(EnrollContract.EnrollSideEffect.PopBackStack) + EnrollScreenType.SECOND -> setState { copy(page = EnrollScreenType.FIRST) } + EnrollScreenType.THIRD -> Unit + } + } + } + } + + is EnrollContract.EnrollEvent.OnEnrollButtonClick -> { + when (currentState.enrollType) { + EnrollType.COURSE -> { + when (currentState.page) { + EnrollScreenType.FIRST -> setState { copy(page = EnrollScreenType.SECOND) } + EnrollScreenType.SECOND -> setState { copy(page = EnrollScreenType.THIRD) } + EnrollScreenType.THIRD -> postCourse() + } + } + + EnrollType.TIMELINE -> { + when (currentState.page) { + EnrollScreenType.FIRST -> setState { copy(page = EnrollScreenType.SECOND) } + EnrollScreenType.SECOND -> postTimeline() + EnrollScreenType.THIRD -> Unit + } + } + } + } + + is EnrollContract.EnrollEvent.OnDateTextFieldClick -> setState { copy(isDatePickerBottomSheetOpen = true) } + is EnrollContract.EnrollEvent.OnTimeTextFieldClick -> setState { copy(isTimePickerBottomSheetOpen = true) } + is EnrollContract.EnrollEvent.OnRegionTextFieldClick -> setState { copy(isRegionBottomSheetOpen = true, onRegionBottomSheetRegionSelected = RegionType.SEOUL, onRegionBottomSheetAreaSelected = null) } + is EnrollContract.EnrollEvent.OnSelectedPlaceCourseTimeClick -> setState { copy(isDurationBottomSheetOpen = true) } + is EnrollContract.EnrollEvent.OnDatePickerBottomSheetDismissRequest -> setState { copy(isDatePickerBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.OnTimePickerBottomSheetDismissRequest -> setState { copy(isTimePickerBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.OnRegionBottomSheetDismissRequest -> setState { copy(isRegionBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.OnDurationBottomSheetDismissRequest -> setState { copy(isDurationBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.FetchEnrollCourseType -> setState { copy(enrollType = event.enrollType) } + is EnrollContract.EnrollEvent.FetchCourseDetail -> setState { copy(fetchEnrollState = event.fetchEnrollState, enroll = event.courseDetail?.toEnroll() ?: currentState.enroll) } + is EnrollContract.EnrollEvent.FetchTimelineDetail -> setState { copy(fetchEnrollState = event.fetchEnrollState, enroll = event.timelineDetail?.toEnroll() ?: currentState.enroll) } + is EnrollContract.EnrollEvent.SetEnrollButtonEnabled -> setState { copy(isEnrollButtonEnabled = event.isEnrollButtonEnabled) } + is EnrollContract.EnrollEvent.SetImage -> setState { copy(enroll = currentState.enroll.copy(images = event.images)) } + is EnrollContract.EnrollEvent.OnImageDeleteButtonClick -> setState { copy(enroll = currentState.enroll.copy(images = currentState.enroll.images.toMutableList().apply { removeAt(event.index) })) } + is EnrollContract.EnrollEvent.OnTitleValueChange -> setState { copy(enroll = currentState.enroll.copy(title = event.title)) } + + is EnrollContract.EnrollEvent.OnDatePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(date = event.date), isDatePickerBottomSheetOpen = false) } + + is EnrollContract.EnrollEvent.OnTimePickerBottomSheetButtonClick -> setState { copy(enroll = currentState.enroll.copy(startAt = event.startAt), isTimePickerBottomSheetOpen = false) } + is EnrollContract.EnrollEvent.OnDateChipClicked -> setState { + copy( + enroll = enroll.copy( + tags = currentState.enroll.tags.toMutableList().apply { + if (contains(event.tag)) { + remove(event.tag) + } else if (size < 3) { + add(event.tag) + } + } + ) + ) + } + + is EnrollContract.EnrollEvent.OnRegionBottomSheetRegionChipClick -> setState { copy(onRegionBottomSheetRegionSelected = event.country) } + is EnrollContract.EnrollEvent.OnRegionBottomSheetAreaChipClick -> setState { copy(onRegionBottomSheetAreaSelected = event.city) } + is EnrollContract.EnrollEvent.OnRegionBottomSheetButtonClick -> setState { copy(isRegionBottomSheetOpen = false, enroll = currentState.enroll.copy(country = event.region, city = event.area)) } + is EnrollContract.EnrollEvent.OnAddPlaceButtonClick -> setState { copy(enroll = currentState.enroll.copy(places = currentState.enroll.places.toMutableList().apply { add(event.place) }), place = currentState.place.copy(title = "", duration = "")) } + is EnrollContract.EnrollEvent.OnPlaceCardDragAndDrop -> setState { copy(enroll = currentState.enroll.copy(places = event.places)) } + is EnrollContract.EnrollEvent.OnPlaceTitleValueChange -> setState { copy(place = currentState.place.copy(title = event.placeTitle)) } + is EnrollContract.EnrollEvent.OnDurationBottomSheetButtonClick -> setState { copy(isDurationBottomSheetOpen = false, place = currentState.place.copy(duration = event.placeDuration)) } + is EnrollContract.EnrollEvent.OnEditableValueChange -> setState { copy(isPlaceEditable = event.editable) } + is EnrollContract.EnrollEvent.OnPlaceCardDeleteButtonClick -> setState { copy(enroll = currentState.enroll.copy(places = currentState.enroll.places.toMutableList().apply { removeAt(event.index) })) } + is EnrollContract.EnrollEvent.OnDescriptionValueChange -> setState { copy(enroll = currentState.enroll.copy(description = event.description)) } + is EnrollContract.EnrollEvent.OnCostValueChange -> setState { copy(enroll = currentState.enroll.copy(cost = event.cost)) } + is EnrollContract.EnrollEvent.Enroll -> setState { copy(loadState = event.loadState) } + is EnrollContract.EnrollEvent.SetTitleValidationState -> setState { copy(titleValidateState = event.titleValidationState) } + is EnrollContract.EnrollEvent.SetDateValidationState -> setState { copy(dateValidateState = event.dateValidationState) } + is EnrollContract.EnrollEvent.SetIsEnrollSuccessDialogOpen -> setState { copy(isEnrollSuccessDialogOpen = event.isEnrollSuccessDialogOpen) } + } + } + + fun fetchCourseDetail(courseId: Int) { + viewModelScope.launch { + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Loading, courseDetail = null)) + getCourseDetailUseCase(courseId = courseId).onSuccess { courseDetail -> + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Success, courseDetail = courseDetail.copy(startAt = courseDetail.startAt.substringBefore(NEAREST_DATE_START_OUTPUT_FORMAT)))) + } + setEvent(EnrollContract.EnrollEvent.FetchCourseDetail(fetchEnrollState = LoadState.Error, courseDetail = null)) + } + } + + fun fetchTimelineDetail(timelineId: Int) { + viewModelScope.launch { + setEvent(EnrollContract.EnrollEvent.FetchTimelineDetail(fetchEnrollState = LoadState.Loading, timelineDetail = null)) + getTimelineDetailUseCase(timelineId = timelineId).onSuccess { timelineDetail -> + setEvent(EnrollContract.EnrollEvent.FetchTimelineDetail(fetchEnrollState = LoadState.Success, timelineDetail = timelineDetail.copy(startAt = timelineDetail.startAt.substringBefore(NEAREST_DATE_START_OUTPUT_FORMAT)))) + }.onFailure { + setEvent(EnrollContract.EnrollEvent.FetchTimelineDetail(fetchEnrollState = LoadState.Error, timelineDetail = null)) + } + } + } + + private fun postCourse() { + viewModelScope.launch { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Loading)) + postCourseUseCase(enroll = currentState.enroll).onSuccess { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Success)) + }.onFailure { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } + } + } + + private fun postTimeline() { + viewModelScope.launch { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Loading)) + postTimelineUseCase(enroll = currentState.enroll).onSuccess { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Success)) + }.onFailure { + setEvent(EnrollContract.EnrollEvent.Enroll(loadState = LoadState.Error)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollAddPhotoButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollAddPhotoButton.kt new file mode 100644 index 000000000..75388a6ce --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollAddPhotoButton.kt @@ -0,0 +1,69 @@ +package org.sopt.dateroad.presentation.ui.enroll.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollAddPhotoButton( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Column( + modifier = modifier + .width(130.dp) + .aspectRatio(1f) + .noRippleClickable(onClick = onClick) + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.gray100), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(17.dp)) + Image( + modifier = Modifier + .clip(CircleShape) + .background(DateRoadTheme.colors.gray200) + .padding(horizontal = 9.dp, vertical = 10.dp), + painter = painterResource(id = R.drawable.ic_all_camera), + contentDescription = null + ) + Spacer(modifier = Modifier.height(13.dp)) + Text( + text = stringResource(id = R.string.enroll_add_photo), + color = DateRoadTheme.colors.gray300, + style = DateRoadTheme.typography.capBold11, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +fun EnrollAddPhotoButtonPreview() { + DATEROADTheme { + EnrollAddPhotoButton() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt new file mode 100644 index 000000000..edb1c05ef --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotoPreviewCard.kt @@ -0,0 +1,82 @@ +package org.sopt.dateroad.presentation.ui.enroll.component + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollPhotoPreviewCard( + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + id: Int, + isDeletable: Boolean, + image: String, + onDeleteButtonClick: (Int) -> Unit = {} +) { + Box( + modifier = modifier + .width(130.dp) + .aspectRatio(1f) + ) { + AsyncImage( + model = ImageRequest.Builder(context = context) + .data(image) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(14.dp)) + ) + if (isDeletable) { + Image( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 6.dp, end = 6.dp) + .clip(CircleShape) + .background(DateRoadTheme.colors.gray200) + .padding(5.dp) + .noRippleClickable(onClick = { onDeleteButtonClick(id) }), + painter = painterResource(id = R.drawable.ic_all_close), + contentDescription = null + ) + } + } +} + +@Preview +@Composable +fun EnrollPhotoPreviewCardPreview() { + DATEROADTheme { + EnrollPhotoPreviewCard( + id = 0, + isDeletable = true, + image = "" + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotos.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotos.kt new file mode 100644 index 000000000..991d0d8fe --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPhotos.kt @@ -0,0 +1,92 @@ +package org.sopt.dateroad.presentation.ui.enroll.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollPhotos( + modifier: Modifier = Modifier, + isDeletable: Boolean, + images: List, + onPhotoButtonClick: () -> Unit = {}, + onDeleteButtonClick: (Int) -> Unit = {} +) { + Box( + modifier = modifier + .fillMaxWidth() + ) { + LazyRow( + modifier = Modifier.padding(bottom = 10.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (images.isEmpty()) { + item { + EnrollAddPhotoButton( + onClick = onPhotoButtonClick + ) + } + } + items(images.size) { index -> + EnrollPhotoPreviewCard( + id = index, + isDeletable = isDeletable, + image = images[index], + onDeleteButtonClick = { onDeleteButtonClick(index) } + ) + } + } + if (images.isNotEmpty() && isDeletable) { + Image( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 16.dp) + .clip(CircleShape) + .background(DateRoadTheme.colors.gray200) + .padding(horizontal = 9.dp, vertical = 10.dp) + .noRippleClickable(onClick = onPhotoButtonClick), + painter = painterResource(id = R.drawable.ic_all_camera), + contentDescription = null + ) + } + DateRoadTextTag( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 16.dp), + textContent = stringResource(id = R.string.fraction_format, images.size, 10), + tagContentType = TagType.ENROLL_PHOTO_NUMBER + ) + } +} + +@Preview +@Composable +fun EnrollPhotosPreview() { + DATEROADTheme { + EnrollPhotos( + isDeletable = false, + images = listOf() + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt new file mode 100644 index 000000000..abd621824 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/component/EnrollPlaceInsertBar.kt @@ -0,0 +1,125 @@ +package org.sopt.dateroad.presentation.ui.enroll.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadPickerBottomSheet +import org.sopt.dateroad.presentation.ui.component.bottomsheet.model.Picker +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.presentation.ui.component.textfield.DateRoadBasicTextField +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun EnrollPlaceInsertBar( + modifier: Modifier = Modifier, + title: String = "", + duration: String = "", + onSelectedCourseTimeClick: () -> Unit = {}, + onTitleChange: (String) -> Unit = {}, + onAddCourseButtonClick: () -> Unit = {} +) { + var textFieldHeight by remember { mutableStateOf(0) } + val density = LocalDensity.current + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + DateRoadBasicTextField( + modifier = Modifier + .weight(196f) + .onGloballyPositioned { coordinates -> + textFieldHeight = maxOf(textFieldHeight, coordinates.size.height) + }, + placeholder = stringResource(id = R.string.enroll_place_insert_bar_enter_place_placeholder), + value = title, + onValueChange = onTitleChange + ) + Text( + modifier = Modifier + .weight(72f) + .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(14.dp)) + .padding(vertical = 16.dp) + .onGloballyPositioned { coordinates -> + textFieldHeight = maxOf(textFieldHeight, coordinates.size.height) + } + .padding(horizontal = 15.dp) + .noRippleClickable(onClick = onSelectedCourseTimeClick), + text = duration.ifEmpty { stringResource(id = R.string.enroll_place_insert_bar_select_course_time_placeholder) }, + color = if (duration.isEmpty()) DateRoadTheme.colors.gray300 else DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodySemi13, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + DateRoadImageButton( + modifier = Modifier + .size(with(density) { textFieldHeight.toDp() }), + isEnabled = duration.isNotEmpty() && title.isNotEmpty(), + cornerRadius = 14.dp, + paddingHorizontal = 15.dp, + paddingVertical = 15.dp, + disabledBackgroundColor = DateRoadTheme.colors.gray100, + disabledContentColor = DateRoadTheme.colors.gray300, + onClick = { if (duration.isNotEmpty() && title.isNotEmpty()) onAddCourseButtonClick() else Unit } + ) + } +} + +@Preview +@Composable +fun EnrollPlaceInsertBarPreview() { + DATEROADTheme { + var title by remember { mutableStateOf("") } + var duration by remember { mutableStateOf("") } + var isBottomSheetOpen by rememberSaveable { mutableStateOf(false) } + var pickerItems by remember { + mutableStateOf(listOf(Picker(items = (1..12).map { (it * 0.5).toString() }))) + } + + EnrollPlaceInsertBar( + title = title, + duration = duration, + onTitleChange = { titleValue -> + title = titleValue + }, + onSelectedCourseTimeClick = { + isBottomSheetOpen = true + } + ) + + DateRoadPickerBottomSheet( + isBottomSheetOpen = isBottomSheetOpen, + isButtonEnabled = true, + buttonText = "적용하기", + pickers = pickerItems, + onButtonClick = { + duration = pickerItems[0].pickerState.selectedItem + isBottomSheetOpen = false + } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/navigation/EnrollNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/navigation/EnrollNavigation.kt new file mode 100644 index 000000000..e7138d309 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/enroll/navigation/EnrollNavigation.kt @@ -0,0 +1,62 @@ +package org.sopt.dateroad.presentation.ui.enroll.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.ui.enroll.EnrollRoute +import org.sopt.dateroad.presentation.ui.mycourse.navigation.MyCourseRoute + +fun NavController.navigationEnroll(enrollType: EnrollType, viewPath: String, courseId: Int? = null) { + navigate( + route = EnrollRoute.route(enrollType = enrollType, viewPath = viewPath, courseId = courseId) + ) { + popUpTo(MyCourseRoute.route(MyCourseType.READ)) { inclusive = true } + launchSingleTop = true + } +} + +fun NavGraphBuilder.enrollNavGraph( + padding: PaddingValues, + popBackStack: () -> Unit, + navigationToMyCourse: (MyCourseType) -> Unit +) { + composable( + route = EnrollRoute.ROUTE_WITH_ARGUMENT, + arguments = listOf( + navArgument(EnrollRoute.ENROLL_TYPE) { + type = NavType.StringType + }, + navArgument(EnrollRoute.VIEW_PATH) { + type = NavType.StringType + }, + navArgument(EnrollRoute.TIMELINE_ID) { + type = NavType.StringType + nullable = true + } + ) + ) { backStackEntry -> + val enrollType = backStackEntry.arguments?.getString(EnrollRoute.ENROLL_TYPE)?.let { + EnrollType.valueOf(it) + } ?: EnrollType.COURSE + + val viewPath = backStackEntry.arguments?.getString(EnrollRoute.VIEW_PATH).orEmpty() + + val timelineId = backStackEntry.arguments?.getString(EnrollRoute.TIMELINE_ID)?.toIntOrNull() + + EnrollRoute(padding = padding, popBackStack = popBackStack, navigateToMyCourse = navigationToMyCourse, enrollType = enrollType, viewPath = viewPath, timelineId = timelineId) + } +} + +object EnrollRoute { + const val ROUTE = "enroll" + const val ENROLL_TYPE = "enrollType" + const val VIEW_PATH = "viewPath" + const val TIMELINE_ID = "timelineId" + const val ROUTE_WITH_ARGUMENT = "$ROUTE/{$ENROLL_TYPE}/{$VIEW_PATH}/{$TIMELINE_ID}" + fun route(enrollType: EnrollType, viewPath: String, courseId: Int?) = "$ROUTE/${enrollType.name}/$viewPath/$courseId" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeContract.kt new file mode 100644 index 000000000..a60308d06 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeContract.kt @@ -0,0 +1,44 @@ +package org.sopt.dateroad.presentation.ui.home + +import org.sopt.dateroad.domain.model.Advertisement +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class HomeContract { + data class HomeUiState( + val loadState: LoadState = LoadState.Idle, + val nearestTimeline: NearestTimeline = NearestTimeline(), + val topLikedCourses: List = listOf(), + val latestCourses: List = listOf(), + val advertisements: List = listOf(), + val userPoint: UserPoint = UserPoint(), + val currentBannerPage: Int = 0, + val profileImageUrl: String? = null + ) : UiState + + sealed interface HomeSideEffect : UiSideEffect { + data object NavigateToPointHistory : HomeSideEffect + data object NavigateToLook : HomeSideEffect + data class NavigateToTimelineDetail(val timelineType: TimelineType, val timelineId: Int) : HomeSideEffect + data class NavigateToEnroll(val enrollType: EnrollType, val viewPath: String, val id: Int?) : HomeSideEffect + data class NavigateToAdvertisementDetail(val advertisementId: Int) : HomeSideEffect + data class NavigateToCourseDetail(val courseId: Int) : HomeSideEffect + } + + sealed class HomeEvent : UiEvent { + data class FetchNearestTimeline(val loadState: LoadState, val nearestTimeline: NearestTimeline) : HomeEvent() + data class FetchTopLikedCourses(val loadState: LoadState, val topLikedCourses: List) : HomeEvent() + data class FetchLatestCourses(val loadState: LoadState, val latestCourses: List) : HomeEvent() + data class FetchAdvertisements(val loadState: LoadState, val advertisements: List) : HomeEvent() + data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : HomeEvent() + data class ChangeBannerPage(val page: Int) : HomeEvent() + data class FetchProfileImage(val loadState: LoadState, val profileImageUrl: String?) : HomeEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeScreen.kt new file mode 100644 index 000000000..d14c96047 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeScreen.kt @@ -0,0 +1,316 @@ +package org.sopt.dateroad.presentation.ui.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.type.SortByType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MainNavigationBarItemType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.presentation.ui.component.button.DateRoadTextButton +import org.sopt.dateroad.presentation.ui.component.card.DateRoadCourseCard +import org.sopt.dateroad.presentation.ui.component.partialcolortext.PartialColorText +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.home.component.DateRoadHomeTopBar +import org.sopt.dateroad.presentation.ui.home.component.HomeAdvertisement +import org.sopt.dateroad.presentation.ui.home.component.HomeHotCourseCard +import org.sopt.dateroad.presentation.ui.home.component.HomeTimeLineCard +import org.sopt.dateroad.presentation.util.ViewPath.HOME +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun HomeRoute( + padding: PaddingValues, + viewModel: HomeViewModel = hiltViewModel(), + navigateToPointHistory: () -> Unit, + navigateToLook: (MainNavigationBarItemType) -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToAdvertisementDetail: (Int) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState() + + LaunchedEffect(Unit) { + viewModel.fetchAdvertisements() + viewModel.fetchSortedCourses(SortByType.POPULAR) + viewModel.fetchSortedCourses(SortByType.LATEST) + viewModel.fetchNearestDate() + viewModel.fetchUserPoint() + + while (true) { + delay(4000) + coroutineScope.launch { + if (uiState.advertisements.isNotEmpty()) { + val nextPage = (pagerState.currentPage + 1) % uiState.advertisements.size + pagerState.animateScrollToPage(nextPage) + } + } + } + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { homeSideEffect -> + when (homeSideEffect) { + is HomeContract.HomeSideEffect.NavigateToPointHistory -> navigateToPointHistory() + is HomeContract.HomeSideEffect.NavigateToLook -> navigateToLook(MainNavigationBarItemType.LOOK) + is HomeContract.HomeSideEffect.NavigateToTimelineDetail -> navigateToTimelineDetail(homeSideEffect.timelineType, homeSideEffect.timelineId) + is HomeContract.HomeSideEffect.NavigateToEnroll -> navigateToEnroll(homeSideEffect.enrollType, homeSideEffect.viewPath, homeSideEffect.id) + is HomeContract.HomeSideEffect.NavigateToAdvertisementDetail -> navigateToAdvertisementDetail(homeSideEffect.advertisementId) + is HomeContract.HomeSideEffect.NavigateToCourseDetail -> navigateToCourseDetail(homeSideEffect.courseId) + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + HomeScreen( + padding = padding, + uiState = uiState, + pagerState = pagerState, + navigateToEnroll = { viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToEnroll(enrollType = EnrollType.TIMELINE, viewPath = HOME, id = null)) }, + navigateToPointHistory = { viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToPointHistory) }, + navigateToLook = { viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToLook) }, + navigateToTimelineDetail = { timelineType, timelineId -> viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToTimelineDetail(timelineType = timelineType, timelineId = timelineId)) }, + onFabClick = { viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToEnroll(enrollType = EnrollType.COURSE, viewPath = HOME, id = null)) }, + navigateToAdvertisementDetail = { advertisementId: Int -> viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToAdvertisementDetail(advertisementId = advertisementId)) }, + navigateToCourseDetail = { courseId: Int -> viewModel.setSideEffect(HomeContract.HomeSideEffect.NavigateToCourseDetail(courseId = courseId)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun HomeScreen( + padding: PaddingValues, + uiState: HomeContract.HomeUiState, + pagerState: PagerState, + navigateToEnroll: () -> Unit, + navigateToPointHistory: () -> Unit, + navigateToLook: () -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit, + navigateToAdvertisementDetail: (Int) -> Unit, + navigateToCourseDetail: (Int) -> Unit, + onFabClick: () -> Unit +) { + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .background(DateRoadTheme.colors.purple600) + .verticalScroll(rememberScrollState()) + ) { + DateRoadHomeTopBar( + title = uiState.userPoint.point, + profileImage = uiState.userPoint.imageUrl, + onClick = navigateToPointHistory + ) + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 16.dp) + ) { + HomeTimeLineCard( + nearestTimeline = uiState.nearestTimeline, + onClick = { + if (uiState.nearestTimeline == NearestTimeline()) { + navigateToEnroll() + } else { + navigateToTimelineDetail(TimelineType.PINK, uiState.nearestTimeline.timelineId) + } + } + ) + } + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(color = DateRoadTheme.colors.white) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Spacer(modifier = Modifier.height(17.dp)) + Text( + modifier = Modifier.padding(start = 16.dp), + text = PartialColorText( + stringResource(id = R.string.home_hot_date_course_title, uiState.userPoint.name), + keywords = listOf("오늘은", "이런 데이트 코스 어떠세요?"), + color = DateRoadTheme.colors.black + ), + color = DateRoadTheme.colors.purple600, + style = DateRoadTheme.typography.titleExtra24 + ) + Spacer(modifier = Modifier.height(6.dp)) + Row( + modifier = Modifier + .padding(start = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.home_hot_date_course_description), + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.gray400 + ) + DateRoadTextButton( + textContent = stringResource(id = R.string.button_more), + textStyle = DateRoadTheme.typography.bodyBold13, + textColor = DateRoadTheme.colors.purple600, + paddingHorizontal = 20.dp, + paddingVertical = 8.dp, + onClick = navigateToLook + ) + } + Spacer(modifier = Modifier.height(13.dp)) + Row { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(uiState.topLikedCourses) { topLikedCourses -> + HomeHotCourseCard( + course = topLikedCourses, + onClick = { navigateToCourseDetail(topLikedCourses.courseId) } + ) + } + } + } + Spacer(modifier = Modifier.height(30.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + ) { + HorizontalPager( + count = uiState.advertisements.size, + state = pagerState, + modifier = Modifier + .fillMaxWidth() + ) { page -> + HomeAdvertisement( + advertisement = uiState.advertisements[page], + onClick = { navigateToAdvertisementDetail(uiState.advertisements[page].advertisementId) } + ) + } + DateRoadTextTag( + textContent = stringResource( + id = R.string.home_advertisement_number, + pagerState.currentPage + 1, + uiState.advertisements.size + ), + tagContentType = TagType.ADVERTISEMENT_PAGE_NUMBER, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 23.dp, bottom = 6.dp) + ) + } + Spacer(modifier = Modifier.height(32.dp)) + Text( + text = stringResource(id = R.string.home_new_date_course_title), + style = DateRoadTheme.typography.titleExtra20, + color = DateRoadTheme.colors.black, + modifier = Modifier + .padding(start = 16.dp) + .fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.home_new_date_course_description), + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.gray400, + modifier = Modifier.padding(horizontal = 16.dp) + ) + DateRoadTextButton( + textContent = stringResource(id = R.string.button_more), + textStyle = DateRoadTheme.typography.bodyBold13, + textColor = DateRoadTheme.colors.purple600, + paddingHorizontal = 20.dp, + paddingVertical = 8.dp, + onClick = navigateToLook + ) + } + + uiState.latestCourses.forEach { latestCourses -> + DateRoadCourseCard( + course = latestCourses, + onClick = { navigateToCourseDetail(latestCourses.courseId) } + ) + } + } + } + } + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + Alignment.BottomEnd + ) { + DateRoadImageButton( + isEnabled = true, + onClick = { onFabClick() }, + cornerRadius = 44.dp, + paddingHorizontal = 16.dp, + paddingVertical = 16.dp, + modifier = Modifier + .padding(16.dp) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeViewModel.kt new file mode 100644 index 000000000..fd988fda9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/HomeViewModel.kt @@ -0,0 +1,95 @@ +package org.sopt.dateroad.presentation.ui.home + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.domain.type.SortByType +import org.sopt.dateroad.domain.usecase.GetAdvertisementsUseCase +import org.sopt.dateroad.domain.usecase.GetNearestTimelineUseCase +import org.sopt.dateroad.domain.usecase.GetSortedCoursesUseCase +import org.sopt.dateroad.domain.usecase.GetUserPointUseCase +import org.sopt.dateroad.domain.usecase.SetNicknameUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val getAdvertisementsUseCase: GetAdvertisementsUseCase, + private val getNearestTimelineUseCase: GetNearestTimelineUseCase, + private val getSortedCoursesUseCase: GetSortedCoursesUseCase, + private val getUserPointUseCase: GetUserPointUseCase, + private val setNicknameUseCase: SetNicknameUseCase +) : BaseViewModel() { + override fun createInitialState(): HomeContract.HomeUiState = HomeContract.HomeUiState() + + override suspend fun handleEvent(event: HomeContract.HomeEvent) { + when (event) { + is HomeContract.HomeEvent.ChangeBannerPage -> setState { copy(currentBannerPage = event.page) } + is HomeContract.HomeEvent.FetchAdvertisements -> setState { copy(loadState = event.loadState, advertisements = event.advertisements) } + is HomeContract.HomeEvent.FetchLatestCourses -> setState { copy(loadState = event.loadState, latestCourses = event.latestCourses) } + is HomeContract.HomeEvent.FetchTopLikedCourses -> setState { copy(loadState = event.loadState, topLikedCourses = event.topLikedCourses) } + is HomeContract.HomeEvent.FetchNearestTimeline -> setState { copy(loadState = event.loadState, nearestTimeline = event.nearestTimeline) } + is HomeContract.HomeEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } + is HomeContract.HomeEvent.FetchProfileImage -> setState { copy(loadState = loadState, profileImageUrl = event.profileImageUrl) } + } + } + + fun fetchAdvertisements() { + viewModelScope.launch { + setEvent(HomeContract.HomeEvent.FetchAdvertisements(loadState = LoadState.Loading, advertisements = currentState.advertisements)) + getAdvertisementsUseCase() + .onSuccess { advertisements -> + setEvent(HomeContract.HomeEvent.FetchAdvertisements(loadState = LoadState.Success, advertisements = advertisements)) + } + .onFailure { + setEvent(HomeContract.HomeEvent.FetchAdvertisements(loadState = LoadState.Error, advertisements = currentState.advertisements)) + } + } + } + + fun fetchNearestDate() { + viewModelScope.launch { + setEvent(HomeContract.HomeEvent.FetchNearestTimeline(loadState = LoadState.Loading, nearestTimeline = NearestTimeline())) + getNearestTimelineUseCase() + .onSuccess { nearestTimeline -> + setEvent(HomeContract.HomeEvent.FetchNearestTimeline(loadState = LoadState.Success, nearestTimeline = nearestTimeline)) + } + .onFailure { + setEvent(HomeContract.HomeEvent.FetchNearestTimeline(loadState = LoadState.Success, nearestTimeline = NearestTimeline())) + } + } + } + + fun fetchSortedCourses(sortBy: SortByType) { + viewModelScope.launch { + setEvent(HomeContract.HomeEvent.FetchLatestCourses(loadState = LoadState.Loading, latestCourses = currentState.latestCourses)) + getSortedCoursesUseCase(sortBy) + .onSuccess { responseCoursesDto -> + if (sortBy == SortByType.POPULAR) { + setEvent(HomeContract.HomeEvent.FetchTopLikedCourses(loadState = LoadState.Success, topLikedCourses = responseCoursesDto)) + } else { + setEvent(HomeContract.HomeEvent.FetchLatestCourses(loadState = LoadState.Success, latestCourses = responseCoursesDto)) + } + } + .onFailure { + setEvent(HomeContract.HomeEvent.FetchLatestCourses(loadState = LoadState.Error, latestCourses = currentState.latestCourses)) + } + } + } + + fun fetchUserPoint() { + viewModelScope.launch { + setEvent(HomeContract.HomeEvent.FetchUserPoint(loadState = LoadState.Loading, userPoint = currentState.userPoint)) + getUserPointUseCase() + .onSuccess { userPoint -> + setEvent(HomeContract.HomeEvent.FetchUserPoint(loadState = LoadState.Success, userPoint = userPoint)) + setNicknameUseCase(nickname = userPoint.name) + } + .onFailure { + setEvent(HomeContract.HomeEvent.FetchUserPoint(loadState = LoadState.Error, userPoint = currentState.userPoint)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeAdvertisement.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeAdvertisement.kt new file mode 100644 index 000000000..9e34a2dde --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeAdvertisement.kt @@ -0,0 +1,56 @@ +package org.sopt.dateroad.presentation.ui.home.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.domain.model.Advertisement +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable + +@Composable +fun HomeAdvertisement( + advertisement: Advertisement, + onClick: (Int) -> Unit = {} +) { + Box( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .noRippleClickable(onClick = { onClick(advertisement.advertisementId) }) + ) { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(advertisement.thumbnail) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Preview +@Composable +fun HomeAdvertisementPreview() { + Column { + HomeAdvertisement( + advertisement = Advertisement( + advertisementId = 0, + thumbnail = "www.naver.jpg" + ) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeHotCourseCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeHotCourseCard.kt new file mode 100644 index 000000000..074287cd2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeHotCourseCard.kt @@ -0,0 +1,122 @@ +package org.sopt.dateroad.presentation.ui.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun HomeHotCourseCard( + course: Course, + onClick: (Int) -> Unit = {} +) { + Column( + modifier = Modifier + .background(DateRoadTheme.colors.white) + .width(230.dp) + .noRippleClickable(onClick = { onClick(course.courseId) }) + ) { + Text( + text = course.city, + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.white, + modifier = Modifier + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .background(DateRoadTheme.colors.purple500) + .padding(vertical = 4.dp, horizontal = 13.dp) + ) + Box( + modifier = Modifier + .aspectRatio(1f) + ) { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(course.thumbnail) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(topEnd = 14.dp, bottomEnd = 14.dp, bottomStart = 14.dp)) + ) + DateRoadImageTag( + textContent = course.like, + imageContent = R.drawable.ic_tag_heart, + tagContentType = TagType.HEART, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 5.dp, bottom = 5.dp) + ) + } + Spacer(modifier = Modifier.size(6.dp)) + Text( + text = course.title, + style = DateRoadTheme.typography.bodyBold17, + color = DateRoadTheme.colors.black, + modifier = Modifier + .fillMaxWidth(), + maxLines = 2, + minLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.size(8.dp)) + Row { + DateRoadImageTag( + textContent = course.cost, + imageContent = R.drawable.ic_all_money_12, + tagContentType = TagType.MONEY + ) + Spacer(modifier = Modifier.size(6.dp)) + DateRoadImageTag( + textContent = course.duration, + imageContent = R.drawable.ic_all_clock_12, + tagContentType = TagType.TIME + ) + } + } +} + +@Preview +@Composable +fun HomeHotCourseCardPreview() { + Column { + HomeHotCourseCard( + course = Course( + courseId = 1, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "여기 야키니쿠 꼭 먹으러 가세요\n하지만 일본에 있습니다에 있습니다.", + cost = "10만원 이상", + duration = "10시간", + like = "999" + ) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTimeLineCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTimeLineCard.kt new file mode 100644 index 000000000..65c8aaeb1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTimeLineCard.kt @@ -0,0 +1,180 @@ +package org.sopt.dateroad.presentation.ui.home.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.NearestTimeline +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun HomeTimeLineCard( + nearestTimeline: NearestTimeline = NearestTimeline(), + onClick: () -> Unit = {} +) { + val purple600 = DateRoadTheme.colors.purple600 + + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Max) + .clip(RoundedCornerShape(20.dp)) + .background(DateRoadTheme.colors.purple500), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .padding(start = if (nearestTimeline.dateName.isNotEmpty()) 16.dp else 23.dp, end = 9.dp) + .weight(4.5f), + verticalArrangement = Arrangement.Center + ) { + if (nearestTimeline.dateName.isNotEmpty()) { + DateRoadTextTag( + textContent = nearestTimeline.dDay, + tagContentType = TagType.TIMELINE_D_DAY + ) + } + if (nearestTimeline.dateName.isNotEmpty()) { + Spacer(modifier = Modifier.height(7.dp)) + } + Text( + text = if (nearestTimeline.dateName.isNotEmpty()) nearestTimeline.dateName else stringResource(id = R.string.home_timeline_is_not), + style = DateRoadTheme.typography.titleBold20, + color = DateRoadTheme.colors.white, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(bottom = 2.dp) + .fillMaxWidth() + ) + + if (nearestTimeline.dateName.isNotEmpty()) { + Row { + Text( + text = nearestTimeline.date, + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.purple300, + maxLines = 1, + textAlign = if (nearestTimeline.dateName.isEmpty()) TextAlign.Center else TextAlign.Start, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = nearestTimeline.startAt, + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.purple300, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(start = 19.dp) + ) + } + } else { + Text( + text = stringResource(id = R.string.home_timeline_enroll), + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.purple300, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + } + Canvas( + modifier = Modifier + .fillMaxHeight() + .width(2.dp) + ) { + val canvasHeight = size.height + val dotLength = 3.dp.toPx() + val dotSpacing = 3.dp.toPx() + var yOffset = 0f + val strokeWidth = 2.dp.toPx() + + drawCircle( + color = purple600, + radius = strokeWidth * 4, + center = Offset(x = 0f, y = strokeWidth / 2 - 5) + ) + + while (yOffset < canvasHeight) { + drawLine( + color = purple600, + start = Offset(0f, yOffset), + end = Offset(0f, yOffset + dotLength), + strokeWidth = 2.dp.toPx(), + cap = StrokeCap.Butt + ) + yOffset += dotLength + dotSpacing + } + drawCircle( + color = purple600, + radius = strokeWidth * 4, + center = Offset(x = 0f, y = canvasHeight - strokeWidth / 2 + 5) + ) + } + Column( + modifier = Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.height(30.dp)) + Image( + painter = painterResource(id = if (nearestTimeline.dateName.isNotEmpty()) R.drawable.ic_home_right_arrow_purple else R.drawable.ic_home_plus_purple), + contentDescription = null, + modifier = Modifier + .padding(horizontal = 10.dp) + .align(Alignment.CenterHorizontally) + .noRippleClickable { onClick() } + ) + Spacer(modifier = Modifier.height(30.dp)) + } + } +} + +@Preview +@Composable +fun DateRoadDateSchedulePreview() { + Column { + HomeTimeLineCard( + nearestTimeline = NearestTimeline( + timelineId = 1, + dDay = "3", + dateName = "성수 데이트", + date = "2024.06.13", + startAt = "14:00 PM" + ) + ) + HomeTimeLineCard( + nearestTimeline = NearestTimeline() + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTopBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTopBar.kt new file mode 100644 index 000000000..f9f6d0ed1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/component/HomeTopBar.kt @@ -0,0 +1,54 @@ +package org.sopt.dateroad.presentation.ui.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadPointTag +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadHomeTopBar( + title: String = "0 P", + profileImage: String? = null, + onClick: () -> Unit = {} +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(horizontal = 22.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_dateroad_logo), + contentDescription = null, + tint = DateRoadTheme.colors.white + ) + Spacer(modifier = Modifier.weight(1f)) + DateRoadPointTag( + text = title, + profileImage = profileImage, + onClick = onClick + ) + } +} + +@Preview +@Composable +fun DateRoadHomeTopBarPreview() { + Column { + DateRoadHomeTopBar("5000 P") + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/navigation/HomeNavigation.kt new file mode 100644 index 000000000..0f4d121f2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/home/navigation/HomeNavigation.kt @@ -0,0 +1,41 @@ +package org.sopt.dateroad.presentation.ui.home.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MainNavigationBarItemType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.home.HomeRoute + +fun NavController.navigationHome(navOptions: NavOptions) { + navigate( + route = MainNavigationBarRoute.Home::class.simpleName.orEmpty(), + navOptions = navOptions + ) +} + +fun NavGraphBuilder.homeNavGraph( + padding: PaddingValues, + navigateToPointHistory: () -> Unit, + navigateToLook: (MainNavigationBarItemType) -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToAdvertisement: (Int) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + composable(route = MainNavigationBarRoute.Home::class.simpleName.orEmpty()) { + HomeRoute( + padding = padding, + navigateToPointHistory = navigateToPointHistory, + navigateToLook = navigateToLook, + navigateToTimelineDetail = navigateToTimelineDetail, + navigateToEnroll = navigateToEnroll, + navigateToAdvertisementDetail = navigateToAdvertisement, + navigateToCourseDetail = navigateToCourseDetail + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookContract.kt new file mode 100644 index 000000000..26833292f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookContract.kt @@ -0,0 +1,38 @@ +package org.sopt.dateroad.presentation.ui.look + +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class LookContract { + data class LookUiState( + val loadState: LoadState = LoadState.Idle, + val isRegionBottomSheetOpen: Boolean = false, + val region: RegionType? = null, + val area: Any? = null, + val money: MoneyTagType? = null, + val regionBottomSheetSelectedRegion: RegionType? = RegionType.SEOUL, + val regionBottomSheetSelectedArea: Any? = null, + val courses: List = listOf() + ) : UiState + + sealed interface LookSideEffect : UiSideEffect { + data class NavigateToCourseDetail(val courseId: Int) : LookSideEffect + data object NavigateToEnroll : LookSideEffect + } + + sealed class LookEvent : UiEvent { + data object OnAreaButtonClicked : LookEvent() + data object OnResetButtonClicked : LookEvent() + data object OnRegionBottomSheetDismissRequest : LookEvent() + data class FetchCourses(val loadState: LoadState, val courses: List) : LookEvent() + data class OnMoneyChipClicked(val money: MoneyTagType?) : LookEvent() + data class OnRegionBottomSheetButtonClicked(val region: RegionType?, val area: Any?) : LookEvent() + data class OnRegionBottomSheetRegionClicked(val region: RegionType?) : LookEvent() + data class OnRegionBottomSheetAreaClicked(val area: Any?) : LookEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookScreen.kt new file mode 100644 index 000000000..abf4ea273 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookScreen.kt @@ -0,0 +1,260 @@ +package org.sopt.dateroad.presentation.ui.look + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.type.GyeonggiAreaType +import org.sopt.dateroad.domain.type.IncheonAreaType +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.type.SeoulAreaType +import org.sopt.dateroad.presentation.type.ChipType +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadRegionBottomSheet +import org.sopt.dateroad.presentation.ui.component.button.DateRoadAreaButton +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.presentation.ui.component.chip.DateRoadTextChip +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadLeftTitleTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.look.component.LookCourseCard +import org.sopt.dateroad.presentation.util.Default +import org.sopt.dateroad.presentation.util.ViewPath.LOOK +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun LookRoute( + padding: PaddingValues, + viewModel: LookViewModel = hiltViewModel(), + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(uiState.area, uiState.money) { + viewModel.fetchFilteredCourses( + country = uiState.region, + city = when (uiState.area) { + is SeoulAreaType -> (uiState.area as SeoulAreaType) + is GyeonggiAreaType -> (uiState.area as GyeonggiAreaType) + is IncheonAreaType -> (uiState.area as IncheonAreaType) + else -> null + }, + cost = uiState.money + ) + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { lookSideEffect -> + when (lookSideEffect) { + is LookContract.LookSideEffect.NavigateToCourseDetail -> navigateToCourseDetail(lookSideEffect.courseId) + is LookContract.LookSideEffect.NavigateToEnroll -> navigateToEnroll(EnrollType.COURSE, LOOK, null) + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadLoadingView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + LookScreen( + padding = padding, + lookUiState = uiState, + onAreaButtonClicked = { + viewModel.setEvent( + LookContract.LookEvent.OnAreaButtonClicked + ) + }, + onResetButtonClicked = { + viewModel.setEvent( + LookContract.LookEvent.OnResetButtonClicked + ) + }, + onRegionBottomSheetDismissRequest = { + viewModel.setEvent( + LookContract.LookEvent.OnRegionBottomSheetDismissRequest + ) + }, + onMoneyChipClicked = { moneyTagType -> + viewModel.setEvent( + LookContract.LookEvent.OnMoneyChipClicked(money = moneyTagType) + ) + }, + onRegionBottomSheetButtonClicked = { region: RegionType?, area: Any? -> + viewModel.setEvent( + LookContract.LookEvent.OnRegionBottomSheetButtonClicked(region = region, area = area) + ) + }, + onRegionBottomSheetRegionClicked = { region: RegionType? -> + viewModel.setEvent( + LookContract.LookEvent.OnRegionBottomSheetRegionClicked(region = region) + ) + }, + onRegionBottomSheetAreaClicked = { area: Any? -> + viewModel.setEvent( + LookContract.LookEvent.OnRegionBottomSheetAreaClicked(area = area) + ) + }, + onEnrollButtonClicked = { viewModel.setSideEffect(LookContract.LookSideEffect.NavigateToEnroll) }, + onCourseCardClicked = { courseId -> viewModel.setSideEffect(LookContract.LookSideEffect.NavigateToCourseDetail(courseId = courseId)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun LookScreen( + padding: PaddingValues, + lookUiState: LookContract.LookUiState = LookContract.LookUiState(), + onAreaButtonClicked: () -> Unit = {}, + onResetButtonClicked: () -> Unit = {}, + onRegionBottomSheetDismissRequest: () -> Unit = {}, + onMoneyChipClicked: (MoneyTagType?) -> Unit = {}, + onRegionBottomSheetButtonClicked: (RegionType?, Any?) -> Unit = { _, _ -> }, + onRegionBottomSheetRegionClicked: (RegionType?) -> Unit = {}, + onRegionBottomSheetAreaClicked: (Any?) -> Unit = {}, + onEnrollButtonClicked: () -> Unit = {}, + onCourseCardClicked: (Int) -> Unit = {} +) { + Column( + modifier = Modifier + .background(color = DateRoadTheme.colors.white) + .padding(padding) + .fillMaxSize() + ) { + DateRoadLeftTitleTopBar( + title = stringResource(id = R.string.top_bar_title_look), + buttonContent = { + DateRoadImageButton( + isEnabled = true, + onClick = onEnrollButtonClicked, + cornerRadius = 14.dp, + paddingHorizontal = 16.dp, + paddingVertical = 9.dp + ) + } + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(16.dp)) + DateRoadAreaButton( + modifier = Modifier.weight(1f), + isSelected = lookUiState.area != null, + textContent = when (lookUiState.area) { + is SeoulAreaType -> lookUiState.area.title + is GyeonggiAreaType -> lookUiState.area.title + is IncheonAreaType -> lookUiState.area.title + else -> Default.REGION + }, + onClick = onAreaButtonClicked + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + modifier = Modifier + .padding(8.dp) + .noRippleClickable(onClick = onResetButtonClicked), + painter = painterResource(id = R.drawable.ic_all_reset), + contentDescription = null, + tint = DateRoadTheme.colors.gray300 + ) + } + LazyRow( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(MoneyTagType.entries.size) { index -> + DateRoadTextChip( + text = MoneyTagType.entries[index].title, + chipType = ChipType.MONEY, + isSelected = lookUiState.money == MoneyTagType.entries[index], + onSelectedChange = { + onMoneyChipClicked(MoneyTagType.entries[index]) + } + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) + if (lookUiState.courses.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + DateRoadEmptyView(emptyViewType = EmptyViewType.LOOK) + } + } + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(lookUiState.courses.size) { index -> + LookCourseCard(course = lookUiState.courses[index], onClick = { onCourseCardClicked(lookUiState.courses[index].courseId) }) + } + } + } + + DateRoadRegionBottomSheet( + isBottomSheetOpen = lookUiState.isRegionBottomSheetOpen, + isButtonEnabled = lookUiState.regionBottomSheetSelectedRegion != null && lookUiState.regionBottomSheetSelectedArea != null, + selectedRegion = lookUiState.regionBottomSheetSelectedRegion, + onSelectedRegionChanged = { region -> onRegionBottomSheetRegionClicked(region) }, + selectedArea = lookUiState.regionBottomSheetSelectedArea, + onSelectedAreaChanged = { area -> onRegionBottomSheetAreaClicked(area) }, + onButtonClick = { region, area -> + onRegionBottomSheetButtonClicked(region, area) + }, + onDismissRequest = onRegionBottomSheetDismissRequest + ) +} + +@Preview() +@Composable +fun LookScreenPreview() { + DATEROADTheme { + LookScreen(padding = PaddingValues(0.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookViewModel.kt new file mode 100644 index 000000000..343e88292 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/LookViewModel.kt @@ -0,0 +1,69 @@ +package org.sopt.dateroad.presentation.ui.look + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.type.MoneyTagType +import org.sopt.dateroad.domain.type.RegionType +import org.sopt.dateroad.domain.usecase.GetFilteredCourses +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class LookViewModel @Inject constructor( + private val getFilteredCourses: GetFilteredCourses +) : BaseViewModel() { + override fun createInitialState(): LookContract.LookUiState = LookContract.LookUiState() + + override suspend fun handleEvent(event: LookContract.LookEvent) { + when (event) { + is LookContract.LookEvent.OnAreaButtonClicked -> { + setState { copy(isRegionBottomSheetOpen = true, regionBottomSheetSelectedRegion = RegionType.SEOUL, regionBottomSheetSelectedArea = null) } + } + + is LookContract.LookEvent.OnResetButtonClicked -> { + setState { copy(region = null, area = null, money = null) } + } + + is LookContract.LookEvent.OnRegionBottomSheetDismissRequest -> { + setState { copy(isRegionBottomSheetOpen = false) } + } + + is LookContract.LookEvent.FetchCourses -> setState { copy(loadState = event.loadState, courses = event.courses) } + + is LookContract.LookEvent.OnMoneyChipClicked -> { + setState { copy(money = event.money.takeUnless { it == currentState.money }) } + } + + is LookContract.LookEvent.OnRegionBottomSheetButtonClicked -> { + setState { copy(isRegionBottomSheetOpen = false, region = event.region, area = event.area) } + } + + is LookContract.LookEvent.OnRegionBottomSheetRegionClicked -> { + setState { copy(regionBottomSheetSelectedRegion = event.region) } + } + + is LookContract.LookEvent.OnRegionBottomSheetAreaClicked -> { + setState { copy(regionBottomSheetSelectedArea = event.area) } + } + } + } + + fun fetchFilteredCourses(country: RegionType?, city: Any?, cost: MoneyTagType?) { + viewModelScope.launch { + setEvent( + LookContract.LookEvent.FetchCourses(loadState = LoadState.Loading, courses = currentState.courses) + ) + getFilteredCourses(country = country, city = city, cost = cost).onSuccess { courses -> + setEvent( + LookContract.LookEvent.FetchCourses(loadState = LoadState.Success, courses = courses) + ) + }.onFailure { + setEvent( + LookContract.LookEvent.FetchCourses(loadState = LoadState.Error, courses = currentState.courses) + ) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/look/component/LookCourseCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/component/LookCourseCard.kt new file mode 100644 index 000000000..2941cd549 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/component/LookCourseCard.kt @@ -0,0 +1,159 @@ +package org.sopt.dateroad.presentation.ui.look.component + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun LookCourseCard( + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + course: Course, + onClick: (Int) -> Unit = {} +) { + Column( + modifier = modifier + .fillMaxWidth() + .noRippleClickable(onClick = { onClick(course.courseId) }) + ) { + Box { + AsyncImage( + model = ImageRequest.Builder(context = context) + .data(course.thumbnail) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(158f / 140f) + .clip(RoundedCornerShape(14.dp)), + contentScale = ContentScale.Crop + ) + DateRoadImageTag( + textContent = course.like, + imageContent = R.drawable.ic_tag_heart, + tagContentType = TagType.HEART, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 6.dp, bottom = 5.dp) + ) + } + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = course.city, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = course.title, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold15Course, + maxLines = 2, + minLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(5.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_all_money_12), + contentDescription = null + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + modifier = Modifier.weight(1f), + text = course.cost, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.capReg11 + ) + Spacer(modifier = Modifier.width(14.dp)) + Image( + painter = painterResource(id = R.drawable.ic_all_clock_12), + contentDescription = null + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + modifier = Modifier.weight(1f), + text = course.duration, + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.capReg11 + ) + } + } +} + +@Preview +@Composable +fun LookCourseCardPreview() { + DATEROADTheme { + val courses = listOf( + Course( + courseId = 1, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "성수동 당일치기 데이트 코스 둘러보러 가실까요?", + cost = "5만원 이하", + duration = "10시간", + like = "999" + ), + Course( + courseId = 1, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "홍대", + title = "데로 파이띵 !", + cost = "10만원 이하", + duration = "1시간", + like = "3" + ) + ) + + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(courses.size) { index -> + LookCourseCard(course = courses[index]) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/look/navigation/LookNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/navigation/LookNavigation.kt new file mode 100644 index 000000000..4e6dabf23 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/look/navigation/LookNavigation.kt @@ -0,0 +1,27 @@ +package org.sopt.dateroad.presentation.ui.look.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.look.LookRoute + +fun NavController.navigationLook(navOptions: NavOptions) { + navigate( + route = MainNavigationBarRoute.Look::class.simpleName.orEmpty(), + navOptions = navOptions + ) +} + +fun NavGraphBuilder.lookNavGraph( + padding: PaddingValues, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + composable(route = MainNavigationBarRoute.Look::class.simpleName.orEmpty()) { + LookRoute(padding = padding, navigateToEnroll = navigateToEnroll, navigateToCourseDetail = navigateToCourseDetail) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseContract.kt new file mode 100644 index 000000000..ac3a89586 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseContract.kt @@ -0,0 +1,28 @@ +package org.sopt.dateroad.presentation.ui.mycourse + +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class MyCourseContract { + data class MyCourseUiState( + val loadState: LoadState = LoadState.Idle, + val myCourseType: MyCourseType = MyCourseType.READ, + val courses: List = listOf() + ) : UiState + + sealed interface MyCourseSideEffect : UiSideEffect { + data class NavigateToEnroll(val courseId: Int) : MyCourseSideEffect + data class NavigateToCourseDetail(val courseId: Int) : MyCourseSideEffect + data object PopBackStack : MyCourseSideEffect + } + + sealed class MyCourseEvent : UiEvent { + data class FetchMyCourseRead(val loadState: LoadState, val courses: List) : MyCourseEvent() + data class FetchMyCourseEnroll(val loadState: LoadState, val courses: List) : MyCourseEvent() + data class SetMyCourseType(val myCourseType: MyCourseType) : MyCourseEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseScreen.kt new file mode 100644 index 000000000..c17f294db --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseScreen.kt @@ -0,0 +1,198 @@ +package org.sopt.dateroad.presentation.ui.mycourse + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.ui.component.card.DateRoadCourseCard +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.util.MyCourseAmplitude.CLICK_PURCHASED_BACK +import org.sopt.dateroad.presentation.util.MyCourseAmplitude.VIEW_PURCHASED_COURSE +import org.sopt.dateroad.presentation.util.ViewPath.MY_COURSE_READ +import org.sopt.dateroad.presentation.util.amplitude.AmplitudeUtils +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun MyCourseRoute( + padding: PaddingValues, + viewModel: MyCourseViewModel = hiltViewModel(), + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit, + myCourseType: MyCourseType +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.setEvent( + MyCourseContract.MyCourseEvent.SetMyCourseType(myCourseType = myCourseType) + ) + + when (myCourseType) { + MyCourseType.ENROLL -> viewModel.fetchMyCourseEnroll() + MyCourseType.READ -> { + viewModel.fetchMyCourseRead() + AmplitudeUtils.trackEvent(eventName = VIEW_PURCHASED_COURSE) + } + } + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { myCourseSideEffect -> + when (myCourseSideEffect) { + is MyCourseContract.MyCourseSideEffect.NavigateToEnroll -> navigateToEnroll(EnrollType.TIMELINE, MY_COURSE_READ, myCourseSideEffect.courseId) + is MyCourseContract.MyCourseSideEffect.NavigateToCourseDetail -> navigateToCourseDetail(myCourseSideEffect.courseId) + is MyCourseContract.MyCourseSideEffect.PopBackStack -> popBackStack() + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + MyCourseScreen( + padding = padding, + myCourseUiState = uiState, + onIconClick = { + popBackStack() + AmplitudeUtils.trackEvent(CLICK_PURCHASED_BACK) + }, + navigateToEnroll = { courseId -> viewModel.setSideEffect(MyCourseContract.MyCourseSideEffect.NavigateToEnroll(courseId = courseId)) }, + navigateToCourseDetail = { courseId -> viewModel.setSideEffect(MyCourseContract.MyCourseSideEffect.NavigateToCourseDetail(courseId = courseId)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun MyCourseScreen( + padding: PaddingValues, + myCourseUiState: MyCourseContract.MyCourseUiState = MyCourseContract.MyCourseUiState(), + onIconClick: () -> Unit, + navigateToEnroll: (Int) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .background(color = DateRoadTheme.colors.white) + ) { + DateRoadBasicTopBar( + title = stringResource(id = myCourseUiState.myCourseType.topBarTitleRes), + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white, + onLeftIconClick = onIconClick + ) + LazyColumn { + if (myCourseUiState.courses.isEmpty()) { + item { + DateRoadEmptyView( + emptyViewType = when (myCourseUiState.myCourseType) { + MyCourseType.ENROLL -> EmptyViewType.MY_COURSE_ENROLL + MyCourseType.READ -> EmptyViewType.MY_COURSE_READ + } + ) + } + } + items(myCourseUiState.courses) { course -> + DateRoadCourseCard( + course = course, + onClick = { + when (myCourseUiState.myCourseType) { + MyCourseType.ENROLL -> navigateToCourseDetail(course.courseId) + MyCourseType.READ -> navigateToEnroll(course.courseId) + } + } + ) + } + } + } +} + +@Preview +@Composable +fun MyCourseScreenPreview() { + DATEROADTheme { + MyCourseScreen( + padding = PaddingValues(0.dp), + myCourseUiState = MyCourseContract.MyCourseUiState( + loadState = LoadState.Success, + myCourseType = MyCourseType.READ, + courses = listOf( + Course( + courseId = 1, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "여기 야키니쿠 꼭 먹으러 가세요\n하지만 일본에 있습니다.", + cost = "10만원 초과", + duration = "10시간", + like = "99999" + ), + Course( + courseId = 2, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "부천", + title = "여기 야키니쿠 꼭 먹으러 가세요.", + cost = "10만원 초과", + duration = "10시간", + like = "999" + ), + Course( + courseId = 3, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "여기 야키니쿠 꼭 먹으러 가세요\n하지만 일본에 있습니다.하지만 일본에 있습니다.하지만 일본에 있습니다.하지만 일본에 있습니다.하지만 일본에 있습니다.", + cost = "10만원 초과", + duration = "10시간", + like = "999" + ), + Course( + courseId = 4, + thumbnail = "https://avatars.githubusercontent.com/u/103172971?v=4", + city = "건대/성수/왕십리", + title = "여기 야키니쿠 꼭 먹으러 가세요\n하지만 일본에 있습니다.", + cost = "10만원 초과", + duration = "10시간", + like = "999" + ) + ) + ), + onIconClick = {}, + navigateToEnroll = {}, + navigateToCourseDetail = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseViewModel.kt new file mode 100644 index 000000000..1085a8995 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/MyCourseViewModel.kt @@ -0,0 +1,60 @@ +package org.sopt.dateroad.presentation.ui.mycourse + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.usecase.GetMyCourseEnrollUseCase +import org.sopt.dateroad.domain.usecase.GetMyCourseReadUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class MyCourseViewModel @Inject constructor( + private val getMyCourseEnrollUseCase: GetMyCourseEnrollUseCase, + private val getMyCourseReadUseCase: GetMyCourseReadUseCase +) : BaseViewModel() { + override fun createInitialState(): MyCourseContract.MyCourseUiState = MyCourseContract.MyCourseUiState() + + override suspend fun handleEvent(event: MyCourseContract.MyCourseEvent) { + when (event) { + is MyCourseContract.MyCourseEvent.FetchMyCourseEnroll -> setState { copy(loadState = event.loadState, courses = event.courses) } + is MyCourseContract.MyCourseEvent.FetchMyCourseRead -> setState { copy(loadState = event.loadState, courses = event.courses) } + is MyCourseContract.MyCourseEvent.SetMyCourseType -> setState { copy(myCourseType = event.myCourseType) } + } + } + + fun fetchMyCourseRead() { + viewModelScope.launch { + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseRead(loadState = LoadState.Loading, courses = currentState.courses) + ) + getMyCourseReadUseCase().onSuccess { courses -> + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseRead(loadState = LoadState.Success, courses = courses) + ) + }.onFailure { + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseRead(loadState = LoadState.Error, courses = currentState.courses) + ) + } + } + } + + fun fetchMyCourseEnroll() { + viewModelScope.launch { + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseEnroll(loadState = LoadState.Loading, courses = currentState.courses) + ) + getMyCourseEnrollUseCase().onSuccess { courses -> + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseEnroll(loadState = LoadState.Success, courses = courses) + ) + }.onFailure { + setEvent( + MyCourseContract.MyCourseEvent.FetchMyCourseEnroll(loadState = LoadState.Error, courses = currentState.courses) + ) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/navigation/MyCourseNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/navigation/MyCourseNavigation.kt new file mode 100644 index 000000000..3be4c91fc --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mycourse/navigation/MyCourseNavigation.kt @@ -0,0 +1,45 @@ +package org.sopt.dateroad.presentation.ui.mycourse.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.ui.mycourse.MyCourseRoute +import org.sopt.dateroad.presentation.ui.mycourse.navigation.MyCourseRoute.ROUTE_WITH_ARGUMENT + +fun NavController.navigateMyCourses(myCourseType: MyCourseType) { + this.navigate(route = MyCourseRoute.route(myCourseType = myCourseType)) +} + +fun NavGraphBuilder.myCoursesNavGraph( + padding: PaddingValues, + popBackStack: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + composable( + route = ROUTE_WITH_ARGUMENT, + arguments = listOf( + navArgument(MyCourseRoute.ARGUMENT) { + type = NavType.StringType + } + ) + ) { backStackEntry -> + val myCourseType = backStackEntry.arguments?.getString(MyCourseRoute.ARGUMENT)?.let { + MyCourseType.valueOf(it) + } ?: MyCourseType.ENROLL + + MyCourseRoute(padding = padding, popBackStack = popBackStack, myCourseType = myCourseType, navigateToEnroll = navigateToEnroll, navigateToCourseDetail = navigateToCourseDetail) + } +} + +object MyCourseRoute { + private const val ROUTE = "myCourses" + const val ARGUMENT = "myCourseType" + const val ROUTE_WITH_ARGUMENT = "$ROUTE/{$ARGUMENT}" + fun route(myCourseType: MyCourseType) = "$ROUTE/${myCourseType.name}" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageContract.kt new file mode 100644 index 000000000..564b78bab --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageContract.kt @@ -0,0 +1,37 @@ +package org.sopt.dateroad.presentation.ui.mypage + +import org.sopt.dateroad.domain.model.Profile +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class MyPageContract { + data class MyPageUiState( + val loadState: LoadState = LoadState.Idle, + val profile: Profile = Profile(), + val showLogoutDialog: Boolean = false, + val showWithdrawalDialog: Boolean = false, + val showWebView: Boolean = false, + val deleteUserLoadState: LoadState = LoadState.Idle, + val deleteSignOutLoadState: LoadState = LoadState.Idle + ) : UiState + + sealed interface MyPageSideEffect : UiSideEffect { + data object NavigateToEditProfile : MyPageSideEffect + data object NavigateToPointHistory : MyPageSideEffect + data object NavigateToMyCourse : MyPageSideEffect + data object NavigateToPointGuide : MyPageSideEffect + data object NavigateToLogin : MyPageSideEffect + } + + sealed class MyPageEvent : UiEvent { + data class FetchProfile(val loadState: LoadState, val profile: Profile) : MyPageEvent() + data class DeleteLogout(val showLogoutDialog: Boolean, val deleteSignOutLoadState: LoadState) : MyPageEvent() + data class DeleteWithdrawal(val showWithdrawalDialog: Boolean, val deleteUserLoadState: LoadState) : MyPageEvent() + data class SetLogoutDialog(val showLogoutDialog: Boolean) : MyPageEvent() + data class SetWithdrawalDialog(val showWithdrawalDialog: Boolean) : MyPageEvent() + data object OnWebViewClick : MyPageEvent() + data object WebViewClose : MyPageEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageScreen.kt new file mode 100644 index 000000000..8bcdf071f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageScreen.kt @@ -0,0 +1,319 @@ +package org.sopt.dateroad.presentation.ui.mypage + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.sopt.dateroad.R +import org.sopt.dateroad.data.dataremote.util.Point +import org.sopt.dateroad.domain.model.Profile +import org.sopt.dateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.type.MyPageMenuType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.type.TwoButtonDialogType +import org.sopt.dateroad.presentation.type.TwoButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadTextButton +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialog +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadLeftTitleTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadWebView +import org.sopt.dateroad.presentation.ui.mypage.component.MyPageButton +import org.sopt.dateroad.presentation.ui.mypage.component.MyPagePointBox +import org.sopt.dateroad.presentation.util.WebViewUrl.ASK_URL +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun MyPageRoute( + padding: PaddingValues, + viewModel: MyPageViewModel = hiltViewModel(), + navigateToEditProfile: () -> Unit, + navigateToPointHistory: () -> Unit, + navigateToMyCourse: (MyCourseType) -> Unit, + navigateToPointGuide: () -> Unit, + navigateToSignIn: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.fetchProfile() + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { myPageSideEffect -> + when (myPageSideEffect) { + is MyPageContract.MyPageSideEffect.NavigateToEditProfile -> navigateToEditProfile() + is MyPageContract.MyPageSideEffect.NavigateToPointHistory -> navigateToPointHistory() + is MyPageContract.MyPageSideEffect.NavigateToMyCourse -> navigateToMyCourse(MyCourseType.ENROLL) + is MyPageContract.MyPageSideEffect.NavigateToPointGuide -> navigateToPointGuide() + is MyPageContract.MyPageSideEffect.NavigateToLogin -> navigateToSignIn() + } + } + } + + when (uiState.deleteUserLoadState) { + LoadState.Success -> navigateToSignIn() + else -> Unit + } + + when (uiState.deleteSignOutLoadState) { + LoadState.Success -> { + navigateToSignIn() + } + else -> Unit + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + MyPageScreen( + padding = padding, + myPageUiState = uiState, + deleteLogout = { + viewModel.deleteLogout() + }, + deleteWithdrawal = { + viewModel.withdrawal() + }, + navigateToEditProfile = { + viewModel.setSideEffect(MyPageContract.MyPageSideEffect.NavigateToEditProfile) + }, + navigateToPointHistory = { viewModel.setSideEffect(MyPageContract.MyPageSideEffect.NavigateToPointHistory) }, + navigateToMyCourse = { viewModel.setSideEffect(MyPageContract.MyPageSideEffect.NavigateToMyCourse) }, + navigateToPointGuide = { viewModel.setSideEffect(MyPageContract.MyPageSideEffect.NavigateToPointGuide) }, + setLogoutDialog = { showLogoutDialog -> + viewModel.setEvent( + MyPageContract.MyPageEvent.SetLogoutDialog(showLogoutDialog = showLogoutDialog) + ) + }, + setWithdrawalDialog = { showWithdrawalDialog -> + viewModel.setEvent( + MyPageContract.MyPageEvent.SetWithdrawalDialog(showWithdrawalDialog = showWithdrawalDialog) + ) + }, + showWebView = { + viewModel.setEvent(MyPageContract.MyPageEvent.OnWebViewClick) + }, + webViewClose = { viewModel.setEvent(MyPageContract.MyPageEvent.WebViewClose) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun MyPageScreen( + padding: PaddingValues, + myPageUiState: MyPageContract.MyPageUiState = MyPageContract.MyPageUiState(), + deleteLogout: () -> Unit, + deleteWithdrawal: () -> Unit, + navigateToEditProfile: () -> Unit, + navigateToPointHistory: () -> Unit, + navigateToMyCourse: () -> Unit, + navigateToPointGuide: () -> Unit, + setLogoutDialog: (Boolean) -> Unit, + setWithdrawalDialog: (Boolean) -> Unit, + showWebView: (Boolean) -> Unit, + webViewClose: () -> Unit +) { + if (myPageUiState.showWebView) { + DateRoadWebView(url = ASK_URL, onClose = webViewClose) + } else { + Column( + modifier = Modifier + .padding(paddingValues = padding) + .background(color = DateRoadTheme.colors.white) + .fillMaxSize(), + horizontalAlignment = Alignment.End + ) { + Column( + modifier = Modifier + .background(color = DateRoadTheme.colors.gray100, shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp)) + ) { + DateRoadLeftTitleTopBar(title = stringResource(id = R.string.top_bar_title_my_page)) + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + when (myPageUiState.profile.imageUrl) { + null -> { + Image( + modifier = Modifier + .width(44.dp) + .aspectRatio(1f) + .clip(CircleShape), + painter = painterResource(id = R.drawable.img_profile_default), + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + + else -> { + AsyncImage( + modifier = Modifier + .width(44.dp) + .aspectRatio(1f) + .clip(CircleShape), + model = ImageRequest.Builder(context = LocalContext.current) + .data(myPageUiState.profile.imageUrl) + .crossfade(true) + .build(), + placeholder = null, + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + } + Spacer(modifier = Modifier.width(13.dp)) + Text( + text = myPageUiState.profile.name, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24 + ) + Spacer(modifier = Modifier.width(5.dp)) + Icon( + modifier = Modifier.noRippleClickable(onClick = { + navigateToEditProfile() + }), + painter = painterResource(id = R.drawable.ic_my_page_pencil), + contentDescription = null + ) + } + Spacer(modifier = Modifier.height(16.dp)) + LazyRow( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + items(myPageUiState.profile.tag) { tag -> + tag.getDateTagTypeByName()?.let { tagType -> + DateRoadImageTag( + textContent = stringResource(id = tagType.titleRes), + imageContent = tagType.imageRes, + tagContentType = TagType.MY_PAGE_DATE + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + MyPagePointBox( + modifier = Modifier.padding(horizontal = 16.dp), + nickname = myPageUiState.profile.name, + point = myPageUiState.profile.point, + onClick = navigateToPointHistory + ) + Spacer(modifier = Modifier.height(16.dp)) + } + MyPageMenuType.entries.forEach { myPageMenuType -> + MyPageButton( + modifier = Modifier.padding(horizontal = 16.dp), + myPageMenuType = myPageMenuType, + onClick = { + when (myPageMenuType) { + MyPageMenuType.MY_COURSE_ENROLL -> navigateToMyCourse() + MyPageMenuType.POINT_SYSTEM -> navigateToPointGuide() + MyPageMenuType.QUESTION -> showWebView(true) + MyPageMenuType.LOGOUT -> setLogoutDialog(true) + } + } + ) + } + Spacer(modifier = Modifier.weight(1f)) + DateRoadTextButton( + textContent = stringResource(id = R.string.my_page_menu_withdrawal), + textStyle = DateRoadTheme.typography.bodyMed13, + textColor = DateRoadTheme.colors.gray400, + paddingHorizontal = 20.dp, + paddingVertical = 6.dp, + onClick = { setWithdrawalDialog(true) } + ) + Spacer(modifier = Modifier.height(30.dp)) + } + + if (myPageUiState.showLogoutDialog) { + DateRoadTwoButtonDialog( + twoButtonDialogType = TwoButtonDialogType.LOGOUT, + onDismissRequest = { setLogoutDialog(false) }, + onClickConfirm = deleteLogout, + onClickDismiss = { setLogoutDialog(false) } + ) + } + + if (myPageUiState.showWithdrawalDialog) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.WITHDRAWAL, + onDismissRequest = { setWithdrawalDialog(false) }, + onClickConfirm = { setWithdrawalDialog(false) }, + onClickDismiss = { deleteWithdrawal() } + ) + } + } +} + +@Preview +@Composable +fun MyPageScreenPreview() { + DATEROADTheme { + MyPageScreen( + padding = PaddingValues(0.dp), + myPageUiState = MyPageContract.MyPageUiState( + profile = Profile("지현", listOf("드라이브", "쇼핑", "실내"), "200 $Point") + ), + deleteLogout = {}, + deleteWithdrawal = {}, + navigateToEditProfile = {}, + navigateToPointHistory = {}, + navigateToMyCourse = {}, + navigateToPointGuide = {}, + setLogoutDialog = {}, + setWithdrawalDialog = {}, + showWebView = {}, + webViewClose = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageViewModel.kt new file mode 100644 index 000000000..1b6606715 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/MyPageViewModel.kt @@ -0,0 +1,71 @@ +package org.sopt.dateroad.presentation.ui.mypage + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.usecase.ClearUserInfoUseCase +import org.sopt.dateroad.domain.usecase.DeleteSignOutUseCase +import org.sopt.dateroad.domain.usecase.DeleteWithdrawUseCase +import org.sopt.dateroad.domain.usecase.GetUserUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class MyPageViewModel @Inject constructor( + private val clearUserInfoUseCase: ClearUserInfoUseCase, + private val deleteWithdrawUserUseCase: DeleteWithdrawUseCase, + private val deleteSignOutUseCase: DeleteSignOutUseCase, + private val getUserUseCase: GetUserUseCase +) : BaseViewModel() { + + override fun createInitialState(): MyPageContract.MyPageUiState = + MyPageContract.MyPageUiState() + + override suspend fun handleEvent(event: MyPageContract.MyPageEvent) { + when (event) { + is MyPageContract.MyPageEvent.DeleteLogout -> setState { copy(deleteSignOutLoadState = event.deleteSignOutLoadState) } + is MyPageContract.MyPageEvent.DeleteWithdrawal -> setState { copy(showWithdrawalDialog = event.showWithdrawalDialog, deleteUserLoadState = event.deleteUserLoadState) } + is MyPageContract.MyPageEvent.SetLogoutDialog -> setState { copy(showLogoutDialog = event.showLogoutDialog) } + is MyPageContract.MyPageEvent.SetWithdrawalDialog -> setState { copy(showWithdrawalDialog = event.showWithdrawalDialog) } + is MyPageContract.MyPageEvent.FetchProfile -> setState { copy(loadState = event.loadState, profile = event.profile) } + is MyPageContract.MyPageEvent.OnWebViewClick -> setState { copy(showWebView = true) } + is MyPageContract.MyPageEvent.WebViewClose -> setState { copy(showWebView = false) } + } + } + + fun fetchProfile() { + viewModelScope.launch { + setEvent(MyPageContract.MyPageEvent.FetchProfile(loadState = LoadState.Loading, profile = currentState.profile)) + getUserUseCase().onSuccess { profile -> + setEvent(MyPageContract.MyPageEvent.FetchProfile(loadState = LoadState.Success, profile = profile)) + }.onFailure { + setEvent(MyPageContract.MyPageEvent.FetchProfile(loadState = LoadState.Error, profile = currentState.profile)) + } + } + } + + fun deleteLogout() { + viewModelScope.launch { + setEvent(MyPageContract.MyPageEvent.DeleteLogout(showLogoutDialog = false, deleteSignOutLoadState = LoadState.Loading)) + deleteSignOutUseCase().onSuccess { + setEvent(MyPageContract.MyPageEvent.DeleteLogout(showLogoutDialog = false, deleteSignOutLoadState = LoadState.Success)) + clearUserInfoUseCase() + }.onFailure { + setEvent(MyPageContract.MyPageEvent.DeleteLogout(showLogoutDialog = false, deleteSignOutLoadState = LoadState.Error)) + } + } + } + + fun withdrawal(authCode: String? = null) { + viewModelScope.launch { + setEvent(MyPageContract.MyPageEvent.DeleteWithdrawal(showWithdrawalDialog = true, deleteUserLoadState = LoadState.Loading)) + deleteWithdrawUserUseCase(authCode).onSuccess { + setEvent(MyPageContract.MyPageEvent.DeleteWithdrawal(showWithdrawalDialog = false, deleteUserLoadState = LoadState.Success)) + clearUserInfoUseCase() + }.onFailure { + setEvent(MyPageContract.MyPageEvent.DeleteWithdrawal(showWithdrawalDialog = false, deleteUserLoadState = LoadState.Error)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPageButton.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPageButton.kt new file mode 100644 index 000000000..4e0ca7056 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPageButton.kt @@ -0,0 +1,58 @@ +package org.sopt.dateroad.presentation.ui.mypage.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.MyPageMenuType +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun MyPageButton( + modifier: Modifier = Modifier, + myPageMenuType: MyPageMenuType, + onClick: () -> Unit = {} +) { + Row( + modifier = modifier + .padding(top = 19.dp, bottom = 20.dp) + .fillMaxWidth() + .noRippleClickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(id = myPageMenuType.titleRes), style = DateRoadTheme.typography.bodySemi15, color = DateRoadTheme.colors.black, modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(20.dp)) + Icon(painter = painterResource(id = myPageMenuType.iconRes), contentDescription = null) + } +} + +@Preview +@Composable +fun DateRoadMyPageButtonPreview() { + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + MyPageButton(myPageMenuType = MyPageMenuType.MY_COURSE_ENROLL) + MyPageButton(myPageMenuType = MyPageMenuType.POINT_SYSTEM) + MyPageButton(myPageMenuType = MyPageMenuType.QUESTION) + MyPageButton(myPageMenuType = MyPageMenuType.LOGOUT) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPagePointBox.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPagePointBox.kt new file mode 100644 index 000000000..131640b68 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/component/MyPagePointBox.kt @@ -0,0 +1,87 @@ +package org.sopt.dateroad.presentation.ui.mypage.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.data.dataremote.util.Point.POINT +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun MyPagePointBox( + modifier: Modifier = Modifier, + nickname: String, + point: String, + onClick: () -> Unit = {} +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.white) + .padding(start = 14.dp, top = 18.dp, bottom = 11.dp, end = 10.dp), + verticalAlignment = Alignment.Bottom + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(id = R.string.point_box_nickname, nickname), + color = DateRoadTheme.colors.gray400, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(9.dp)) + Text( + text = point, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.titleExtra24, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(3.dp)) + } + Spacer(modifier = Modifier.width(25.dp)) + Row( + modifier = Modifier + .padding(start = 14.dp, end = 5.dp, top = 5.dp, bottom = 5.dp) + .noRippleClickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.my_page_point_card_text), + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.gray400 + ) + Spacer(modifier = Modifier.width(10.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_my_page_point_record_arrow), + contentDescription = null, + tint = DateRoadTheme.colors.gray400 + ) + } + } +} + +@Preview +@Composable +fun DateRoadMyPagePointBoxPreview() { + MyPagePointBox(nickname = "호은", point = "200 $POINT") +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/navigation/MyPageNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/navigation/MyPageNavigation.kt new file mode 100644 index 000000000..5c97d553c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/mypage/navigation/MyPageNavigation.kt @@ -0,0 +1,37 @@ +package org.sopt.dateroad.presentation.ui.mypage.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.ui.mypage.MyPageRoute + +fun NavController.navigationMyPage(navOptions: NavOptions) { + navigate( + route = MainNavigationBarRoute.MyPage::class.simpleName.orEmpty(), + navOptions = navOptions + ) +} + +fun NavGraphBuilder.myPageNavGraph( + padding: PaddingValues, + navigateToPointHistory: () -> Unit, + navigateToMyCourse: (MyCourseType) -> Unit, + navigateToPointGuide: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToEditProfile: () -> Unit +) { + composable(route = MainNavigationBarRoute.MyPage::class.simpleName.orEmpty()) { + MyPageRoute( + padding = padding, + navigateToPointHistory = navigateToPointHistory, + navigateToMyCourse = navigateToMyCourse, + navigateToPointGuide = navigateToPointGuide, + navigateToSignIn = navigateToSignIn, + navigateToEditProfile = navigateToEditProfile + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainActivity.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainActivity.kt new file mode 100644 index 000000000..19e5129ed --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainActivity.kt @@ -0,0 +1,46 @@ +package org.sopt.dateroad.presentation.ui.navigator + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import org.sopt.dateroad.presentation.ui.splash.SplashScreen +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + installSplashScreen().setOnExitAnimationListener { splashScreenView -> + splashScreenView.remove() + } + setContent { + val navigator: MainNavigator = rememberMainNavigator() + var showSplash by remember { mutableStateOf(true) } + + DATEROADTheme { + LaunchedEffect(Unit) { + delay(SPLASH_SCREEN_DELAY) + showSplash = false + } + if (showSplash) { + SplashScreen() + } else { + MainScreen(navigator = navigator) + } + } + } + } + + companion object { + const val SPLASH_SCREEN_DELAY = 2000L + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainNavigator.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainNavigator.kt new file mode 100644 index 000000000..c84f5a165 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainNavigator.kt @@ -0,0 +1,163 @@ +package org.sopt.dateroad.presentation.ui.navigator + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.model.Route +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.MainNavigationBarItemType +import org.sopt.dateroad.presentation.type.MyCourseType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.advertisement.navigation.navigationAdvertisement +import org.sopt.dateroad.presentation.ui.coursedetail.navigation.navigationCourseDetail +import org.sopt.dateroad.presentation.ui.enroll.navigation.navigationEnroll +import org.sopt.dateroad.presentation.ui.home.navigation.navigationHome +import org.sopt.dateroad.presentation.ui.look.navigation.navigationLook +import org.sopt.dateroad.presentation.ui.mycourse.navigation.navigateMyCourses +import org.sopt.dateroad.presentation.ui.mypage.navigation.navigationMyPage +import org.sopt.dateroad.presentation.ui.onboarding.navigation.navigationOnboarding +import org.sopt.dateroad.presentation.ui.past.navigation.navigationPast +import org.sopt.dateroad.presentation.ui.pointguide.navigation.navigationPointGuide +import org.sopt.dateroad.presentation.ui.pointhistory.navigation.navigationPointHistory +import org.sopt.dateroad.presentation.ui.profile.navigation.navigationEditProfile +import org.sopt.dateroad.presentation.ui.profile.navigation.navigationEnrollProfile +import org.sopt.dateroad.presentation.ui.read.navigation.navigationRead +import org.sopt.dateroad.presentation.ui.signin.navigation.SignInRoute +import org.sopt.dateroad.presentation.ui.signin.navigation.navigationSignIn +import org.sopt.dateroad.presentation.ui.timeline.navigation.navigationTimeline +import org.sopt.dateroad.presentation.ui.timelinedetail.navigation.navigateToTimelineDetail + +class MainNavigator( + val navHostController: NavHostController +) { + private val currentDestination: NavDestination? + @Composable get() = navHostController.currentBackStackEntryAsState().value?.destination + + val startDestination = SignInRoute.ROUTE + + val currentMainNavigationBarItem: MainNavigationBarItemType? + @Composable get() = MainNavigationBarItemType.find { mainNavigationBarRoute -> + currentDestination?.route == mainNavigationBarRoute::class.simpleName + } + + fun navigateMainNavigation(mainNavigationBarItemType: MainNavigationBarItemType) { + navOptions { + popUpTo(MainNavigationBarRoute.Home::class.simpleName.orEmpty()) { + saveState = true + } + launchSingleTop = true + restoreState = true + }.let { navOptions -> + when (mainNavigationBarItemType) { + MainNavigationBarItemType.HOME -> navHostController.navigationHome(navOptions) + MainNavigationBarItemType.LOOK -> navHostController.navigationLook(navOptions) + MainNavigationBarItemType.TIMELINE -> navHostController.navigationTimeline(navOptions) + MainNavigationBarItemType.READ -> navHostController.navigationRead(navOptions) + MainNavigationBarItemType.MY_PAGE -> navHostController.navigationMyPage(navOptions) + // TODO:MainNavigationBarItemType.SEARCH -> navHostController.navigationDummy(navOptions) + } + } + } + + fun navigateToAdvertisement(advertisementId: Int) { + navHostController.navigationAdvertisement(advertisementId = advertisementId) + } + + fun navigateToCourseDetail(courseId: Int) { + navHostController.navigationCourseDetail(courseId = courseId) + } + + fun navigateToEnroll(enrollType: EnrollType, viewPath: String, courseId: Int?) { + navHostController.navigationEnroll(enrollType = enrollType, viewPath = viewPath, courseId = courseId) + } + + fun navigateToMyPage(navOptions: NavOptions? = null) { + navHostController.navigationMyPage( + navOptions ?: navOptions { + popUpTo(navHostController.graph.findStartDestination().id) { + inclusive = true + } + launchSingleTop = true + } + ) + } + + fun navigateToHome(navOptions: NavOptions? = null) { + navHostController.navigationHome( + navOptions ?: navOptions { + popUpTo(navHostController.graph.findStartDestination().id) { + inclusive = true + } + launchSingleTop = true + } + ) + } + + fun navigateToMyCourse(myCourseType: MyCourseType) { + navHostController.navigateMyCourses(myCourseType = myCourseType) + } + + fun navigateToOnboarding() { + navHostController.navigationOnboarding() + } + + fun navigateToPast() { + navHostController.navigationPast() + } + + fun navigateToPointGuide() { + navHostController.navigationPointGuide() + } + + fun navigateToPointHistory() { + navHostController.navigationPointHistory() + } + + fun navigateToEnrollProfile() { + navHostController.navigationEnrollProfile() + } + + fun navigateToEditProfile() { + navHostController.navigationEditProfile() + } + + fun navigateToSignIn() { + navHostController.navigationSignIn() + } + + fun navigateToTimelineDetail(timelineType: TimelineType, dateId: Int) { + navHostController.navigateToTimelineDetail(timelineType, dateId) + } + + private fun popBackStack() { + navHostController.popBackStack() + } + + fun popBackStackIfNotHome() { + if (!isSameCurrentDestination()) { + popBackStack() + } + } + + private inline fun isSameCurrentDestination(): Boolean = + navHostController.currentDestination?.route == T::class.simpleName + + @Composable + fun showBottomBar(): Boolean = MainNavigationBarItemType.contains { + currentDestination?.route == it::class.simpleName + } +} + +@Composable +fun rememberMainNavigator( + navHostController: NavHostController = rememberNavController() +): MainNavigator = remember(navHostController) { + MainNavigator(navHostController = navHostController) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainScreen.kt new file mode 100644 index 000000000..9e78684d2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/MainScreen.kt @@ -0,0 +1,51 @@ +package org.sopt.dateroad.presentation.ui.navigator + +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import org.sopt.dateroad.presentation.type.MainNavigationBarItemType +import org.sopt.dateroad.presentation.ui.navigator.component.MainBottomBar +import org.sopt.dateroad.presentation.ui.navigator.component.MainNavHost +import org.sopt.dateroad.ui.theme.DATEROADTheme + +@Composable +fun MainScreen( + navigator: MainNavigator = rememberMainNavigator() +) { + MainScreenContent( + navigator = navigator + ) +} + +@Composable +private fun MainScreenContent( + modifier: Modifier = Modifier, + navigator: MainNavigator +) { + Scaffold( + modifier = modifier, + content = { padding -> + MainNavHost( + navigator = navigator, + padding = padding + ) + }, + bottomBar = { + MainBottomBar( + isVisible = navigator.showBottomBar(), + navigationBarItems = MainNavigationBarItemType.entries.toList(), + currentNavigationBarItem = navigator.currentMainNavigationBarItem, + onNavigationBarItemSelected = { navigator.navigateMainNavigation(it) } + ) + } + ) +} + +@Preview(showBackground = true) +@Composable +fun MainPreview() { + DATEROADTheme { + MainScreen() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainBottomBar.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainBottomBar.kt new file mode 100644 index 000000000..a37d40f36 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainBottomBar.kt @@ -0,0 +1,105 @@ +package org.sopt.dateroad.presentation.ui.navigator.component + +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.MainNavigationBarItemType +import org.sopt.dateroad.ui.theme.Black +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme +import org.sopt.dateroad.ui.theme.Gray200 +import org.sopt.dateroad.ui.theme.Gray300 +import org.sopt.dateroad.ui.theme.White + +@Composable +fun CustomNavigationBarItem( + context: Context, + mainNavigationBarItemType: MainNavigationBarItemType, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clickable(onClick = onClick) + .background(if (isSelected) White else Color.Transparent), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.height(11.dp)) + Icon( + painter = painterResource(id = mainNavigationBarItemType.iconRes), + tint = if (isSelected) Black else Gray200, + contentDescription = context.getString(mainNavigationBarItemType.label) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = context.getString(mainNavigationBarItemType.label), + style = DateRoadTheme.typography.capReg11, + color = if (isSelected) Black else Gray300 + ) + Spacer(modifier = Modifier.height(6.dp)) + } +} + +@Composable +fun MainBottomBar( + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + isVisible: Boolean, + navigationBarItems: List, + currentNavigationBarItem: MainNavigationBarItemType?, + onNavigationBarItemSelected: (MainNavigationBarItemType) -> Unit +) { + AnimatedVisibility(visible = isVisible) { + Row( + modifier = modifier + .background(White) + .border(1.dp, DateRoadTheme.colors.gray100) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + navigationBarItems.forEach { mainNavigationBarItemType -> + CustomNavigationBarItem( + context = context, + mainNavigationBarItemType = mainNavigationBarItemType, + isSelected = currentNavigationBarItem == mainNavigationBarItemType, + onClick = { onNavigationBarItemSelected(mainNavigationBarItemType) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Preview +@Composable +private fun MainBottomBarPreview() { + DATEROADTheme { + MainBottomBar( + isVisible = true, + navigationBarItems = MainNavigationBarItemType.entries.toList(), + currentNavigationBarItem = MainNavigationBarItemType.HOME, + onNavigationBarItemSelected = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainNavHost.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainNavHost.kt new file mode 100644 index 000000000..d2ea98f24 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/navigator/component/MainNavHost.kt @@ -0,0 +1,150 @@ +package org.sopt.dateroad.presentation.ui.navigator.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import org.sopt.dateroad.presentation.type.ProfileType +import org.sopt.dateroad.presentation.ui.advertisement.navigation.advertisementGraph +import org.sopt.dateroad.presentation.ui.coursedetail.navigation.courseDetailGraph +import org.sopt.dateroad.presentation.ui.enroll.navigation.enrollNavGraph +import org.sopt.dateroad.presentation.ui.home.navigation.homeNavGraph +import org.sopt.dateroad.presentation.ui.look.navigation.lookNavGraph +import org.sopt.dateroad.presentation.ui.mycourse.navigation.myCoursesNavGraph +import org.sopt.dateroad.presentation.ui.mypage.navigation.myPageNavGraph +import org.sopt.dateroad.presentation.ui.navigator.MainNavigator +import org.sopt.dateroad.presentation.ui.onboarding.navigation.onboardingNavGraph +import org.sopt.dateroad.presentation.ui.past.navigation.pastNavGraph +import org.sopt.dateroad.presentation.ui.pointguide.navigation.pointGuideGraph +import org.sopt.dateroad.presentation.ui.pointhistory.navigation.pointHistoryGraph +import org.sopt.dateroad.presentation.ui.profile.navigation.editProfileNavGraph +import org.sopt.dateroad.presentation.ui.profile.navigation.enrollProfileNavGraph +import org.sopt.dateroad.presentation.ui.read.navigation.readNavGraph +import org.sopt.dateroad.presentation.ui.signin.navigation.signInGraph +import org.sopt.dateroad.presentation.ui.timeline.navigation.timelineNavGraph +import org.sopt.dateroad.presentation.ui.timelinedetail.navigation.timelineDetailGraph +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun MainNavHost( + modifier: Modifier = Modifier, + navigator: MainNavigator, + padding: PaddingValues +) { + Box( + modifier = modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + NavHost( + navController = navigator.navHostController, + startDestination = navigator.startDestination + ) { + advertisementGraph( + popBackStack = navigator::popBackStackIfNotHome + ) + + courseDetailGraph( + popBackStack = navigator::popBackStackIfNotHome, + navigateToEnroll = navigator::navigateToEnroll + ) + + enrollNavGraph( + padding = padding, + popBackStack = navigator::popBackStackIfNotHome, + navigationToMyCourse = navigator::navigateToMyCourse + ) + + homeNavGraph( + padding = padding, + navigateToPointHistory = navigator::navigateToPointHistory, + navigateToLook = navigator::navigateMainNavigation, + navigateToTimelineDetail = navigator::navigateToTimelineDetail, + navigateToEnroll = navigator::navigateToEnroll, + navigateToAdvertisement = navigator::navigateToAdvertisement, + navigateToCourseDetail = navigator::navigateToCourseDetail + ) + + lookNavGraph( + padding = padding, + navigateToEnroll = navigator::navigateToEnroll, + navigateToCourseDetail = navigator::navigateToCourseDetail + ) + + myCoursesNavGraph( + padding = padding, + popBackStack = navigator::popBackStackIfNotHome, + navigateToEnroll = navigator::navigateToEnroll, + navigateToCourseDetail = navigator::navigateToCourseDetail + ) + + myPageNavGraph( + padding = padding, + navigateToPointHistory = navigator::navigateToPointHistory, + navigateToMyCourse = navigator::navigateToMyCourse, + navigateToPointGuide = navigator::navigateToPointGuide, + navigateToSignIn = navigator::navigateToSignIn, + navigateToEditProfile = navigator::navigateToEditProfile + ) + + onboardingNavGraph( + navigateToEnrollProfile = navigator::navigateToEnrollProfile, + navigateToSignIn = navigator::navigateToSignIn + ) + + pastNavGraph( + padding = padding, + popBackStack = navigator::popBackStackIfNotHome, + navigateToTimelineDetail = navigator::navigateToTimelineDetail + ) + + pointGuideGraph( + padding = padding, + popBackStack = navigator::popBackStackIfNotHome + ) + + pointHistoryGraph( + padding = padding, + popBackStack = navigator::popBackStackIfNotHome + ) + + enrollProfileNavGraph( + navigateToHome = navigator::navigateToHome, + navigateToMyPage = navigator::navigateToMyPage, + profileType = ProfileType.ENROLL, + popBackStack = navigator::popBackStackIfNotHome + ) + editProfileNavGraph( + navigateToHome = navigator::navigateToHome, + navigateToMyPage = navigator::navigateToMyPage, + profileType = ProfileType.EDIT, + popBackStack = navigator::popBackStackIfNotHome + ) + + readNavGraph( + padding = padding, + navigateToEnroll = navigator::navigateToEnroll, + navigateToCourseDetail = navigator::navigateToCourseDetail + ) + + signInGraph( + navigateToOnboarding = navigator::navigateToOnboarding, + navigateToHome = navigator::navigateToHome + ) + + timelineNavGraph( + padding = padding, + navigateToPast = navigator::navigateToPast, + navigateToEnroll = navigator::navigateToEnroll, + navigateToTimelineDetail = navigator::navigateToTimelineDetail + ) + + timelineDetailGraph( + popBackStack = navigator::popBackStackIfNotHome + ) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingContract.kt new file mode 100644 index 000000000..e5387a838 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingContract.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad.presentation.ui.onboarding + +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState + +class OnBoardingContract { + class OnBoardingUiState : UiState + + sealed interface OnBoardingSideEffect : UiSideEffect { + data object NavigateToProfile : OnBoardingSideEffect + data object NavigateToSignIn : OnBoardingSideEffect + } + + sealed class OnBoardingEvent : UiEvent +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingViewModel.kt new file mode 100644 index 000000000..6acf7bac9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnBoardingViewModel.kt @@ -0,0 +1,13 @@ +package org.sopt.dateroad.presentation.ui.onboarding + +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import org.sopt.dateroad.presentation.util.base.BaseViewModel + +@HiltViewModel +class OnBoardingViewModel @Inject constructor() : BaseViewModel() { + override fun createInitialState(): OnBoardingContract.OnBoardingUiState = + OnBoardingContract.OnBoardingUiState() + + override suspend fun handleEvent(event: OnBoardingContract.OnBoardingEvent) {} +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnboardingScreen.kt new file mode 100644 index 000000000..945cafdeb --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/OnboardingScreen.kt @@ -0,0 +1,174 @@ +package org.sopt.dateroad.presentation.ui.onboarding + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.OnboardingType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.presentation.ui.component.dotsindicator.DotsIndicator +import org.sopt.dateroad.presentation.ui.component.partialcolortext.PartialColorText +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun OnboardingRoute( + viewModel: OnBoardingViewModel = hiltViewModel(), + navigateToProfile: () -> Unit, + navigateToSignIn: () -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState() + + BackHandler { + when (pagerState.currentPage) { + 0 -> viewModel.setSideEffect(OnBoardingContract.OnBoardingSideEffect.NavigateToSignIn) + + else -> { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + } + } + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { onBoardingSideEffect -> + when (onBoardingSideEffect) { + is OnBoardingContract.OnBoardingSideEffect.NavigateToProfile -> navigateToProfile() + is OnBoardingContract.OnBoardingSideEffect.NavigateToSignIn -> navigateToSignIn() + } + } + } + + OnboardingScreen( + pagerState = pagerState, + coroutineScope = coroutineScope, + onEnrollProfileButtonClicked = { viewModel.setSideEffect(OnBoardingContract.OnBoardingSideEffect.NavigateToProfile) } + ) +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun OnboardingScreen( + pagerState: PagerState = rememberPagerState(), + coroutineScope: CoroutineScope = rememberCoroutineScope(), + onEnrollProfileButtonClicked: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + HorizontalPager( + count = OnboardingType.entries.size, + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { page -> + val onboardingType = OnboardingType.entries[page] + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = painterResource(id = onboardingType.imageRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + Spacer(modifier = Modifier.height(22.dp)) + Text( + text = PartialColorText( + stringResource(id = onboardingType.titleRes), + keywords = if (page == 0) { listOf("포인트", "데이트 코스", "100", "다양한") } else listOf("100 포인트", "다양한"), + color = DateRoadTheme.colors.purple600 + ), + style = DateRoadTheme.typography.titleExtra24, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = onboardingType.descriptionRes), + style = DateRoadTheme.typography.bodySemi13, + color = DateRoadTheme.colors.gray500, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = stringResource(id = onboardingType.subDescriptionRes), + style = DateRoadTheme.typography.capReg11, + color = DateRoadTheme.colors.gray400, + textAlign = TextAlign.Center + ) + } + } + Spacer(modifier = Modifier.height(14.dp)) + DateRoadFilledButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(242.dp), + isEnabled = true, + textContent = + if (pagerState.currentPage == OnboardingType.entries.size - 1) { + stringResource(id = R.string.onboarding_profile_enroll) + } else { + stringResource(id = R.string.onboarding_next) + }, + onClick = { + if (pagerState.currentPage == OnboardingType.entries.size - 1) { + onEnrollProfileButtonClicked() + } else { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + }, + textStyle = DateRoadTheme.typography.bodyBold15, + enabledBackgroundColor = DateRoadTheme.colors.purple600, + enabledTextColor = DateRoadTheme.colors.white, + disabledBackgroundColor = DateRoadTheme.colors.gray200, + disabledTextColor = DateRoadTheme.colors.gray400, + cornerRadius = 29.dp, + paddingHorizontal = 0.dp, + paddingVertical = 16.dp + ) + Spacer(modifier = Modifier.height(15.dp)) + DotsIndicator( + totalDots = OnboardingType.entries.size, + selectedIndex = pagerState.currentPage, + indicatorSize = 8.dp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(15.dp)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/navigation/OnboardingNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/navigation/OnboardingNavigation.kt new file mode 100644 index 000000000..df9c2ebd5 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/onboarding/navigation/OnboardingNavigation.kt @@ -0,0 +1,28 @@ +package org.sopt.dateroad.presentation.ui.onboarding.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.ui.onboarding.OnboardingRoute + +fun NavController.navigationOnboarding() { + navigate( + route = OnboardingRoute.ROUTE + ) +} + +fun NavGraphBuilder.onboardingNavGraph( + navigateToEnrollProfile: () -> Unit, + navigateToSignIn: () -> Unit +) { + composable(route = OnboardingRoute.ROUTE) { + OnboardingRoute( + navigateToProfile = navigateToEnrollProfile, + navigateToSignIn = navigateToSignIn + ) + } +} + +object OnboardingRoute { + const val ROUTE = "onboarding" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastContract.kt new file mode 100644 index 000000000..cdaeeb93b --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastContract.kt @@ -0,0 +1,24 @@ +package org.sopt.dateroad.presentation.ui.past + +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class PastContract { + data class PastUiState( + val loadState: LoadState = LoadState.Idle, + val timelines: List = listOf() + ) : UiState + + sealed interface PastSideEffect : UiSideEffect { + data object PopBackStack : PastSideEffect + data class NavigateToTimelineDetail(val timelineType: TimelineType, val timelineId: Int) : PastSideEffect + } + + sealed class PastEvent : UiEvent { + data class FetchPastDate(val loadState: LoadState, val timelines: List) : PastEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastScreen.kt new file mode 100644 index 000000000..2966406ae --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastScreen.kt @@ -0,0 +1,124 @@ +package org.sopt.dateroad.presentation.ui.past + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.type.TimelineTimeType +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.past.component.PastCard +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun PastRoute( + padding: PaddingValues, + viewModel: PastViewModel = hiltViewModel(), + popBackStack: () -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.fetchPastDate(TimelineTimeType.PAST) + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle).collect { pastSideEffect -> + when (pastSideEffect) { + is PastContract.PastSideEffect.PopBackStack -> popBackStack() + is PastContract.PastSideEffect.NavigateToTimelineDetail -> navigateToTimelineDetail(pastSideEffect.timelineType, pastSideEffect.timelineId) + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + PastScreen( + padding = padding, + pastUiState = uiState, + popBackStack = { viewModel.setSideEffect(PastContract.PastSideEffect.PopBackStack) }, + navigateToTimelineDetail = { timelineType, timelineId -> viewModel.setSideEffect(PastContract.PastSideEffect.NavigateToTimelineDetail(timelineType = timelineType, timelineId = timelineId)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun PastScreen( + padding: PaddingValues, + pastUiState: PastContract.PastUiState = PastContract.PastUiState(), + popBackStack: () -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit +) { + Column( + modifier = Modifier + .padding(paddingValues = padding) + .background(color = DateRoadTheme.colors.white) + .fillMaxSize() + ) { + DateRoadBasicTopBar( + title = stringResource(id = R.string.top_bar_title_past), + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white, + onLeftIconClick = popBackStack + ) + if (pastUiState.timelines.isEmpty()) { + DateRoadEmptyView(emptyViewType = EmptyViewType.PAST) + } else { + LazyColumn( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 6.dp, bottom = 11.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(pastUiState.timelines.size) { index -> + PastCard( + timeline = pastUiState.timelines[index], + timelineType = TimelineType.getTimelineTypeByIndex(index), + onClick = { navigateToTimelineDetail(TimelineType.getTimelineTypeByIndex(index), pastUiState.timelines[index].timelineId) } + ) + } + } + } + } +} + +@Preview +@Composable +fun PastScreenPreview() { + DATEROADTheme { + PastScreen( + padding = PaddingValues(0.dp), + popBackStack = { }, + navigateToTimelineDetail = { _, _ -> } + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastViewModel.kt new file mode 100644 index 000000000..e95affac0 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/PastViewModel.kt @@ -0,0 +1,35 @@ +package org.sopt.dateroad.presentation.ui.past + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.type.TimelineTimeType +import org.sopt.dateroad.domain.usecase.GetTimelinesUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class PastViewModel @Inject constructor( + private val getDatesUseCase: GetTimelinesUseCase +) : BaseViewModel() { + override fun createInitialState(): PastContract.PastUiState = + PastContract.PastUiState() + + override suspend fun handleEvent(event: PastContract.PastEvent) { + when (event) { + is PastContract.PastEvent.FetchPastDate -> setState { copy(loadState = event.loadState, timelines = event.timelines) } + } + } + + fun fetchPastDate(timelineTimeType: TimelineTimeType) { + viewModelScope.launch { + setEvent(PastContract.PastEvent.FetchPastDate(loadState = LoadState.Loading, timelines = currentState.timelines)) + getDatesUseCase(timelineTimeType = timelineTimeType).onSuccess { timelines -> + setEvent(PastContract.PastEvent.FetchPastDate(loadState = LoadState.Success, timelines = timelines)) + }.onFailure { + setEvent(PastContract.PastEvent.FetchPastDate(loadState = LoadState.Error, timelines = currentState.timelines)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/past/component/PastCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/component/PastCard.kt new file mode 100644 index 000000000..3ee936984 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/component/PastCard.kt @@ -0,0 +1,167 @@ +package org.sopt.dateroad.presentation.ui.past.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme +import org.sopt.dateroad.ui.theme.defaultDateRoadColors + +@Composable +fun PastCard( + timeline: Timeline, + timelineType: TimelineType, + onClick: (Int) -> Unit = {} +) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .aspectRatio(328 / 203f) + .background(timelineType.backgroundColor) + .noRippleClickable(onClick = { onClick(timeline.timelineId) }) + ) { + Icon( + painter = painterResource(id = R.drawable.bg_past_card), + contentDescription = null, + tint = timelineType.lineColor, + modifier = Modifier + .align(Alignment.BottomEnd) + ) + Column( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .weight(1.5f) + .fillMaxWidth() + .padding(top = 14.dp, start = 16.dp, end = 16.dp) + ) { + Text( + text = timeline.date, + style = DateRoadTheme.typography.bodySemi13, + color = DateRoadTheme.colors.black, + modifier = Modifier + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = timeline.title, + style = DateRoadTheme.typography.titleExtra20, + color = DateRoadTheme.colors.black, + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + ) + } + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + ) { + val canvasWidth = size.width + val dotLength = 3.dp.toPx() + val dotSpacing = 3.dp.toPx() + var xOffset = 0f + val strokeWidth = 2.dp.toPx() + + drawCircle( + color = defaultDateRoadColors.white, + radius = strokeWidth * 4, + center = Offset(x = strokeWidth / 2 - 5, y = 0f) + ) + + while (xOffset < canvasWidth) { + drawLine( + color = defaultDateRoadColors.white, + start = Offset(xOffset, 0f), + end = Offset(xOffset + dotLength, 0f), + strokeWidth = 2.dp.toPx(), + cap = StrokeCap.Butt + ) + xOffset += dotLength + dotSpacing + } + drawCircle( + color = defaultDateRoadColors.white, + radius = strokeWidth * 4, + center = Offset(x = canvasWidth - strokeWidth / 2 + 5, y = 0f) + ) + } + Spacer(modifier = Modifier.height(17.dp)) + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(start = 16.dp) + ) { + Text( + text = timeline.city, + style = DateRoadTheme.typography.bodyMed13, + color = DateRoadTheme.colors.black + ) + LazyRow( + modifier = Modifier.padding(top = 10.dp) + ) { + itemsIndexed(timeline.tags) { index, tag -> + DateRoadImageTag( + textContent = stringResource(id = tag.titleRes), + imageContent = tag.imageRes, + tagContentType = timelineType.tagType, + modifier = Modifier.padding(start = if (index > 0) 6.dp else 0.dp) + ) + } + } + } + } + } +} + +@Preview +@Composable +fun PastCardPreview() { + DATEROADTheme { + PastCard( + timeline = Timeline( + dDay = "D-day", + title = "성수동 당일치기 데이트 가볼까요?\n이정도 어떠신지?", + date = "2024년 6월 24일", + city = "건대/성수/왕십리", + tags = listOf(DateTagType.FOOD, DateTagType.DRIVE) + ), + timelineType = TimelineType.PINK + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/past/navigation/PastNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/navigation/PastNavigation.kt new file mode 100644 index 000000000..daeb2da69 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/past/navigation/PastNavigation.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.ui.past.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.past.PastRoute + +fun NavController.navigationPast() { + navigate( + route = PastRoute.ROUTE + ) +} + +fun NavGraphBuilder.pastNavGraph( + padding: PaddingValues, + popBackStack: () -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit +) { + composable(route = PastRoute.ROUTE) { + PastRoute( + padding = padding, + popBackStack = popBackStack, + navigateToTimelineDetail = navigateToTimelineDetail + ) + } +} + +object PastRoute { + const val ROUTE = "past" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/PointGuideScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/PointGuideScreen.kt new file mode 100644 index 000000000..a2955e179 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/PointGuideScreen.kt @@ -0,0 +1,91 @@ +package org.sopt.dateroad.presentation.ui.pointguide + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.MyPagePointInfoType +import org.sopt.dateroad.presentation.ui.component.partialcolortext.PartialColorText +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.pointguide.component.DateRoadMyPagePointInfo +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun ProfileGuideRoute( + padding: PaddingValues, + popBackStack: () -> Unit +) { + PointGuideScreen( + padding = padding, + onIconClick = popBackStack + ) +} + +@Composable +fun PointGuideScreen( + padding: PaddingValues, + onIconClick: () -> Unit +) { + Column( + Modifier + .padding(paddingValues = padding) + .fillMaxSize() + .background(DateRoadTheme.colors.white) + ) { + DateRoadBasicTopBar( + title = stringResource(id = R.string.point_guide_top_bar), + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white, + onLeftIconClick = onIconClick + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(17.dp)) + Text( + text = PartialColorText( + text = stringResource(id = R.string.point_guide_title), + keywords = listOf("포인트", "데이트 코스"), + color = DateRoadTheme.colors.purple600 + ), + style = DateRoadTheme.typography.titleExtra20 + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.point_guide_description), + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.gray500 + ) + Spacer(modifier = Modifier.height(24.dp)) + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + MyPagePointInfoType.entries.forEach { myPagePointInfoType -> + DateRoadMyPagePointInfo(myPagePointInfoType = myPagePointInfoType) + } + } + } + } +} + +@Preview +@Composable +fun PointGuideScreenPreview() { + PointGuideScreen( + padding = PaddingValues(0.dp), + onIconClick = {} + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/component/DateRoadMyPagePointInfo.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/component/DateRoadMyPagePointInfo.kt new file mode 100644 index 000000000..566653044 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/component/DateRoadMyPagePointInfo.kt @@ -0,0 +1,77 @@ +package org.sopt.dateroad.presentation.ui.pointguide.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.presentation.type.MyPagePointInfoType +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun DateRoadMyPagePointInfo(myPagePointInfoType: MyPagePointInfoType) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.gray100) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically // Aligns items vertically centered + ) { + Image( + painter = painterResource(id = myPagePointInfoType.imageRes), + contentDescription = null, + modifier = Modifier.height(70.dp).aspectRatio(1f) + ) + Spacer(modifier = Modifier.width(10.dp)) + Column { + Text( + text = stringResource(id = myPagePointInfoType.titleRes), + style = DateRoadTheme.typography.bodyBold15, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = myPagePointInfoType.descriptionRes), + color = DateRoadTheme.colors.gray500, + style = DateRoadTheme.typography.bodyMed13, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Preview +@Composable +fun DateRoadMyPagePointInfoPreview() { + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + .padding(horizontal = 16.dp) + ) { + DateRoadMyPagePointInfo(myPagePointInfoType = MyPagePointInfoType.FIRST) + Spacer(modifier = Modifier.height(16.dp)) + DateRoadMyPagePointInfo(myPagePointInfoType = MyPagePointInfoType.SECOND) + Spacer(modifier = Modifier.height(16.dp)) + DateRoadMyPagePointInfo(myPagePointInfoType = MyPagePointInfoType.THIRD) + Spacer(modifier = Modifier.height(16.dp)) + DateRoadMyPagePointInfo(myPagePointInfoType = MyPagePointInfoType.FOURTH) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/navigation/PointGuideNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/navigation/PointGuideNavigation.kt new file mode 100644 index 000000000..287d676c6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointguide/navigation/PointGuideNavigation.kt @@ -0,0 +1,29 @@ +package org.sopt.dateroad.presentation.ui.pointguide.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.ui.pointguide.ProfileGuideRoute + +fun NavController.navigationPointGuide() { + navigate( + route = PointGuideRoute.ROUTE + ) +} + +fun NavGraphBuilder.pointGuideGraph( + padding: PaddingValues, + popBackStack: () -> Unit +) { + composable(route = PointGuideRoute.ROUTE) { + ProfileGuideRoute( + padding = padding, + popBackStack = popBackStack + ) + } +} + +object PointGuideRoute { + const val ROUTE = "pointGuide" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryContract.kt new file mode 100644 index 000000000..5b8b92df1 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryContract.kt @@ -0,0 +1,28 @@ +package org.sopt.dateroad.presentation.ui.pointhistory + +import org.sopt.dateroad.domain.model.PointHistory +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.presentation.type.PointHistoryTabType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class PointHistoryContract { + data class PointHistoryUiState( + val loadState: LoadState = LoadState.Idle, + val userPoint: UserPoint = UserPoint(), + val pointHistoryTabType: PointHistoryTabType = PointHistoryTabType.GAINED_HISTORY, + val pointHistory: PointHistory = PointHistory() + ) : UiState + + sealed interface PointHistorySideEffect : UiSideEffect { + data object PopBackStack : PointHistorySideEffect + } + + sealed class PointHistoryEvent : UiEvent { + data class FetchPointHistory(val loadState: LoadState, val pointHistory: PointHistory) : PointHistoryEvent() + data class FetchUserPoint(val loadState: LoadState, val userPoint: UserPoint) : PointHistoryEvent() + data class OnTabBarClicked(val pointHistoryTabType: PointHistoryTabType) : PointHistoryEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryScreen.kt new file mode 100644 index 000000000..ca89cc52e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryScreen.kt @@ -0,0 +1,182 @@ +package org.sopt.dateroad.presentation.ui.pointhistory + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Point +import org.sopt.dateroad.domain.model.PointHistory +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.PointHistoryTabType +import org.sopt.dateroad.presentation.ui.component.tabbar.DateRoadTabBar +import org.sopt.dateroad.presentation.ui.component.tabbar.DateRoadTabTitle +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.pointhistory.component.PointHistoryCard +import org.sopt.dateroad.presentation.ui.pointhistory.component.PointHistoryPointBox +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun PointHistoryRoute( + padding: PaddingValues, + viewModel: PointHistoryViewModel = hiltViewModel(), + popBackStack: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.fetchPointHistory() + viewModel.fetchUserPoint() + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { pointHistorySideEffect -> + when (pointHistorySideEffect) { + is PointHistoryContract.PointHistorySideEffect.PopBackStack -> popBackStack() + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + PointHistoryScreen( + padding = padding, + pointHistoryUiState = uiState, + onTabBarClicked = { pointHistoryTabType -> + viewModel.setEvent( + PointHistoryContract.PointHistoryEvent.OnTabBarClicked(pointHistoryTabType) + ) + }, + onTopBarIconClicked = { viewModel.setSideEffect(PointHistoryContract.PointHistorySideEffect.PopBackStack) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun PointHistoryScreen( + padding: PaddingValues, + pointHistoryUiState: PointHistoryContract.PointHistoryUiState = PointHistoryContract.PointHistoryUiState(), + onTabBarClicked: (PointHistoryTabType) -> Unit, + onTopBarIconClicked: () -> Unit +) { + Column( + modifier = Modifier + .padding(paddingValues = padding) + .background(color = DateRoadTheme.colors.white) + .fillMaxSize() + ) { + DateRoadBasicTopBar( + title = stringResource(id = R.string.top_bar_title_point_history), + leftIconResource = R.drawable.ic_top_bar_back_white, + backGroundColor = DateRoadTheme.colors.white, + onLeftIconClick = onTopBarIconClicked + ) + Spacer(modifier = Modifier.height(22.dp)) + PointHistoryPointBox( + modifier = Modifier.padding(horizontal = 16.dp), + userPoint = pointHistoryUiState.userPoint + ) + Spacer(modifier = Modifier.height(16.dp)) + DateRoadTabBar( + selectedTabPosition = pointHistoryUiState.pointHistoryTabType.position + ) { + PointHistoryTabType.entries.forEachIndexed { index, pointHistoryTabType -> + DateRoadTabTitle( + title = stringResource(id = pointHistoryTabType.titleRes), + selected = index == pointHistoryUiState.pointHistoryTabType.position, + position = index, + onClick = { + onTabBarClicked(pointHistoryTabType) + } + ) + } + } + LazyColumn { + val pointHistory = when (pointHistoryUiState.pointHistoryTabType) { + PointHistoryTabType.GAINED_HISTORY -> pointHistoryUiState.pointHistory.gained + PointHistoryTabType.USED_HISTORY -> pointHistoryUiState.pointHistory.used + } + if (pointHistory.isEmpty()) { + item { + Box( + modifier = Modifier + .fillParentMaxSize() + ) { + DateRoadEmptyView( + emptyViewType = when (pointHistoryUiState.pointHistoryTabType) { + PointHistoryTabType.USED_HISTORY -> EmptyViewType.POINT_HISTORY_USED_HISTORY + PointHistoryTabType.GAINED_HISTORY -> EmptyViewType.POINT_HISTORY_GAINED_HISTORY + } + ) + } + } + } + items(pointHistory.size) { index -> + PointHistoryCard(point = pointHistory[index]) + if (index < pointHistory.size - 1) { + HorizontalDivider( + color = DateRoadTheme.colors.gray100, + thickness = 1.dp + ) + } + } + } + } +} + +@Preview +@Composable +fun PointHistoryPreview() { + DATEROADTheme { + PointHistoryScreen( + padding = PaddingValues(0.dp), + pointHistoryUiState = PointHistoryContract.PointHistoryUiState( + userPoint = UserPoint(), + loadState = LoadState.Success, + pointHistory = PointHistory( + gained = listOf( + Point(point = "+150", description = "서버의 바다여행", createdAt = "2023.12.31"), + Point(point = "+150", description = "서버의 바다여행", createdAt = "2023.12.31"), + Point(point = "+150", description = "서버의 바다여행", createdAt = "2023.12.31") + ), + used = listOf() + ) + ), + onTabBarClicked = {}, + onTopBarIconClicked = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt new file mode 100644 index 000000000..4335d27c2 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/PointHistoryViewModel.kt @@ -0,0 +1,55 @@ +package org.sopt.dateroad.presentation.ui.pointhistory + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.usecase.GetPointHistoryUseCase +import org.sopt.dateroad.domain.usecase.GetUserPointUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class PointHistoryViewModel @Inject constructor( + private val getPointHistoryUseCase: GetPointHistoryUseCase, + private val getUserPointUseCase: GetUserPointUseCase +) : BaseViewModel() { + override fun createInitialState(): PointHistoryContract.PointHistoryUiState = + PointHistoryContract.PointHistoryUiState() + + override suspend fun handleEvent(event: PointHistoryContract.PointHistoryEvent) { + when (event) { + is PointHistoryContract.PointHistoryEvent.FetchPointHistory -> setState { copy(loadState = event.loadState, pointHistory = event.pointHistory) } + is PointHistoryContract.PointHistoryEvent.OnTabBarClicked -> setState { copy(pointHistoryTabType = event.pointHistoryTabType) } + is PointHistoryContract.PointHistoryEvent.FetchUserPoint -> setState { copy(loadState = event.loadState, userPoint = event.userPoint) } + } + } + + fun fetchPointHistory() { + viewModelScope.launch { + setEvent( + PointHistoryContract.PointHistoryEvent.FetchPointHistory(loadState = LoadState.Loading, pointHistory = currentState.pointHistory) + ) + getPointHistoryUseCase().onSuccess { pointHistory -> + setEvent( + PointHistoryContract.PointHistoryEvent.FetchPointHistory(loadState = LoadState.Success, pointHistory = pointHistory) + ) + }.onFailure { + setEvent( + PointHistoryContract.PointHistoryEvent.FetchPointHistory(loadState = LoadState.Error, pointHistory = currentState.pointHistory) + ) + } + } + } + + fun fetchUserPoint() { + viewModelScope.launch { + setEvent(PointHistoryContract.PointHistoryEvent.FetchUserPoint(loadState = LoadState.Loading, userPoint = currentState.userPoint)) + getUserPointUseCase().onSuccess { userPoint -> + setEvent(PointHistoryContract.PointHistoryEvent.FetchUserPoint(loadState = LoadState.Success, userPoint = userPoint)) + }.onFailure { + setEvent(PointHistoryContract.PointHistoryEvent.FetchUserPoint(loadState = LoadState.Error, userPoint = currentState.userPoint)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryCard.kt new file mode 100644 index 000000000..72deb40cd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryCard.kt @@ -0,0 +1,64 @@ +package org.sopt.dateroad.presentation.ui.pointhistory.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.domain.model.Point +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun PointHistoryCard( + modifier: Modifier = Modifier, + point: Point +) { + Row( + modifier = modifier + .padding(top = 20.dp, start = 16.dp, end = 20.dp, bottom = 20.dp) + ) { + Text( + modifier = Modifier + .weight(1f) + .padding(end = 15.dp), + text = point.point, + color = DateRoadTheme.colors.black, + style = DateRoadTheme.typography.bodyBold15 + ) + Column( + modifier = Modifier.weight(235 / 74f) + ) { + Text( + text = point.description, + color = DateRoadTheme.colors.gray500, + style = DateRoadTheme.typography.bodyBold15 + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = point.createdAt, + color = DateRoadTheme.colors.gray500, + style = DateRoadTheme.typography.bodyMed15 + ) + } + } +} + +@Preview +@Composable +fun PointHistoryCardPreview() { + DATEROADTheme { + PointHistoryCard( + point = Point( + point = "+10P", + description = "코스 등록하기", + createdAt = "2024.06.23" + ) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt new file mode 100644 index 000000000..ab6c75536 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/component/PointHistoryPointBox.kt @@ -0,0 +1,56 @@ +package org.sopt.dateroad.presentation.ui.pointhistory.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.UserPoint +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun PointHistoryPointBox( + modifier: Modifier = Modifier, + userPoint: UserPoint +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(DateRoadTheme.colors.purple600) + .padding(start = 16.dp, top = 16.dp, bottom = 14.dp) + ) { + Text( + text = stringResource(id = R.string.point_box_nickname, userPoint.name), + color = DateRoadTheme.colors.white, + style = DateRoadTheme.typography.bodyMed13 + ) + Spacer(modifier = Modifier.height(11.dp)) + Text( + text = userPoint.point, + color = DateRoadTheme.colors.white, + style = DateRoadTheme.typography.titleExtra24, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Preview +@Composable +fun PointHistoryPointBoxPreview() { + Column { + PointHistoryPointBox(userPoint = UserPoint()) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt new file mode 100644 index 000000000..3f92c96d8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/pointhistory/navigation/PointHistoryNavigation.kt @@ -0,0 +1,26 @@ +package org.sopt.dateroad.presentation.ui.pointhistory.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.ui.pointhistory.PointHistoryRoute + +fun NavController.navigationPointHistory() { + navigate( + route = PointHistoryRoute.ROUTE + ) +} + +fun NavGraphBuilder.pointHistoryGraph( + padding: PaddingValues, + popBackStack: () -> Unit +) { + composable(route = PointHistoryRoute.ROUTE) { + PointHistoryRoute(padding = padding, popBackStack = popBackStack) + } +} + +object PointHistoryRoute { + const val ROUTE = "pointHistory" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileContract.kt new file mode 100644 index 000000000..6f02740a6 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileContract.kt @@ -0,0 +1,55 @@ +package org.sopt.dateroad.presentation.ui.profile + +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.model.Profile +import org.sopt.dateroad.domain.model.SignUp +import org.sopt.dateroad.presentation.type.ProfileType +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class ProfileContract { + data class ProfileUiState( + val loadState: LoadState = LoadState.Idle, + val signUpLoadState: LoadState = LoadState.Idle, + val fetchProfileLoadState: LoadState = LoadState.Idle, + val editProfileLoadState: LoadState = LoadState.Idle, + val profileType: ProfileType = ProfileType.ENROLL, + val signUp: SignUp = SignUp(), + val editProfile: EditProfile = EditProfile(), + val isNicknameButtonEnabled: Boolean = false, + val isEnrollButtonEnabled: Boolean = false, + val isNicknameChecked: Boolean = false, + val isBottomSheetOpen: Boolean = false, + val nicknameValidateResult: TextFieldValidateResult = TextFieldValidateResult.Basic, + val currentProfile: Profile = Profile() + ) : UiState + + sealed interface ProfileSideEffect : UiSideEffect { + data object NavigateToHome : ProfileSideEffect + data object NavigateToMyPage : ProfileSideEffect + data object PopBackStack : ProfileSideEffect + } + + sealed class ProfileEvent : UiEvent { + data class GetNicknameCheck(val loadState: LoadState, val nicknameValidateResult: TextFieldValidateResult) : ProfileEvent() + data class OnDateChipClicked(val tag: String) : ProfileEvent() + data class OnNicknameValueChanged(val name: String) : ProfileEvent() + data object OnImageButtonClicked : ProfileEvent() + data class OnImageValueChanged(val image: String) : ProfileEvent() + data object OnBottomSheetDismissRequest : ProfileEvent() + data class CheckEnrollButtonEnable(val isEnrollButtonEnabled: Boolean) : ProfileEvent() + data class PostSignUp(val signUpLoadState: LoadState) : ProfileEvent() + data class PatchEditProfile(val editProfileLoadState: LoadState) : ProfileEvent() + data class SetSignUpImage(val image: String) : ProfileEvent() + data class SetEditProfileImage(val image: String) : ProfileEvent() + data class InitProfileType(val profileType: ProfileType) : ProfileEvent() + data class FetchProfile( + val fetchProfileLoadState: LoadState, + val editProfile: EditProfile, + val currentProfile: Profile + ) : ProfileEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileScreen.kt new file mode 100644 index 000000000..bb961fe0e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileScreen.kt @@ -0,0 +1,329 @@ +package org.sopt.dateroad.presentation.ui.profile + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import coil.compose.rememberAsyncImagePainter +import org.sopt.dateroad.R +import org.sopt.dateroad.data.mapper.todata.toEditProfile +import org.sopt.dateroad.presentation.type.DateChipGroupType +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.dateroad.presentation.type.ProfileType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet +import org.sopt.dateroad.presentation.ui.component.button.DateRoadBasicButton +import org.sopt.dateroad.presentation.ui.component.chipgroup.DateRoadDateChipGroup +import org.sopt.dateroad.presentation.ui.component.textfield.DateRoadTextFieldWithButton +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.util.Pattern.NICKNAME_REGEX +import org.sopt.dateroad.presentation.util.UserPropertyAmplitude.USER_NAME +import org.sopt.dateroad.presentation.util.amplitude.AmplitudeUtils +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@SuppressLint("StateFlowValueCalledInComposition") +@Composable +fun ProfileRoute( + viewModel: ProfileViewModel = hiltViewModel(), + navigationToHome: () -> Unit, + navigationToMyPage: () -> Unit, + profileType: ProfileType, + popBackStack: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + val getGalleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + viewModel.setEvent(ProfileContract.ProfileEvent.SetSignUpImage(image = uri.toString())) + viewModel.setEvent(ProfileContract.ProfileEvent.SetEditProfileImage(image = uri.toString())) + } + + val getPhotoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? -> + viewModel.setEvent(ProfileContract.ProfileEvent.SetSignUpImage(image = uri.toString())) + viewModel.setEvent(ProfileContract.ProfileEvent.SetEditProfileImage(image = uri.toString())) + } + + LaunchedEffect(Unit) { + viewModel.setEvent(ProfileContract.ProfileEvent.InitProfileType(profileType = profileType)) + if (profileType == ProfileType.EDIT) { + viewModel.fetchProfile() + } + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { profileSideEffect -> + when (profileSideEffect) { + is ProfileContract.ProfileSideEffect.NavigateToHome -> navigationToHome() + is ProfileContract.ProfileSideEffect.NavigateToMyPage -> navigationToMyPage() + is ProfileContract.ProfileSideEffect.PopBackStack -> popBackStack() + } + } + } + + when (profileType) { + ProfileType.ENROLL -> { + when (uiState.signUpLoadState) { + LoadState.Idle -> { + ProfileScreen( + profileUiState = uiState, + onImageButtonClicked = { viewModel.setEvent(ProfileContract.ProfileEvent.OnImageButtonClicked) }, + onNicknameValueChanged = { name -> if (name.matches(NICKNAME_REGEX)) viewModel.setEvent(ProfileContract.ProfileEvent.OnNicknameValueChanged(name = name)) }, + onDateChipClicked = { tag -> viewModel.setEvent(ProfileContract.ProfileEvent.OnDateChipClicked(tag = tag.name)) }, + onBottomSheetDismissRequest = { viewModel.setEvent(ProfileContract.ProfileEvent.OnBottomSheetDismissRequest) }, + onNicknameButtonClicked = { viewModel.getNicknameCheck(uiState.signUp.userSignUpInfo.name) }, + onEnrollButtonClicked = { + viewModel.postSignUp(uiState.signUp) + }, + selectPhoto = { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + getGalleryLauncher.launch("image/*") + } else { + getPhotoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + }, + deletePhoto = { viewModel.setEvent(ProfileContract.ProfileEvent.SetSignUpImage(image = "")) }, + popUpBackStack = { Unit } + + ) + } + + LoadState.Loading -> DateRoadLoadingView() + LoadState.Success -> { + navigationToHome() + AmplitudeUtils.updateStringUserProperty(propertyName = USER_NAME, propertyValue = uiState.signUp.userSignUpInfo.name) + } + + LoadState.Error -> DateRoadErrorView() + } + } + + ProfileType.EDIT -> { + when (uiState.editProfileLoadState) { + LoadState.Idle -> { + ProfileScreen( + profileUiState = uiState, + onImageButtonClicked = { viewModel.setEvent(ProfileContract.ProfileEvent.OnImageButtonClicked) }, + onNicknameValueChanged = { name -> if (name.matches(NICKNAME_REGEX)) viewModel.setEvent(ProfileContract.ProfileEvent.OnNicknameValueChanged(name = name)) }, + onDateChipClicked = { tag -> viewModel.setEvent(ProfileContract.ProfileEvent.OnDateChipClicked(tag = tag.name)) }, + onBottomSheetDismissRequest = { viewModel.setEvent(ProfileContract.ProfileEvent.OnBottomSheetDismissRequest) }, + onNicknameButtonClicked = { + viewModel.getNicknameCheck(uiState.editProfile.name) + }, + onEnrollButtonClicked = { + viewModel.patchEditProfile(uiState.editProfile) + }, + selectPhoto = { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + getGalleryLauncher.launch("image/*") + } else { + getPhotoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + }, + deletePhoto = { viewModel.setEvent(ProfileContract.ProfileEvent.SetEditProfileImage(image = "")) }, + popUpBackStack = { popBackStack() } + ) + } + + LoadState.Loading -> DateRoadLoadingView() + LoadState.Success -> { + navigationToMyPage() + AmplitudeUtils.updateStringUserProperty(propertyName = USER_NAME, propertyValue = uiState.editProfile.name) + } + + LoadState.Error -> DateRoadErrorView() + } + } + } + + when (uiState.profileType) { + ProfileType.ENROLL -> if (uiState.nicknameValidateResult == TextFieldValidateResult.Success && (uiState.signUp.tag.isNotEmpty())) { + viewModel.setEvent(ProfileContract.ProfileEvent.CheckEnrollButtonEnable(true)) + } else { + viewModel.setEvent(ProfileContract.ProfileEvent.CheckEnrollButtonEnable(false)) + } + + ProfileType.EDIT -> { + if ((uiState.editProfile.tags.isNotEmpty()) && uiState.currentProfile.toEditProfile() != uiState.editProfile || uiState.nicknameValidateResult == TextFieldValidateResult.Success) { + viewModel.setEvent(ProfileContract.ProfileEvent.CheckEnrollButtonEnable(true)) + } else { + viewModel.setEvent(ProfileContract.ProfileEvent.CheckEnrollButtonEnable(false)) + } + } + } +} + +@Composable +fun ProfileScreen( + profileUiState: ProfileContract.ProfileUiState = ProfileContract.ProfileUiState(), + onImageButtonClicked: () -> Unit, + onNicknameValueChanged: (String) -> Unit, + onDateChipClicked: (DateTagType) -> Unit, + onBottomSheetDismissRequest: () -> Unit, + onNicknameButtonClicked: () -> Unit, + onEnrollButtonClicked: () -> Unit, + selectPhoto: () -> Unit, + deletePhoto: () -> Unit, + popUpBackStack: () -> Unit +) { + val focusManager = LocalFocusManager.current + + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.white) + .pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + DateRoadBasicTopBar( + leftIconResource = if (profileUiState.profileType == ProfileType.EDIT) { + R.drawable.ic_top_bar_back_white + } else { + null + }, + onLeftIconClick = popUpBackStack, + title = stringResource(id = profileUiState.profileType.topAppBarTitleRes), + backGroundColor = DateRoadTheme.colors.white + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(40.dp)) + Box( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Image( + painter = when (profileUiState.profileType) { + ProfileType.ENROLL -> if (profileUiState.signUp.image.isEmpty() || profileUiState.signUp.image == "null") { + painterResource(id = R.drawable.img_enroll_profile_default) + } else { + rememberAsyncImagePainter(model = profileUiState.signUp.image) + } + + ProfileType.EDIT -> if (profileUiState.editProfile.image.isNullOrEmpty()) { + painterResource(id = R.drawable.img_enroll_profile_default) + } else { + rememberAsyncImagePainter(model = profileUiState.editProfile.image) + } + }, + contentDescription = null, + modifier = Modifier + .height(128.dp) + .aspectRatio(1f) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + + Image( + painter = painterResource(id = R.drawable.btn_my_profile_plus), + contentDescription = null, + modifier = Modifier + .align(Alignment.BottomEnd) + .noRippleClickable(onClick = onImageButtonClicked) + ) + } + Spacer(modifier = Modifier.height(40.dp)) + + DateRoadTextFieldWithButton( + validateState = profileUiState.nicknameValidateResult, + title = stringResource(id = R.string.profile_text_field_title), + titleDescription = stringResource(id = R.string.profile_text_field_title_description), + placeholder = if (profileUiState.profileType == ProfileType.ENROLL) { + stringResource(id = R.string.profile_text_field_placeholder) + } else { + profileUiState.editProfile.name + }, + validationErrorDescription = stringResource(id = R.string.profile_text_field_validation_error_description), + successDescription = stringResource(id = R.string.profile_text_field_success_description), + conflictErrorDescription = stringResource(id = R.string.profile_text_field_conflict_error_description), + buttonText = stringResource(id = R.string.profile_text_field_button_text), + isButtonEnabled = profileUiState.isNicknameButtonEnabled, + value = if (profileUiState.profileType == ProfileType.ENROLL) { + profileUiState.signUp.userSignUpInfo.name + } else { + profileUiState.editProfile.name + }, + onValueChange = onNicknameValueChanged, + onButtonClick = { onNicknameButtonClicked() } + ) + Spacer(modifier = Modifier.height(23.dp)) + + DateRoadDateChipGroup( + dateChipGroupType = DateChipGroupType.PROFILE, + selectedDateTags = if (profileUiState.profileType == ProfileType.ENROLL) { + profileUiState.signUp.tag.mapNotNull { it.getDateTagTypeByName() } + } else { + profileUiState.editProfile.tags.mapNotNull { it.getDateTagTypeByName() } + }, + onSelectedDateTagsChanged = onDateChipClicked + ) + + Spacer(modifier = Modifier.weight(1f)) + + DateRoadBasicButton( + isEnabled = profileUiState.isEnrollButtonEnabled, + textContent = stringResource(id = profileUiState.profileType.buttonTextRes), + onClick = onEnrollButtonClicked + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + DateRoadBasicBottomSheet( + isBottomSheetOpen = profileUiState.isBottomSheetOpen, + title = stringResource(id = R.string.profile_bottom_sheet_title), + isButtonEnabled = false, + buttonText = stringResource(id = R.string.profile_bottom_sheet_button_text), + itemList = listOf( + stringResource(id = R.string.profile_bottom_sheet_button_enroll) to { + selectPhoto() + }, + stringResource(id = R.string.profile_bottom_sheet_button_delete) to { + deletePhoto() + } + ), + onDismissRequest = { onBottomSheetDismissRequest() }, + onButtonClick = { onBottomSheetDismissRequest() } + + ) +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileViewModel.kt new file mode 100644 index 000000000..b11224d7e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/ProfileViewModel.kt @@ -0,0 +1,181 @@ +package org.sopt.dateroad.presentation.ui.profile + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.data.mapper.todata.toEditProfile +import org.sopt.dateroad.domain.model.EditProfile +import org.sopt.dateroad.domain.model.SignUp +import org.sopt.dateroad.domain.usecase.GetNicknameCheckUseCase +import org.sopt.dateroad.domain.usecase.GetUserUseCase +import org.sopt.dateroad.domain.usecase.PatchEditProfileUseCase +import org.sopt.dateroad.domain.usecase.PostSignUpUseCase +import org.sopt.dateroad.domain.usecase.SetAccessTokenUseCase +import org.sopt.dateroad.domain.usecase.SetRefreshTokenUseCase +import org.sopt.dateroad.presentation.type.ProfileType +import org.sopt.dateroad.presentation.ui.component.textfield.model.TextFieldValidateResult +import org.sopt.dateroad.presentation.util.Token +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val getNicknameCheckUseCase: GetNicknameCheckUseCase, + private val postSignUpUseCase: PostSignUpUseCase, + private val setAccessTokenUseCase: SetAccessTokenUseCase, + private val setRefreshTokenUseCase: SetRefreshTokenUseCase, + private val getUserUseCase: GetUserUseCase, + private val patchEditProfileUseCase: PatchEditProfileUseCase +) : BaseViewModel() { + + override fun createInitialState(): ProfileContract.ProfileUiState = ProfileContract.ProfileUiState() + + override suspend fun handleEvent(event: ProfileContract.ProfileEvent) { + when (event) { + is ProfileContract.ProfileEvent.OnImageValueChanged -> setState { copy(signUp = currentState.signUp.copy(image = event.image)) } + is ProfileContract.ProfileEvent.OnDateChipClicked -> setState { + if (currentState.profileType == ProfileType.ENROLL) { + copy( + signUp = currentState.signUp.copy( + tag = currentState.signUp.tag.toMutableList().apply { + if (contains(event.tag)) { + remove(event.tag) + } else if (size < 3) { + add(event.tag) + } + } + ) + ) + } else { + copy( + editProfile = currentState.editProfile.copy( + tags = currentState.editProfile.tags.toMutableList().apply { + if (contains(event.tag)) { + remove(event.tag) + } else if (size < 3) { + add(event.tag) + } + } + ) + ) + } + } + is ProfileContract.ProfileEvent.OnImageButtonClicked -> setState { copy(isBottomSheetOpen = true) } + is ProfileContract.ProfileEvent.GetNicknameCheck -> setState { + copy( + loadState = event.loadState, + nicknameValidateResult = event.nicknameValidateResult + ) + } + + is ProfileContract.ProfileEvent.OnNicknameValueChanged -> setState { + if (currentState.profileType == ProfileType.ENROLL) { + copy( + signUp = currentState.signUp.copy(userSignUpInfo = currentState.signUp.userSignUpInfo.copy(name = event.name)), + isNicknameButtonEnabled = event.name.length in MIN_NICKNAME_LENGTH..MAX_NICKNAME_LENGTH, + nicknameValidateResult = when { + event.name.length < 2 -> TextFieldValidateResult.ValidationError + else -> TextFieldValidateResult.Basic + } + ) + } else { + copy( + editProfile = currentState.editProfile.copy(name = event.name), + isNicknameButtonEnabled = event.name.length in MIN_NICKNAME_LENGTH..MAX_NICKNAME_LENGTH, + nicknameValidateResult = when { + event.name.length < 2 -> TextFieldValidateResult.ValidationError + else -> TextFieldValidateResult.Basic + } + ) + } + } + + is ProfileContract.ProfileEvent.OnBottomSheetDismissRequest -> setState { copy(isBottomSheetOpen = false) } + is ProfileContract.ProfileEvent.CheckEnrollButtonEnable -> setState { copy(isEnrollButtonEnabled = event.isEnrollButtonEnabled) } + is ProfileContract.ProfileEvent.PostSignUp -> setState { copy(signUpLoadState = event.signUpLoadState) } + is ProfileContract.ProfileEvent.SetSignUpImage -> setState { copy(signUp = currentState.signUp.copy(image = event.image)) } + is ProfileContract.ProfileEvent.SetEditProfileImage -> setState { copy(editProfile = currentState.editProfile.copy(image = event.image)) } + is ProfileContract.ProfileEvent.InitProfileType -> setState { copy(profileType = event.profileType) } + is ProfileContract.ProfileEvent.FetchProfile -> setState { copy(fetchProfileLoadState = event.fetchProfileLoadState, editProfile = event.editProfile, currentProfile = event.currentProfile) } + is ProfileContract.ProfileEvent.PatchEditProfile -> setState { copy(editProfileLoadState = event.editProfileLoadState) } + } + } + + fun postSignUp(signUp: SignUp) { + viewModelScope.launch { + setEvent(ProfileContract.ProfileEvent.PostSignUp(signUpLoadState = LoadState.Loading)) + postSignUpUseCase(signUp = signUp).onSuccess { auth -> + setEvent(ProfileContract.ProfileEvent.PostSignUp(signUpLoadState = LoadState.Success)) + setAccessTokenUseCase(accessToken = Token.BEARER + auth.accessToken) + setRefreshTokenUseCase(refreshToken = auth.refreshToken) + }.onFailure { + setEvent(ProfileContract.ProfileEvent.PostSignUp(signUpLoadState = LoadState.Error)) + } + } + } + + fun getNicknameCheck(name: String) { + viewModelScope.launch { + setEvent( + ProfileContract.ProfileEvent.GetNicknameCheck(loadState = LoadState.Loading, nicknameValidateResult = TextFieldValidateResult.Basic) + ) + getNicknameCheckUseCase(name = name).onSuccess { code -> + when (code) { + SUCCESS -> setEvent( + ProfileContract.ProfileEvent.GetNicknameCheck( + loadState = LoadState.Success, + nicknameValidateResult = TextFieldValidateResult.Success + ) + ) + CONFLICT -> setEvent( + ProfileContract.ProfileEvent.GetNicknameCheck( + loadState = LoadState.Success, + nicknameValidateResult = if (currentState.currentProfile.name == name) { + TextFieldValidateResult.Success + } else { + TextFieldValidateResult.ConflictError + } + ) + ) + } + }.onFailure { + setEvent( + ProfileContract.ProfileEvent.GetNicknameCheck( + loadState = LoadState.Error, + nicknameValidateResult = TextFieldValidateResult.ValidationError + ) + ) + } + } + } + + fun fetchProfile() { + viewModelScope.launch { + setEvent(ProfileContract.ProfileEvent.FetchProfile(fetchProfileLoadState = LoadState.Loading, editProfile = currentState.editProfile, currentProfile = currentState.currentProfile)) + getUserUseCase().onSuccess { profile -> + setEvent(ProfileContract.ProfileEvent.FetchProfile(fetchProfileLoadState = LoadState.Success, editProfile = profile.toEditProfile(), currentProfile = profile)) + }.onFailure { + setEvent(ProfileContract.ProfileEvent.FetchProfile(fetchProfileLoadState = LoadState.Error, editProfile = currentState.editProfile, currentProfile = currentState.currentProfile)) + } + } + } + + fun patchEditProfile(editProfile: EditProfile) { + viewModelScope.launch { + setEvent(ProfileContract.ProfileEvent.PatchEditProfile(editProfileLoadState = LoadState.Loading)) + patchEditProfileUseCase(editProfile = editProfile).onSuccess { + setEvent(ProfileContract.ProfileEvent.PatchEditProfile(editProfileLoadState = LoadState.Success)) + }.onFailure { + setEvent(ProfileContract.ProfileEvent.PatchEditProfile(editProfileLoadState = LoadState.Error)) + } + } + } + + companion object { + const val MIN_NICKNAME_LENGTH = 2 + const val MAX_NICKNAME_LENGTH = 5 + const val SUCCESS = 200 + const val CONFLICT = 409 + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/navigation/ProfileNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/navigation/ProfileNavigation.kt new file mode 100644 index 000000000..013bd7d0f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/profile/navigation/ProfileNavigation.kt @@ -0,0 +1,62 @@ +package org.sopt.dateroad.presentation.ui.profile.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.type.ProfileType +import org.sopt.dateroad.presentation.ui.profile.ProfileRoute + +fun NavController.navigationEnrollProfile() { + navigate( + route = EnrollProfileRoute.ROUTE + ) +} + +fun NavController.navigationEditProfile() { + navigate( + route = EditProfileRoute.ROUTE + ) +} + +fun NavGraphBuilder.enrollProfileNavGraph( + navigateToHome: () -> Unit, + navigateToMyPage: () -> Unit, + profileType: ProfileType, + popBackStack: () -> Unit + +) { + composable(route = EnrollProfileRoute.ROUTE) { + ProfileRoute( + navigationToHome = navigateToHome, + navigationToMyPage = navigateToMyPage, + profileType = profileType, + popBackStack = popBackStack + + ) + } +} + +fun NavGraphBuilder.editProfileNavGraph( + navigateToHome: () -> Unit, + navigateToMyPage: () -> Unit, + profileType: ProfileType, + popBackStack: () -> Unit + +) { + composable(route = EditProfileRoute.ROUTE) { + ProfileRoute( + navigationToHome = navigateToHome, + navigationToMyPage = navigateToMyPage, + profileType = profileType, + popBackStack = popBackStack + ) + } +} + +object EnrollProfileRoute { + const val ROUTE = "enrollProfile" +} + +object EditProfileRoute { + const val ROUTE = "editProfile" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadContract.kt new file mode 100644 index 000000000..720c7ed08 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadContract.kt @@ -0,0 +1,25 @@ +package org.sopt.dateroad.presentation.ui.read + +import org.sopt.dateroad.domain.model.Course +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class ReadContract { + data class ReadUiState( + val loadState: LoadState = LoadState.Idle, + val name: String = "", + val courses: List = listOf() + ) : UiState + + sealed interface ReadSideEffect : UiSideEffect { + data object NavigateToEnroll : ReadSideEffect + data class NavigateToCourseDetail(val courseId: Int) : ReadSideEffect + } + + sealed class ReadEvent : UiEvent { + data class FetchMyCourseRead(val loadState: LoadState, val courses: List) : ReadEvent() + data class FetchName(val name: String) : ReadEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadScreen.kt new file mode 100644 index 000000000..97ee380ec --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadScreen.kt @@ -0,0 +1,153 @@ +package org.sopt.dateroad.presentation.ui.read + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.component.card.DateRoadCourseCard +import org.sopt.dateroad.presentation.ui.component.partialcolortext.PartialColorText +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.util.ViewPath.MY_COURSE_READ +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun ReadRoute( + padding: PaddingValues, + viewModel: ReadViewModel = hiltViewModel(), + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.fetchName() + viewModel.fetchMyCourseRead() + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle).collect { readSideEffect -> + when (readSideEffect) { + is ReadContract.ReadSideEffect.NavigateToEnroll -> navigateToEnroll(EnrollType.TIMELINE, MY_COURSE_READ, null) + is ReadContract.ReadSideEffect.NavigateToCourseDetail -> navigateToCourseDetail(readSideEffect.courseId) + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + ReadScreen( + padding = padding, + readUiState = uiState, + navigateToEnroll = { viewModel.setSideEffect(ReadContract.ReadSideEffect.NavigateToEnroll) }, + navigateToCourseDetail = { courseId -> viewModel.setSideEffect(ReadContract.ReadSideEffect.NavigateToCourseDetail(courseId = courseId)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@Composable +fun ReadScreen( + padding: PaddingValues, + readUiState: ReadContract.ReadUiState = ReadContract.ReadUiState(), + navigateToEnroll: () -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + Column( + modifier = Modifier + .background(color = DateRoadTheme.colors.white) + .padding(padding) + .fillMaxSize() + ) { + Spacer(modifier = Modifier.height(52.dp)) + + if (readUiState.courses.isEmpty()) { + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = stringResource(id = R.string.read_title_empty, readUiState.name), + style = DateRoadTheme.typography.titleExtra24 + ) + DateRoadEmptyView(emptyViewType = EmptyViewType.READ) + } else { + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = PartialColorText(text = stringResource(id = R.string.read_title_normal, readUiState.name, readUiState.courses.size), keywords = listOf(readUiState.courses.size.toString()), color = DateRoadTheme.colors.purple600), + style = DateRoadTheme.typography.titleExtra24 + ) + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier + .padding(vertical = 7.dp, horizontal = 16.dp) + .noRippleClickable(onClick = navigateToEnroll), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.read_suggest_enroll), + style = DateRoadTheme.typography.titleBold18, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.width(10.dp)) + Image( + painter = painterResource(id = R.drawable.btn_look_button_arrow), + contentDescription = null + ) + } + Spacer(modifier = Modifier.height(10.dp)) + LazyColumn { + items(readUiState.courses) { course -> + DateRoadCourseCard(course = course, onClick = { navigateToCourseDetail(course.courseId) }) + } + } + } + } +} + +@Preview() +@Composable +fun ReadScreenPreview() { + DATEROADTheme { + ReadScreen(padding = PaddingValues(0.dp), navigateToCourseDetail = {}, navigateToEnroll = {}) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadViewModel.kt new file mode 100644 index 000000000..5555f7e63 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/ReadViewModel.kt @@ -0,0 +1,47 @@ +package org.sopt.dateroad.presentation.ui.read + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.usecase.GetMyCourseReadUseCase +import org.sopt.dateroad.domain.usecase.GetNicknameUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class ReadViewModel @Inject constructor( + private val getNicknameUseCase: GetNicknameUseCase, + private val getMyCourseReadUseCase: GetMyCourseReadUseCase +) : BaseViewModel() { + override fun createInitialState(): ReadContract.ReadUiState = + ReadContract.ReadUiState() + + override suspend fun handleEvent(event: ReadContract.ReadEvent) { + when (event) { + is ReadContract.ReadEvent.FetchMyCourseRead -> setState { copy(loadState = event.loadState, courses = event.courses) } + is ReadContract.ReadEvent.FetchName -> setState { copy(name = event.name) } + } + } + + fun fetchMyCourseRead() { + viewModelScope.launch { + setEvent( + ReadContract.ReadEvent.FetchMyCourseRead(loadState = LoadState.Loading, courses = currentState.courses) + ) + getMyCourseReadUseCase().onSuccess { courses -> + setEvent( + ReadContract.ReadEvent.FetchMyCourseRead(loadState = LoadState.Success, courses = courses) + ) + }.onFailure { + setEvent( + ReadContract.ReadEvent.FetchMyCourseRead(loadState = LoadState.Error, courses = currentState.courses) + ) + } + } + } + + fun fetchName() { + setEvent(ReadContract.ReadEvent.FetchName(name = getNicknameUseCase())) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/read/navigation/ReadNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/navigation/ReadNavigation.kt new file mode 100644 index 000000000..d25c3fded --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/read/navigation/ReadNavigation.kt @@ -0,0 +1,31 @@ +package org.sopt.dateroad.presentation.ui.read.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.ui.read.ReadRoute + +fun NavController.navigationRead(navOptions: NavOptions) { + navigate( + route = MainNavigationBarRoute.Read::class.simpleName.orEmpty(), + navOptions = navOptions + ) +} + +fun NavGraphBuilder.readNavGraph( + padding: PaddingValues, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToCourseDetail: (Int) -> Unit +) { + composable(route = MainNavigationBarRoute.Read::class.simpleName.orEmpty()) { + ReadRoute( + padding = padding, + navigateToEnroll = navigateToEnroll, + navigateToCourseDetail = navigateToCourseDetail + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInContract.kt new file mode 100644 index 000000000..5e4960ffa --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInContract.kt @@ -0,0 +1,26 @@ +package org.sopt.dateroad.presentation.ui.signin + +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class SignInContract { + data class SignInUiState( + val loadState: LoadState = LoadState.Idle, + val authTokenLoadState: LoadState = LoadState.Idle, + var isWebViewOpened: Boolean = false + ) : UiState + + sealed interface SignInSideEffect : UiSideEffect { + data object NavigateToOnboarding : SignInSideEffect + data object NavigateToHome : SignInSideEffect + } + + sealed class SignInEvent : UiEvent { + data class PostSignIn(val loadState: LoadState) : SignInEvent() + data class SetAuthToken(val authTokenLoadState: LoadState) : SignInEvent() + data object OnWebViewClick : SignInEvent() + data object WebViewClose : SignInEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInScreen.kt new file mode 100644 index 000000000..1e22079cb --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInScreen.kt @@ -0,0 +1,135 @@ +package org.sopt.dateroad.presentation.ui.signin + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.user.UserApiClient +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.SignIn +import org.sopt.dateroad.presentation.ui.component.button.DateRoadKakaoLoginButton +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadWebView +import org.sopt.dateroad.presentation.util.WebViewUrl.PRIVACY_POLICY_URL +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DateRoadTheme + +fun setLayoutLoginKakaoClickListener(context: Context, callback: (OAuthToken?, Throwable?) -> Unit) { + if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.instance.loginWithKakaoTalk(context, callback = callback) + } else { + UserApiClient.instance.loginWithKakaoAccount(context, callback = callback) + } +} + +@Composable +fun SignInRoute( + viewModel: SignInViewModel = hiltViewModel(), + navigateToOnboarding: () -> Unit, + navigateToHome: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + val callback: (OAuthToken?, Throwable?) -> Unit = { oAuthToken, message -> + if (oAuthToken != null) { + viewModel.setKakaoAccessToken(oAuthToken.accessToken) + } + } + + LaunchedEffect(Unit) { + viewModel.checkAutoLogin() + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { signInSideEffect -> + when (signInSideEffect) { + is SignInContract.SignInSideEffect.NavigateToOnboarding -> navigateToOnboarding() + is SignInContract.SignInSideEffect.NavigateToHome -> navigateToHome() + } + } + } + + LaunchedEffect(uiState.authTokenLoadState) { + when (uiState.authTokenLoadState) { + LoadState.Success -> viewModel.postSignIn(signIn = SignIn("KAKAO")) + else -> Unit + } + } + + when (uiState.loadState) { + LoadState.Idle -> { + SignInScreen( + signInUiState = uiState, + onSignInClicked = { + setLayoutLoginKakaoClickListener(context = context, callback = callback) + }, + onWebViewClicked = { viewModel.setEvent(SignInContract.SignInEvent.OnWebViewClick) }, + webViewClose = { viewModel.setEvent(SignInContract.SignInEvent.WebViewClose) } + ) + } + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> navigateToHome() + + LoadState.Error -> navigateToOnboarding() + } +} + +@Composable +fun SignInScreen( + signInUiState: SignInContract.SignInUiState = SignInContract.SignInUiState(), + onSignInClicked: () -> Unit, + onWebViewClicked: () -> Unit, + webViewClose: () -> Unit +) { + if (signInUiState.isWebViewOpened) { + DateRoadWebView(url = PRIVACY_POLICY_URL, onClose = webViewClose) + } else { + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.purple600), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(226f)) + Image(painter = painterResource(id = R.drawable.img_splash_logo), contentDescription = null) + Spacer(modifier = Modifier.weight(167f)) + DateRoadKakaoLoginButton( + modifier = Modifier.padding(horizontal = 30.dp), + onClick = onSignInClicked + ) + Spacer(modifier = Modifier.weight(16f)) + Text( + text = "개인정보처리방침", + color = DateRoadTheme.colors.gray200, + style = DateRoadTheme.typography.bodyMed15, + textDecoration = TextDecoration.Underline, + modifier = Modifier.noRippleClickable(onClick = onWebViewClicked) + ) + Spacer(modifier = Modifier.weight(37f)) + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInViewModel.kt new file mode 100644 index 000000000..491c9b909 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/SignInViewModel.kt @@ -0,0 +1,58 @@ +package org.sopt.dateroad.presentation.ui.signin + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.model.SignIn +import org.sopt.dateroad.domain.usecase.GetAccessTokenUseCase +import org.sopt.dateroad.domain.usecase.GetRefreshTokenUseCase +import org.sopt.dateroad.domain.usecase.PostSignInUseCase +import org.sopt.dateroad.domain.usecase.SetAccessTokenUseCase +import org.sopt.dateroad.domain.usecase.SetRefreshTokenUseCase +import org.sopt.dateroad.presentation.util.Token +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class SignInViewModel @Inject constructor( + val getAccessTokenUseCase: GetAccessTokenUseCase, + val getRefreshTokenUseCase: GetRefreshTokenUseCase, + val setAccessTokenUseCase: SetAccessTokenUseCase, + val setRefreshTokenUseCase: SetRefreshTokenUseCase, + val postSignInUseCase: PostSignInUseCase +) : BaseViewModel() { + override fun createInitialState(): SignInContract.SignInUiState = + SignInContract.SignInUiState() + + override suspend fun handleEvent(event: SignInContract.SignInEvent) { + when (event) { + is SignInContract.SignInEvent.PostSignIn -> setState { copy(loadState = event.loadState) } + is SignInContract.SignInEvent.OnWebViewClick -> setState { copy(isWebViewOpened = true) } + is SignInContract.SignInEvent.WebViewClose -> setState { copy(isWebViewOpened = false) } + is SignInContract.SignInEvent.SetAuthToken -> setState { copy(authTokenLoadState = event.authTokenLoadState) } + } + } + + fun setKakaoAccessToken(accessToken: String) { + setAccessTokenUseCase(accessToken) + setEvent(SignInContract.SignInEvent.SetAuthToken(authTokenLoadState = LoadState.Success)) + } + + fun postSignIn(signIn: SignIn) { + viewModelScope.launch { + setEvent(SignInContract.SignInEvent.PostSignIn(loadState = LoadState.Loading)) + postSignInUseCase(authorization = getAccessTokenUseCase(), signIn = signIn).onSuccess { auth -> + setEvent(SignInContract.SignInEvent.PostSignIn(loadState = LoadState.Success)) + setAccessTokenUseCase(Token.BEARER + auth.accessToken) + setRefreshTokenUseCase(auth.refreshToken) + }.onFailure { + setEvent(SignInContract.SignInEvent.PostSignIn(loadState = LoadState.Error)) + } + } + } + + fun checkAutoLogin() { + if (getRefreshTokenUseCase().isNotEmpty()) setEvent(SignInContract.SignInEvent.PostSignIn(LoadState.Success)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/navigation/SignInNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/navigation/SignInNavigation.kt new file mode 100644 index 000000000..9bd2c3ee8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/signin/navigation/SignInNavigation.kt @@ -0,0 +1,32 @@ +package org.sopt.dateroad.presentation.ui.signin.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.ui.signin.SignInRoute + +fun NavController.navigationSignIn() { + navigate( + route = SignInRoute.ROUTE + ) { + popUpTo(graph.id) { + inclusive = true + } + } +} + +fun NavGraphBuilder.signInGraph( + navigateToOnboarding: () -> Unit, + navigateToHome: () -> Unit +) { + composable(route = SignInRoute.ROUTE) { + SignInRoute( + navigateToOnboarding = navigateToOnboarding, + navigateToHome = navigateToHome + ) + } +} + +object SignInRoute { + const val ROUTE = "signIn" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/splash/SplashScreen.kt new file mode 100644 index 000000000..8085b3790 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/splash/SplashScreen.kt @@ -0,0 +1,34 @@ +package org.sopt.dateroad.presentation.ui.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import org.sopt.dateroad.R +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun SplashScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .background(DateRoadTheme.colors.purple600), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(226f / (226f + 284f))) + Image(painter = painterResource(id = R.drawable.img_splash_logo), contentDescription = null) + Spacer(modifier = Modifier.weight(284f / (226f + 284f))) + } +} + +@Preview +@Composable +fun SplashScreenPreview() { + SplashScreen() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineContract.kt new file mode 100644 index 000000000..0a593cb3f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineContract.kt @@ -0,0 +1,29 @@ +package org.sopt.dateroad.presentation.ui.timeline + +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class TimelineContract { + data class TimelineUiState( + val loadState: LoadState = LoadState.Idle, + val timelines: List = listOf(), + val currentPage: Int = 0, + val showMaxTimelineCardModal: Boolean = false + ) : UiState + + sealed interface TimelineSideEffect : UiSideEffect { + data object NavigateToPast : TimelineSideEffect + data object NavigateToEnroll : TimelineSideEffect + data class NavigateToTimelineDetail(val timelineType: TimelineType, val timelineId: Int) : TimelineSideEffect + } + + sealed class TimelineEvent : UiEvent { + data class FetchTimeline(val loadState: LoadState, val timelines: List) : TimelineEvent() + data class PageChanged(val page: Int) : TimelineEvent() + data object ShowMaxItemsModal : TimelineEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineScreen.kt new file mode 100644 index 000000000..7f893c44c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineScreen.kt @@ -0,0 +1,229 @@ +package org.sopt.dateroad.presentation.ui.timeline + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.type.TimelineTimeType +import org.sopt.dateroad.presentation.type.EmptyViewType +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.OneButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.component.button.DateRoadFilledButton +import org.sopt.dateroad.presentation.ui.component.button.DateRoadImageButton +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadOneButtonDialogWithDescription +import org.sopt.dateroad.presentation.ui.component.dotsindicator.DotsIndicator +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadLeftTitleTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadEmptyView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.ui.timeline.component.TimelineCard +import org.sopt.dateroad.presentation.util.TimelineAmplitude.COUNT_DATE_SCHEDULE +import org.sopt.dateroad.presentation.util.TimelineAmplitude.DATE_SCHEDULE_NUM +import org.sopt.dateroad.presentation.util.TimelineAmplitude.VIEW_DATE_SCHEDULE +import org.sopt.dateroad.presentation.util.ViewPath.TIMELINE +import org.sopt.dateroad.presentation.util.amplitude.AmplitudeUtils +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun TimelineRoute( + padding: PaddingValues, + viewModel: TimelineViewModel = hiltViewModel(), + navigateToPast: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pagerState = rememberPagerState() + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + AmplitudeUtils.trackEvent(eventName = VIEW_DATE_SCHEDULE) + viewModel.fetchTimeline(TimelineTimeType.FUTURE) + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is TimelineContract.TimelineSideEffect.NavigateToPast -> navigateToPast() + is TimelineContract.TimelineSideEffect.NavigateToEnroll -> navigateToEnroll(EnrollType.TIMELINE, TIMELINE, null) + is TimelineContract.TimelineSideEffect.NavigateToTimelineDetail -> navigateToTimelineDetail(sideEffect.timelineType, sideEffect.timelineId) + } + } + } + + LaunchedEffect(pagerState.currentPage) { + viewModel.setEvent(TimelineContract.TimelineEvent.PageChanged(pagerState.currentPage)) + } + + LaunchedEffect(uiState.loadState, lifecycleOwner) { + if (uiState.loadState == LoadState.Success) AmplitudeUtils.trackEventWithProperty(eventName = COUNT_DATE_SCHEDULE, propertyName = DATE_SCHEDULE_NUM, propertyValue = uiState.timelines.size) + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + TimelineScreen( + padding = padding, + uiState = uiState, + pagerState = pagerState, + onAddDateCardClick = { if (uiState.timelines.size >= 5) viewModel.setEvent(TimelineContract.TimelineEvent.ShowMaxItemsModal) else viewModel.setSideEffect(TimelineContract.TimelineSideEffect.NavigateToEnroll) }, + onDismissMaxDateCardDialog = { viewModel.setState { copy(showMaxTimelineCardModal = false) } }, + navigateToTimelineDetail = { timelineType, timelineId -> viewModel.setSideEffect(TimelineContract.TimelineSideEffect.NavigateToTimelineDetail(timelineType = timelineType, timelineId = timelineId)) }, + onPastButtonClick = { viewModel.setSideEffect(TimelineContract.TimelineSideEffect.NavigateToPast) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun TimelineScreen( + padding: PaddingValues, + uiState: TimelineContract.TimelineUiState, + pagerState: PagerState, + navigateToTimelineDetail: (TimelineType, Int) -> Unit, + onAddDateCardClick: () -> Unit, + onDismissMaxDateCardDialog: () -> Unit, + onPastButtonClick: () -> Unit +) { + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .background(color = DateRoadTheme.colors.white) + ) { + DateRoadLeftTitleTopBar( + title = stringResource(id = R.string.top_bar_title_timeline), + buttonContent = { + DateRoadImageButton( + isEnabled = true, + onClick = onAddDateCardClick, + cornerRadius = 14.dp, + paddingHorizontal = 16.dp, + paddingVertical = 8.dp + ) + } + ) + Spacer(modifier = Modifier.height(52.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + ) { + if (uiState.timelines.isEmpty()) { + DateRoadEmptyView( + modifier = Modifier.fillMaxWidth(), + emptyViewType = EmptyViewType.TIMELINE + ) + } else { + HorizontalPager( + count = uiState.timelines.size, + state = pagerState, + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 35.dp) + ) { page -> + val date = uiState.timelines[page] + val timelineType = TimelineType.getTimelineTypeByIndex(page) + TimelineCard( + timeline = date, + timelineType = timelineType, + onClick = { navigateToTimelineDetail(timelineType, date.timelineId) }, + modifier = Modifier + .padding(end = 16.dp) + ) + } + + Spacer(modifier = Modifier.height(30.dp)) + DotsIndicator( + totalDots = uiState.timelines.size, + selectedIndex = pagerState.currentPage, + indicatorSize = 8.dp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + + Column( + modifier = Modifier + .padding(bottom = 40.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + DateRoadFilledButton( + isEnabled = true, + textContent = stringResource(id = R.string.button_past_date), + onClick = onPastButtonClick, + textStyle = DateRoadTheme.typography.bodyBold15, + enabledBackgroundColor = DateRoadTheme.colors.gray100, + enabledTextColor = DateRoadTheme.colors.black, + disabledBackgroundColor = DateRoadTheme.colors.gray100, + disabledTextColor = DateRoadTheme.colors.black, + cornerRadius = 14.dp, + paddingHorizontal = 29.dp, + paddingVertical = 11.dp + ) + } + } + + if (uiState.showMaxTimelineCardModal) { + DateRoadOneButtonDialogWithDescription( + oneButtonDialogWithDescriptionType = OneButtonDialogWithDescriptionType.CANNOT_ENROLL_COURSE, + onDismissRequest = onDismissMaxDateCardDialog, + onClickConfirm = onDismissMaxDateCardDialog + ) + } +} + +@OptIn(ExperimentalPagerApi::class) +@Preview +@Composable +fun TimelineScreenPreview() { + DATEROADTheme { + TimelineScreen( + padding = PaddingValues(0.dp), + uiState = TimelineContract.TimelineUiState( + loadState = LoadState.Success, + timelines = listOf() + ), + pagerState = rememberPagerState(), + navigateToTimelineDetail = { _, _ -> }, + onDismissMaxDateCardDialog = {}, + onPastButtonClick = {}, + onAddDateCardClick = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineViewModel.kt new file mode 100644 index 000000000..c43346e9f --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/TimelineViewModel.kt @@ -0,0 +1,38 @@ +package org.sopt.dateroad.presentation.ui.timeline + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.type.TimelineTimeType +import org.sopt.dateroad.domain.usecase.GetTimelinesUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class TimelineViewModel @Inject constructor( + private val getTimelinesUseCase: GetTimelinesUseCase +) : BaseViewModel() { + override fun createInitialState(): TimelineContract.TimelineUiState = TimelineContract.TimelineUiState() + + override suspend fun handleEvent(event: TimelineContract.TimelineEvent) { + when (event) { + is TimelineContract.TimelineEvent.FetchTimeline -> setState { copy(loadState = event.loadState, timelines = event.timelines) } + is TimelineContract.TimelineEvent.PageChanged -> setState { copy(currentPage = event.page) } + is TimelineContract.TimelineEvent.ShowMaxItemsModal -> setState { copy(showMaxTimelineCardModal = true) } + } + } + + fun fetchTimeline(timelineTimeType: TimelineTimeType) { + viewModelScope.launch { + setEvent(TimelineContract.TimelineEvent.FetchTimeline(loadState = LoadState.Loading, timelines = currentState.timelines)) + getTimelinesUseCase(timelineTimeType = timelineTimeType) + .onSuccess { timelines -> + setEvent(TimelineContract.TimelineEvent.FetchTimeline(loadState = LoadState.Success, timelines = timelines)) + } + .onFailure { + setEvent(TimelineContract.TimelineEvent.FetchTimeline(loadState = LoadState.Error, timelines = currentState.timelines)) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/component/TimelineCard.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/component/TimelineCard.kt new file mode 100644 index 000000000..429c500a9 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/component/TimelineCard.kt @@ -0,0 +1,213 @@ +package org.sopt.dateroad.presentation.ui.timeline.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Timeline +import org.sopt.dateroad.presentation.type.DateTagType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.ui.theme.DateRoadTheme +import org.sopt.dateroad.ui.theme.defaultDateRoadColors + +@Composable +fun TimelineCard( + modifier: Modifier, + timeline: Timeline, + timelineType: TimelineType, + onClick: (Int) -> Unit = {} +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(24.dp)) + .aspectRatio(291 / 406f) + .background(timelineType.backgroundColor) + .noRippleClickable(onClick = { onClick(timeline.timelineId) }) + ) { + Icon( + painter = painterResource(id = R.drawable.bg_timeline_card), + contentDescription = null, + tint = timelineType.lineColor, + modifier = Modifier + .fillMaxSize() + ) + Column( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = timeline.date, + style = DateRoadTheme.typography.titleExtra24, + color = DateRoadTheme.colors.black, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + DateRoadTextTag( + textContent = timeline.dDay, + tagContentType = TagType.TIMELINE_D_DAY + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(start = 20.dp, end = 20.dp, bottom = 21.dp), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.Start + ) { + if (timeline.tags.size >= 3) { + DateRoadImageTag( + textContent = stringResource(id = timeline.tags[2].titleRes), + imageContent = timeline.tags[2].imageRes, + tagContentType = timelineType.tagType, + spaceValue = 2, + modifier = Modifier + .graphicsLayer(rotationZ = -12f) + .padding(start = 19.dp, bottom = 5.dp) + ) + } + if (timeline.tags.size >= 2) { + DateRoadImageTag( + textContent = stringResource(id = timeline.tags[1].titleRes), + imageContent = timeline.tags[1].imageRes, + tagContentType = timelineType.tagType, + spaceValue = 2, + modifier = Modifier + .graphicsLayer(rotationZ = 15f) + .padding(start = 60.dp, bottom = 10.dp) + ) + } + if (timeline.tags.isNotEmpty()) { + DateRoadImageTag( + textContent = stringResource(id = timeline.tags[0].titleRes), + imageContent = timeline.tags[0].imageRes, + tagContentType = timelineType.tagType, + spaceValue = 2 + ) + } + } + } + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + ) { + val canvasWidth = size.width + val dotLength = 3.dp.toPx() + val dotSpacing = 3.dp.toPx() + var xOffset = 0f + val strokeWidth = 2.dp.toPx() + + drawCircle( + color = defaultDateRoadColors.white, + radius = strokeWidth * 4, + center = Offset(x = strokeWidth / 2 - 5, y = 0f) + ) + + while (xOffset < canvasWidth) { + drawLine( + color = defaultDateRoadColors.white, + start = Offset(xOffset, 0f), + end = Offset(xOffset + dotLength, 0f), + strokeWidth = 2.dp.toPx(), + cap = StrokeCap.Butt + ) + xOffset += dotLength + dotSpacing + } + drawCircle( + color = defaultDateRoadColors.white, + radius = strokeWidth * 4, + center = Offset(x = canvasWidth - strokeWidth / 2 + 5, y = 0f) + ) + } + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 25.dp) + ) { + Text( + text = timeline.city, + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.gray500, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = timeline.title, + style = DateRoadTheme.typography.titleExtra24, + color = DateRoadTheme.colors.black, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + ) + } + } + } +} + +@Preview +@Composable +fun TimelineCardPreview() { + Column { + TimelineCard( + modifier = Modifier, + timelineType = TimelineType.PURPLE, + timeline = Timeline( + timelineId = 0, + dDay = "3", + title = "성수동 당일치기 데이트\n가볼까요?", + date = "JUNE.23", + city = "건대/성수/왕십리", + tags = listOf(DateTagType.SHOPPING, DateTagType.DRIVE, DateTagType.EXHIBITION_POPUP) + ) + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/navigation/TimelineNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/navigation/TimelineNavigation.kt new file mode 100644 index 000000000..dce874073 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timeline/navigation/TimelineNavigation.kt @@ -0,0 +1,34 @@ +package org.sopt.dateroad.presentation.ui.timeline.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import org.sopt.dateroad.presentation.model.MainNavigationBarRoute +import org.sopt.dateroad.presentation.type.EnrollType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.timeline.TimelineRoute + +fun NavController.navigationTimeline(navOptions: NavOptions) { + navigate( + route = MainNavigationBarRoute.Timeline::class.simpleName.orEmpty(), + navOptions = navOptions + ) +} + +fun NavGraphBuilder.timelineNavGraph( + padding: PaddingValues, + navigateToPast: () -> Unit, + navigateToEnroll: (EnrollType, String, Int?) -> Unit, + navigateToTimelineDetail: (TimelineType, Int) -> Unit +) { + composable(route = MainNavigationBarRoute.Timeline::class.simpleName.orEmpty()) { + TimelineRoute( + padding = padding, + navigateToPast = navigateToPast, + navigateToEnroll = navigateToEnroll, + navigateToTimelineDetail = navigateToTimelineDetail + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailContract.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailContract.kt new file mode 100644 index 000000000..aab51df94 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailContract.kt @@ -0,0 +1,34 @@ +package org.sopt.dateroad.presentation.ui.timelinedetail + +import android.content.Context +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.presentation.util.base.UiEvent +import org.sopt.dateroad.presentation.util.base.UiSideEffect +import org.sopt.dateroad.presentation.util.base.UiState +import org.sopt.dateroad.presentation.util.view.LoadState + +class TimelineDetailContract { + data class TimelineDetailUiState( + val loadState: LoadState = LoadState.Idle, + val deleteLoadState: LoadState = LoadState.Idle, + val showKakaoDialog: Boolean = false, + val showDeleteBottomSheet: Boolean = false, + val showDeleteDialog: Boolean = false, + val timelineDetail: TimelineDetail = TimelineDetail() + ) : UiState + + sealed interface TimelineDetailSideEffect : UiSideEffect { + data object PopBackStack : TimelineDetailSideEffect + } + + sealed class TimelineDetailEvent : UiEvent { + data class SetTimelineDetail(val loadState: LoadState, val timelineDetail: TimelineDetail) : TimelineDetailEvent() + data class SetShowDeleteBottomSheet(val showDeleteBottomSheet: Boolean) : TimelineDetailEvent() + data class SetShowDeleteDialog(val showDeleteDialog: Boolean) : TimelineDetailEvent() + data class SetShowKakaoDialog(val showKakaoDialog: Boolean) : TimelineDetailEvent() + data class DeleteTimeline(val deleteLoadState: LoadState) : TimelineDetailEvent() + data class SetLoadState(val loadState: LoadState) : TimelineDetailEvent() + data class SetSideEffect(val sideEffect: TimelineDetailSideEffect) : TimelineDetailEvent() + data class ShareKakao(val context: Context, val timelineDetail: TimelineDetail) : TimelineDetailEvent() + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt new file mode 100644 index 000000000..b2f88b626 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailScreen.kt @@ -0,0 +1,362 @@ +package org.sopt.dateroad.presentation.ui.timelinedetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import org.sopt.dateroad.R +import org.sopt.dateroad.domain.model.Place +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.presentation.type.DateTagType.Companion.getDateTagTypeByName +import org.sopt.dateroad.presentation.type.PlaceCardType +import org.sopt.dateroad.presentation.type.TagType +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.type.TwoButtonDialogType +import org.sopt.dateroad.presentation.type.TwoButtonDialogWithDescriptionType +import org.sopt.dateroad.presentation.ui.component.bottomsheet.DateRoadBasicBottomSheet +import org.sopt.dateroad.presentation.ui.component.card.DateRoadPlaceCard +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialog +import org.sopt.dateroad.presentation.ui.component.dialog.DateRoadTwoButtonDialogWithDescription +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadImageTag +import org.sopt.dateroad.presentation.ui.component.tag.DateRoadTextTag +import org.sopt.dateroad.presentation.ui.component.topbar.DateRoadBasicTopBar +import org.sopt.dateroad.presentation.ui.component.view.DateRoadErrorView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadIdleView +import org.sopt.dateroad.presentation.ui.component.view.DateRoadLoadingView +import org.sopt.dateroad.presentation.util.modifier.noRippleClickable +import org.sopt.dateroad.presentation.util.view.LoadState +import org.sopt.dateroad.ui.theme.DATEROADTheme +import org.sopt.dateroad.ui.theme.DateRoadTheme + +@Composable +fun TimelineDetailRoute( + popBackStack: () -> Unit, + timelineId: Int, + timelineType: TimelineType +) { + val viewModel: TimelineDetailViewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.fetchTimelineDetail(timelineId = timelineId) + } + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is TimelineDetailContract.TimelineDetailSideEffect.PopBackStack -> popBackStack() + } + } + } + + when (uiState.loadState) { + LoadState.Idle -> DateRoadIdleView() + + LoadState.Loading -> DateRoadLoadingView() + + LoadState.Success -> { + TimelineDetailScreen( + uiState = uiState, + timelineType = timelineType, + onTopBarItemClick = popBackStack, + onButtonClick = { viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowDeleteBottomSheet(true)) }, + showKakaoClicked = { viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowKakaoDialog(true)) }, + setShowKakaoDialog = { showKakaoDialog -> viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowKakaoDialog(showKakaoDialog)) }, + setShowDeleteBottomSheet = { showDeleteBottomSheet -> viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowDeleteBottomSheet(showDeleteBottomSheet)) }, + setShowDeleteDialog = { showDeleteDialog -> viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.SetShowDeleteDialog(showDeleteDialog)) }, + onDeleteConfirm = { viewModel.deleteTimeline(timelineId = timelineId) }, + onKakaoShareConfirm = { viewModel.setEvent(TimelineDetailContract.TimelineDetailEvent.ShareKakao(context, uiState.timelineDetail)) } + ) + } + + LoadState.Error -> DateRoadErrorView() + } + + when (uiState.deleteLoadState) { + LoadState.Success -> popBackStack() + else -> Unit + } +} + +@Composable +fun TimelineDetailScreen( + uiState: TimelineDetailContract.TimelineDetailUiState, + timelineType: TimelineType, + onTopBarItemClick: () -> Unit = {}, + onButtonClick: () -> Unit = {}, + showKakaoClicked: () -> Unit = {}, + setShowKakaoDialog: (Boolean) -> Unit, + setShowDeleteBottomSheet: (Boolean) -> Unit, + setShowDeleteDialog: (Boolean) -> Unit, + onDeleteConfirm: () -> Unit, + onKakaoShareConfirm: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = timelineType.backgroundColor) + ) { + DateRoadBasicTopBar( + title = stringResource(id = R.string.top_bar_title_timeline), + leftIconResource = R.drawable.ic_top_bar_back_white, + buttonContent = { + Icon( + painterResource(id = R.drawable.btn_course_detail_more_black), + contentDescription = null, + modifier = Modifier.noRippleClickable(onClick = onButtonClick) + ) + }, + onLeftIconClick = onTopBarItemClick + ) + Box( + modifier = Modifier + .fillMaxWidth() + .background(timelineType.backgroundColor) + ) { + Icon( + painter = painterResource(id = R.drawable.bg_timeline_detail), + contentDescription = null, + tint = timelineType.lineColor, + modifier = Modifier + .align(Alignment.BottomEnd) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 11.dp, start = 16.dp, end = 16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = uiState.timelineDetail.date, + style = DateRoadTheme.typography.bodyMed15, + color = DateRoadTheme.colors.black + ) + if (uiState.timelineDetail.dDay != "") { + DateRoadTextTag( + textContent = uiState.timelineDetail.dDay, + tagContentType = TagType.TIMELINE_D_DAY + ) + } + } + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = uiState.timelineDetail.title, + style = DateRoadTheme.typography.titleExtra24, + color = DateRoadTheme.colors.black, + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = uiState.timelineDetail.city, + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.gray500 + ) + LazyRow( + modifier = Modifier + .padding(top = 10.dp), + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + items(uiState.timelineDetail.tags) { tag -> + tag.getDateTagTypeByName()?.let { tagType -> + DateRoadImageTag( + textContent = stringResource(id = tagType.titleRes), + imageContent = tagType.imageRes, + tagContentType = timelineType.tagType + ) + } + } + } + Spacer(modifier = Modifier.height(18.dp)) + } + } + Box( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .fillMaxSize() + .background(color = DateRoadTheme.colors.white) + ) { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 14.dp, bottom = 90.dp) + ) { + Text( + text = uiState.timelineDetail.startAt, + style = DateRoadTheme.typography.bodySemi15, + color = DateRoadTheme.colors.black + ) + Spacer(modifier = Modifier.height(14.dp)) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(uiState.timelineDetail.places.size) { index -> + DateRoadPlaceCard( + placeCardType = PlaceCardType.COURSE_NORMAL, + sequence = index, + place = uiState.timelineDetail.places[index] + ) + } + } + } + + if (uiState.timelineDetail.dDay.isNotEmpty()) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = 16.dp, horizontal = 70.dp) + .background(DateRoadTheme.colors.purple600, CircleShape) + .noRippleClickable(onClick = showKakaoClicked) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .padding(top = 14.dp, bottom = 14.dp, start = 24.dp) + .background(DateRoadTheme.colors.kakaoYellow, CircleShape) + .clip(CircleShape) + ) { + Image( + painter = painterResource(id = R.drawable.ic_kakao_logo), + contentDescription = null, + modifier = Modifier + .padding(6.dp) + .background(DateRoadTheme.colors.kakaoYellow) + .clip(CircleShape) + ) + } + Text( + text = stringResource(id = R.string.one_button_dialog_with_description_share_kakao), + style = DateRoadTheme.typography.bodyBold15, + color = DateRoadTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(end = 24.dp) + ) + } + } + } + } + } + + if (uiState.showKakaoDialog) { + DateRoadTwoButtonDialog( + twoButtonDialogType = TwoButtonDialogType.OPEN_KAKAOTALK, + onDismissRequest = { setShowKakaoDialog(false) }, + onClickConfirm = { + setShowKakaoDialog(false) + onKakaoShareConfirm() + }, + onClickDismiss = { setShowKakaoDialog(false) } + ) + } + + if (uiState.showDeleteBottomSheet) { + DateRoadBasicBottomSheet( + isBottomSheetOpen = true, + title = stringResource(id = R.string.timeline_detail_bottom_sheet_title), + isButtonEnabled = false, + buttonText = stringResource(id = R.string.dialog_cancel), + onButtonClick = { setShowDeleteBottomSheet(false) }, + itemList = listOf( + stringResource(id = R.string.timeline_detail_delete) to { setShowDeleteDialog(true) } + ), + onDismissRequest = { setShowDeleteBottomSheet(false) } + ) + } + + if (uiState.showDeleteDialog) { + DateRoadTwoButtonDialogWithDescription( + twoButtonDialogWithDescriptionType = TwoButtonDialogWithDescriptionType.DELETE_TIMELINE, + onDismissRequest = { setShowDeleteDialog(false) }, + onClickConfirm = onDeleteConfirm, + onClickDismiss = { setShowDeleteDialog(false) } + ) + } +} + +@Preview +@Composable +fun TimelineDetailScreenPreview() { + DATEROADTheme { + TimelineDetailScreen( + uiState = TimelineDetailContract.TimelineDetailUiState( + loadState = LoadState.Success, + timelineDetail = TimelineDetail( + date = "2024-08-17", + dDay = "D-3", + title = "Seoul City Tour", + city = "Seoul", + startAt = "10:00 AM", + places = listOf( + Place( + title = "도당고등학교 2-7 데이트", + duration = "1.5" + ), + Place( + title = "2번 데이트", + duration = "2.5" + ) + ), + tags = listOf("History", "Culture"), + timelineId = 123 + ), + showKakaoDialog = false, + showDeleteBottomSheet = false, + showDeleteDialog = false, + deleteLoadState = LoadState.Idle + ), + timelineType = TimelineType.getTimelineTypeByIndex(1), + onTopBarItemClick = {}, + onButtonClick = {}, + showKakaoClicked = {}, + setShowKakaoDialog = {}, + setShowDeleteBottomSheet = {}, + setShowDeleteDialog = {}, + onDeleteConfirm = {}, + onKakaoShareConfirm = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailViewModel.kt new file mode 100644 index 000000000..2b8e85004 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/TimelineDetailViewModel.kt @@ -0,0 +1,90 @@ +package org.sopt.dateroad.presentation.ui.timelinedetail + +import android.content.Context +import androidx.lifecycle.viewModelScope +import com.kakao.sdk.common.util.KakaoCustomTabsClient +import com.kakao.sdk.share.ShareClient +import com.kakao.sdk.share.WebSharerClient +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch +import org.sopt.dateroad.domain.model.TimelineDetail +import org.sopt.dateroad.domain.usecase.DeleteTimelineUseCase +import org.sopt.dateroad.domain.usecase.GetNicknameUseCase +import org.sopt.dateroad.domain.usecase.GetTimelineDetailUseCase +import org.sopt.dateroad.presentation.util.base.BaseViewModel +import org.sopt.dateroad.presentation.util.view.LoadState + +@HiltViewModel +class TimelineDetailViewModel @Inject constructor( + private val deleteTimelineUseCase: DeleteTimelineUseCase, + private val getTimelineDetailUseCase: GetTimelineDetailUseCase, + private val getNickNameUseCase: GetNicknameUseCase +) : BaseViewModel() { + override fun createInitialState(): TimelineDetailContract.TimelineDetailUiState = TimelineDetailContract.TimelineDetailUiState() + + override suspend fun handleEvent(event: TimelineDetailContract.TimelineDetailEvent) { + when (event) { + is TimelineDetailContract.TimelineDetailEvent.SetTimelineDetail -> setState { copy(loadState = event.loadState, timelineDetail = event.timelineDetail) } + is TimelineDetailContract.TimelineDetailEvent.SetShowDeleteBottomSheet -> setState { copy(showDeleteBottomSheet = event.showDeleteBottomSheet) } + is TimelineDetailContract.TimelineDetailEvent.SetShowDeleteDialog -> setState { copy(showDeleteDialog = event.showDeleteDialog) } + is TimelineDetailContract.TimelineDetailEvent.SetShowKakaoDialog -> setState { copy(showKakaoDialog = event.showKakaoDialog) } + is TimelineDetailContract.TimelineDetailEvent.DeleteTimeline -> setState { copy(deleteLoadState = event.deleteLoadState) } + is TimelineDetailContract.TimelineDetailEvent.SetLoadState -> setState { copy(loadState = event.loadState) } + is TimelineDetailContract.TimelineDetailEvent.SetSideEffect -> setSideEffect(event.sideEffect) + is TimelineDetailContract.TimelineDetailEvent.ShareKakao -> shareKakao(event.context, event.timelineDetail) + } + } + + fun fetchTimelineDetail(timelineId: Int) { + viewModelScope.launch { + setEvent(TimelineDetailContract.TimelineDetailEvent.SetTimelineDetail(loadState = LoadState.Loading, timelineDetail = currentState.timelineDetail)) + getTimelineDetailUseCase(timelineId).onSuccess { timelineDetail -> + setEvent(TimelineDetailContract.TimelineDetailEvent.SetTimelineDetail(loadState = LoadState.Success, timelineDetail = timelineDetail)) + }.onFailure { + setEvent(TimelineDetailContract.TimelineDetailEvent.SetTimelineDetail(loadState = LoadState.Error, timelineDetail = currentState.timelineDetail)) + } + } + } + + fun deleteTimeline(timelineId: Int) { + viewModelScope.launch { + setEvent(TimelineDetailContract.TimelineDetailEvent.DeleteTimeline(deleteLoadState = LoadState.Loading)) + deleteTimelineUseCase(timelineId).onSuccess { + setEvent(TimelineDetailContract.TimelineDetailEvent.DeleteTimeline(deleteLoadState = LoadState.Success)) + }.onFailure { + setEvent(TimelineDetailContract.TimelineDetailEvent.DeleteTimeline(deleteLoadState = LoadState.Error)) + } + } + } + + fun shareKakao(context: Context, timelineDetail: TimelineDetail) { + val templateId = 109999 + val templateArgs = mutableMapOf() + + templateArgs["userName"] = getNickNameUseCase() + templateArgs["startAt"] = timelineDetail.startAt + + timelineDetail.places.forEachIndexed { index, place -> + if (index < 5) { + templateArgs["name${index + 1}"] = place.title + templateArgs["duration${index + 1}"] = place.duration + } + } + + if (ShareClient.instance.isKakaoTalkSharingAvailable(context)) { + ShareClient.instance.shareCustom(context, templateId.toLong(), templateArgs) { sharingResult, error -> + if (sharingResult != null) { + context.startActivity(sharingResult.intent) + } + } + } else { + val sharerUrl = WebSharerClient.instance.makeCustomUrl(templateId.toLong(), templateArgs) + try { + KakaoCustomTabsClient.openWithDefault(context, sharerUrl) + } catch (e: UnsupportedOperationException) { + KakaoCustomTabsClient.open(context, sharerUrl) + } + } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt new file mode 100644 index 000000000..dfc9c1efa --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/ui/timelinedetail/navigation/TimelineDetailNavigation.kt @@ -0,0 +1,44 @@ +package org.sopt.dateroad.presentation.ui.timelinedetail.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import org.sopt.dateroad.presentation.type.TimelineType +import org.sopt.dateroad.presentation.ui.timelinedetail.TimelineDetailRoute + +fun NavController.navigateToTimelineDetail(timelineType: TimelineType, timelineId: Int, navOptions: NavOptions? = null) { + navigate(TimelineDetailRoutes.route(timelineType, timelineId), navOptions) +} + +fun NavGraphBuilder.timelineDetailGraph( + popBackStack: () -> Unit +) { + composable( + route = TimelineDetailRoutes.ROUTE_WITH_ARGUMENT, + arguments = listOf( + navArgument(TimelineDetailRoutes.TIMELINE_TYPE) { type = NavType.StringType }, + navArgument(TimelineDetailRoutes.TIMELINE_ID) { type = NavType.IntType } + ) + ) { backStackEntry -> + val timelineType = TimelineType.valueOf(backStackEntry.arguments?.getString(TimelineDetailRoutes.TIMELINE_TYPE) ?: TimelineType.PINK.name) + val timelineId = backStackEntry.arguments?.getInt(TimelineDetailRoutes.TIMELINE_ID) ?: 1 + + TimelineDetailRoute( + popBackStack = popBackStack, + timelineId = timelineId, + timelineType = timelineType + ) + } +} + +object TimelineDetailRoutes { + private const val ROUTE = "timeline_detail" + const val TIMELINE_TYPE = "timelineType" + const val TIMELINE_ID = "timelineId" + const val ROUTE_WITH_ARGUMENT = "$ROUTE/{$TIMELINE_TYPE}/{$TIMELINE_ID}" + + fun route(timelineType: TimelineType, timelineId: Int) = "$ROUTE/${timelineType.name}/$timelineId" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/Constraints.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/Constraints.kt new file mode 100644 index 000000000..c6894b405 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/Constraints.kt @@ -0,0 +1,125 @@ +package org.sopt.dateroad.presentation.util + +object PointHistoryTab { + const val GAINED_HISTORY_POSITION = 0 + const val USED_HISTORY_POSITION = 1 +} + +object EnrollScreen { + const val FIRST = 1 + const val SECOND = 2 + const val THIRD = 3 + const val MAX_ITEMS = 10 + const val TITLE_MIN_LENGTH = 5 +} + +object TimePicker { + const val AM = "오전" + const val PM = "오후" +} + +object TotalCostZero { + const val ZERO_COST = "무지출" +} + +object DatePicker { + const val DATE_PATTERN = "yyyy.MM.dd" +} + +object WebViewUrl { + const val PRIVACY_POLICY_URL = "https://www.notion.so/hooooooni/04da4aa279ca4b599193784091a52859" + const val REPORT_URL = "https://tally.so/r/w4L1a5" + const val ASK_URL = "https://dateroad.notion.site/1055d2f7bfe94b3fa6c03709448def21?pvs=4" +} + +object Default { + const val REGION = "지역" +} + +object Point { + const val POINT = 50 + const val POINT_USED = "POINT_USED" + const val POINT_USED_DESCRIPTION = "코스 열람하기" +} + +object Token { + const val BEARER = "Bearer " +} + +object Time { + const val TIME = " 시간" +} +object LoadingView { + const val LOTTIE = "loading.json" + const val CLIPMIN = 0 + const val CLIPMAX = 1200 +} + +object Pattern { + private const val NICKNAME_PATTERN = "^[ㄱ-ㅎ가-힣a-zA-Z0-9]*$" + val NICKNAME_REGEX = Regex(NICKNAME_PATTERN) +} + +object ViewPath { + const val HOME = "홈" + const val TIMELINE = "데이트 일정" + const val MY_COURSE_READ = "내가 열람한 코스" + const val COURSE_DETAIL = "코스 상세" + const val LOOK = "코스 둘러보기" +} + +object EnrollAmplitude { + const val VIEW_ADD_SCHEDULE = "view_add_schedule" + const val CLICK_SCHEDULE1_BACK = "click_schedule1_back" + const val VIEW_ADD_SCHEDULE2 = "view_add_schedule2" + const val CLICK_SCHEDULE2_BACK = "click_schedule2_back" + const val CLICK_BRING_COURSE = "click_bring_course" + const val VIEW_ADD_BRING_COURSE = "view_add_bringcourse" + const val VIEW_ADD_BRING_COURSE2 = "view_add_bringcourse2" + const val VIEW_COURSE1 = "view_course1" + const val CLICK_COURSE1_BACK = "click_course1_back" + const val CLICK_COURSE2_BACK = "click_course2_back" + const val CLICK_COURSE3_BACK = "click_course3_back" + const val VIEW_PATH = "view_path" + const val DATE_TITLE = "date_title" + const val DATE_DATE = "date_date" + const val DATE_TIME = "date_time" + const val DATE_TAG_NUM = "date_tag_num" + const val DATE_AREA = "date_area" + const val DATE_DETAIL_LOCATION = "date_detail_location" + const val DATE_DETAIL_TIME = "date_detail_time" + const val DATE_COURSE_NUM = "date_course_num" + const val COURSE_IMAGE = "course_image" + const val COURSE_TITLE = "course_title" + const val COURSE_DATE = "course_date" + const val COURSE_START_TIME = "course_start_time" + const val COURSE_TAGS = "course_tags" + const val COURSE_LOCATION = "course_location" + const val DATE_LOCATION = "date_location" + const val DATE_SPEND_TIME = "date_spend_time" + const val LOCATION_NUM = "location_num" + const val COURSE_CONTENT_BOOL = "course_content_bool" + const val COURSE_CONTENT_NUM = "course_content_num" + const val COURSE_COST = "course_cost" +} + +object MyCourseAmplitude { + const val VIEW_PURCHASED_COURSE = "view_purchased_course" + const val CLICK_PURCHASED_BACK = "click_purchased_back" +} + +object TimelineAmplitude { + const val VIEW_DATE_SCHEDULE = "view_date_schedule" + const val COUNT_DATE_SCHEDULE = "count_date_schedule" + const val CLICK_ADD_SCHEDULE = "click_add_schedule" + const val DATE_SCHEDULE_NUM = "date_schedule_num" +} + +object UserPropertyAmplitude { + const val USER_NAME = "user_name" + const val USER_POINT = "user_point" + const val USER_FREE_REMAINED = "user_free_remained" + const val USER_PURCHASE_COUNT = "user_purchase_count" + const val USER_COURSE_COUNT = "user_course_count" + const val USER_SCHEDULE_NUM = "user_schedule_num" +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/amplitude/AmplitudeUtils.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/amplitude/AmplitudeUtils.kt new file mode 100644 index 000000000..3cbe4630e --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/amplitude/AmplitudeUtils.kt @@ -0,0 +1,43 @@ +package org.sopt.dateroad.presentation.util.amplitude + +import android.content.Context +import com.amplitude.android.Amplitude +import com.amplitude.android.Configuration +import com.amplitude.core.events.Identify +import org.sopt.dateroad.BuildConfig + +object AmplitudeUtils { + private lateinit var amplitude: Amplitude + + fun initAmplitude(context: Context) { + amplitude = Amplitude( + Configuration( + apiKey = BuildConfig.AMPLITUDE_API_KEY, + context = context + ) + ) + } + + fun trackEvent(eventName: String) { + amplitude.track(eventType = eventName) + } + + fun trackEventWithProperty(eventName: String, propertyName: String, propertyValue: T) { + amplitude.track( + eventType = eventName, + eventProperties = mapOf(propertyName to propertyValue) + ) + } + + fun trackEventWithProperties(eventName: String, properties: Map) { + amplitude.track(eventType = eventName, eventProperties = properties) + } + + fun updateStringUserProperty(propertyName: String, propertyValue: String) { + amplitude.identify(Identify().set(property = propertyName, value = propertyValue)) + } + + fun updateIntUserProperty(propertyName: String, propertyValue: Int) { + amplitude.identify(Identify().set(property = propertyName, value = propertyValue)) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/base/BaseViewModel.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/base/BaseViewModel.kt new file mode 100644 index 000000000..f56743741 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/base/BaseViewModel.kt @@ -0,0 +1,50 @@ +package org.sopt.dateroad.presentation.util.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +abstract class BaseViewModel() : + ViewModel() { + private val initialState: State by lazy { createInitialState() } + abstract fun createInitialState(): State + + private val _uiState = MutableStateFlow(initialState) + val uiState: StateFlow + get() = _uiState.asStateFlow() + val currentState: State + get() = uiState.value + + private val _event: MutableSharedFlow = MutableSharedFlow() + val event: SharedFlow + get() = _event.asSharedFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect: Flow + get() = _sideEffect.asSharedFlow() + + fun setState(reduce: State.() -> State) { + _uiState.value = currentState.reduce() + } + + open fun setEvent(event: Event) { + dispatchEvent(event) + } + + fun dispatchEvent(event: Event) = viewModelScope.launch { + handleEvent(event) + } + + protected abstract suspend fun handleEvent(event: Event) + + fun setSideEffect(sideEffect: SideEffect) { + viewModelScope.launch { _sideEffect.emit(sideEffect) } + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiEvent.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiEvent.kt new file mode 100644 index 000000000..ef1246c2c --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiEvent.kt @@ -0,0 +1,3 @@ +package org.sopt.dateroad.presentation.util.base + +interface UiEvent diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiSideEffect.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiSideEffect.kt new file mode 100644 index 000000000..a869c3c3d --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiSideEffect.kt @@ -0,0 +1,3 @@ +package org.sopt.dateroad.presentation.util.base + +interface UiSideEffect diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiState.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiState.kt new file mode 100644 index 000000000..f2f1b45b3 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/base/UiState.kt @@ -0,0 +1,3 @@ +package org.sopt.dateroad.presentation.util.base + +interface UiState diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/context/ContextExt.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/context/ContextExt.kt new file mode 100644 index 000000000..aca08ee36 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/context/ContextExt.kt @@ -0,0 +1,9 @@ +package org.sopt.dateroad.presentation.util.context + +import android.content.Context +import android.widget.Toast + +fun Context.showToast(message: String, isShort: Boolean = true) { + val duration = if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + Toast.makeText(this, message, duration).show() +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/draganddrop/DragAndDropListState.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/draganddrop/DragAndDropListState.kt new file mode 100644 index 000000000..e0783f1cd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/draganddrop/DragAndDropListState.kt @@ -0,0 +1,104 @@ +package org.sopt.dateroad.presentation.util.draganddrop + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import kotlinx.coroutines.Job +import org.sopt.dateroad.presentation.util.lazylist.getVisibleItemInfoFor +import org.sopt.dateroad.presentation.util.lazylist.offsetEnd + +class DragAndDropListState( + val lazyListState: LazyListState, + private val onMove: (Int, Int) -> Unit +) { + private var draggedDistance by mutableFloatStateOf(0f) + private var initiallyDraggedElement by mutableStateOf(null) + var currentIndexOfDraggedItem by mutableStateOf(null) + private val initialOffsets: Pair? + get() = initiallyDraggedElement?.let { + Pair(it.offset, it.offsetEnd) + } + val elementDisplacement: Float? + get() = currentIndexOfDraggedItem + ?.let { lazyListState.getVisibleItemInfoFor(absoluteIndex = it) } + ?.let { item -> (initiallyDraggedElement?.offset ?: 0f).toFloat() + draggedDistance - item.offset } + + private val currentElement: LazyListItemInfo? + get() = currentIndexOfDraggedItem?.let { + lazyListState.getVisibleItemInfoFor(absoluteIndex = it) + } + + private var overScrollJob by mutableStateOf(null) + + fun onDragStart(offset: Offset) { + lazyListState.layoutInfo.visibleItemsInfo + .firstOrNull { item -> + offset.y.toInt() in item.offset..item.offsetEnd + }?.also { + currentIndexOfDraggedItem = it.index + initiallyDraggedElement = it + } + } + + fun onDragInterrupted() { + draggedDistance = 0f + currentIndexOfDraggedItem = null + initiallyDraggedElement = null + overScrollJob?.cancel() + } + + fun onDrag(offset: Offset) { + draggedDistance += offset.y + + initialOffsets?.let { (topOffset, bottomOffset) -> + val startOffset = topOffset + draggedDistance + val endOffset = bottomOffset + draggedDistance + + currentElement?.let { hovered -> + lazyListState.layoutInfo.visibleItemsInfo + .filterNot { item -> + item.offsetEnd <= startOffset || item.offset >= endOffset || hovered.index == item.index + } + .firstOrNull { item -> + when { + startOffset > hovered.offset -> (endOffset > item.offset + item.size / 2) + else -> (startOffset < item.offset + item.size / 2) + } + }?.also { item -> + currentIndexOfDraggedItem?.let { current -> + onMove.invoke(current, item.index) + } + currentIndexOfDraggedItem = item.index + } + } + } + } + + fun checkForOverScroll(): Float { + return initiallyDraggedElement?.let { + val startOffset = it.offset + draggedDistance + val endOffset = it.offsetEnd + draggedDistance + + return@let when { + draggedDistance > 0 -> (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 } + + draggedDistance < 0 -> (startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 } + + else -> null + } + } ?: 0f + } +} + +@Composable +fun rememberDragAndDropListState( + lazyListState: LazyListState = rememberLazyListState(), + onMove: (Int, Int) -> Unit +): DragAndDropListState = remember { DragAndDropListState(lazyListState = lazyListState, onMove = onMove) } diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/lazylist/LazyListExt.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/lazylist/LazyListExt.kt new file mode 100644 index 000000000..b5fc809cd --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/lazylist/LazyListExt.kt @@ -0,0 +1,11 @@ +package org.sopt.dateroad.presentation.util.lazylist + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState + +fun LazyListState.getVisibleItemInfoFor(absoluteIndex: Int): LazyListItemInfo? { + return this.layoutInfo.visibleItemsInfo.getOrNull(absoluteIndex - this.layoutInfo.visibleItemsInfo.first().index) +} + +val LazyListItemInfo.offsetEnd: Int + get() = this.offset + this.size diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/modifier/ModifierExt.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/modifier/ModifierExt.kt new file mode 100644 index 000000000..8287b1c08 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/modifier/ModifierExt.kt @@ -0,0 +1,48 @@ +package org.sopt.dateroad.presentation.util.modifier + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.input.pointer.pointerInteropFilter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +inline fun Modifier.noRippleClickable( + crossinline onClick: () -> Unit = {} +): Modifier = composed { + this.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun Modifier.noRippleDebounceClickable( + onClick: suspend () -> Unit +): Modifier = composed { + var clickable by remember { mutableStateOf(true) } + + pointerInteropFilter { + if (clickable) { + clickable = false + CoroutineScope(Dispatchers.Main).launch { + onClick() + delay(500) + clickable = true + } + } + true + } +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/mutablelist/MutableListExt.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/mutablelist/MutableListExt.kt new file mode 100644 index 000000000..33d1b73b8 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/mutablelist/MutableListExt.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.presentation.util.mutablelist + +fun MutableList.move(from: Int, to: Int): MutableList { + if (from == to) return this + val item = this.removeAt(from) + this.add(to, item) + return this +} diff --git a/app/src/main/java/org/sopt/dateroad/presentation/util/view/LoadState.kt b/app/src/main/java/org/sopt/dateroad/presentation/util/view/LoadState.kt new file mode 100644 index 000000000..e02e0f0bf --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/presentation/util/view/LoadState.kt @@ -0,0 +1,8 @@ +package org.sopt.dateroad.presentation.util.view + +enum class LoadState { + Idle, + Loading, + Success, + Error +} diff --git a/app/src/main/java/org/sopt/dateroad/ui/theme/Color.kt b/app/src/main/java/org/sopt/dateroad/ui/theme/Color.kt new file mode 100644 index 000000000..f9007a194 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/ui/theme/Color.kt @@ -0,0 +1,108 @@ +package org.sopt.dateroad.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +// Main +val Purple100 = Color(0xFFD3D0FF) +val Purple200 = Color(0xFFBDB8FF) +val Purple300 = Color(0xFFB3ACFF) +val Purple400 = Color(0xFFA8A0FF) +val Purple500 = Color(0xFF6E61FF) +val Purple600 = Color(0xFF4A3CEF) +val Purple700 = Color(0xFF4322BB) + +// Sub +val Pink100 = Color(0xFFEFD6F8) +val Pink200 = Color(0xFFE1C2F9) +val Pink300 = Color(0xFFE1B7F0) +val Pink400 = Color(0xFFE3A3F9) +val Lime100 = Color(0xFFEFFDB7) +val Lime200 = Color(0xFFDFF37C) +val Lime300 = Color(0xFFD3EB77) + +// GrayScale +val Black = Color(0xFF090909) +val Gray100 = Color(0xFFEBEBF3) +val Gray200 = Color(0xFFD5D5DE) +val Gray300 = Color(0xFFAEAFBC) +val Gray400 = Color(0xFF7B7C87) +val Gray500 = Color(0xFF53525B) +val Gray600 = Color(0xFF080909) +val White = Color(0xFFFFFFFF) + +// Notif +val AlertRed = Color(0xFFFF0000) + +// Kakao +val KakaoYellow = Color(0xFFFEE500) + +@Immutable +data class DateRoadColors( + // Main + val purple100: Color, + val purple200: Color, + val purple300: Color, + val purple400: Color, + val purple500: Color, + val purple600: Color, + val purple700: Color, + val pink100: Color, + val pink200: Color, + val pink300: Color, + val pink400: Color, + val lime100: Color, + val lime200: Color, + val lime300: Color, + + // GrayScale + val black: Color, + val gray100: Color, + val gray200: Color, + val gray300: Color, + val gray400: Color, + val gray500: Color, + val gray600: Color, + val white: Color, + + // Notif + val alertRed: Color, + + // Kakao + val kakaoYellow: Color +) + +val defaultDateRoadColors = DateRoadColors( + // Main + purple100 = Purple100, + purple200 = Purple200, + purple300 = Purple300, + purple400 = Purple400, + purple500 = Purple500, + purple600 = Purple600, + purple700 = Purple700, + pink100 = Pink100, + pink200 = Pink200, + pink300 = Pink300, + pink400 = Pink400, + lime100 = Lime100, + lime200 = Lime200, + lime300 = Lime300, + + // GrayScale + black = Black, + gray100 = Gray100, + gray200 = Gray200, + gray300 = Gray300, + gray400 = Gray400, + gray500 = Gray500, + gray600 = Gray600, + white = White, + + // Notif + alertRed = AlertRed, + kakaoYellow = KakaoYellow +) + +val LocalDateRoadColors = staticCompositionLocalOf { defaultDateRoadColors } diff --git a/app/src/main/java/org/sopt/dateroad/ui/theme/Theme.kt b/app/src/main/java/org/sopt/dateroad/ui/theme/Theme.kt new file mode 100644 index 000000000..29fadb183 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/ui/theme/Theme.kt @@ -0,0 +1,59 @@ +package org.sopt.dateroad.ui.theme + +import android.app.Activity +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +object DateRoadTheme { + val colors: DateRoadColors + @Composable + @ReadOnlyComposable + get() = LocalDateRoadColors.current + + val typography: DateRoadTypography + @Composable + @ReadOnlyComposable + get() = LocalDateRoadTypography.current +} + +@Composable +fun ProvideDateRoadColorsAndTypography( + colors: DateRoadColors, + typography: DateRoadTypography, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalDateRoadColors provides colors, + LocalDateRoadTypography provides typography, + content = content + ) +} + +@Composable +fun DATEROADTheme( + backgroundColor: Color = defaultDateRoadColors.white, + content: @Composable () -> Unit +) { + ProvideDateRoadColorsAndTypography(colors = defaultDateRoadColors, typography = defaultDateRoadTypography) { + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + (view.context as Activity).window.run { + statusBarColor = backgroundColor.toArgb() + WindowCompat.getInsetsController(this, view).isAppearanceLightStatusBars = true + } + } + } + + MaterialTheme( + content = content + ) + } +} diff --git a/app/src/main/java/org/sopt/dateroad/ui/theme/Type.kt b/app/src/main/java/org/sopt/dateroad/ui/theme/Type.kt new file mode 100644 index 000000000..160ca1240 --- /dev/null +++ b/app/src/main/java/org/sopt/dateroad/ui/theme/Type.kt @@ -0,0 +1,143 @@ +package org.sopt.dateroad.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.sp +import org.sopt.dateroad.R + +val SuitBold = FontFamily(Font(R.font.suit_bold)) +val SuitExtraBold = FontFamily(Font(R.font.suit_extrabold)) +val SuitExtraLight = FontFamily(Font(R.font.suit_extralight)) +val SuitMedium = FontFamily(Font(R.font.suit_medium)) +val SuitRegular = FontFamily(Font(R.font.suit_regular)) +val SuitSemiBold = FontFamily(Font(R.font.suit_semibold)) + +@Immutable +data class DateRoadTypography( + // Title + val titleExtra24: TextStyle, + val titleExtra20: TextStyle, + val titleBold20: TextStyle, + val titleBold18: TextStyle, + val titleMed18: TextStyle, + + // Body + val bodyBold17: TextStyle, + val bodySemi17: TextStyle, + val bodyMed17: TextStyle, + val bodyBold15: TextStyle, + val bodyBold15Course: TextStyle, + val bodySemi15: TextStyle, + val bodyMed15: TextStyle, + val bodyBold13: TextStyle, + val bodySemi13: TextStyle, + val bodyMed13: TextStyle, + val bodyMed13Context: TextStyle, + + // Caption + val capBold11: TextStyle, + val capReg11: TextStyle +) + +val defaultDateRoadTypography = DateRoadTypography( + // Title + titleExtra24 = TextStyle( + fontFamily = SuitExtraBold, + fontSize = 24.sp, + lineHeight = 24.sp * 1.3 + ), + titleExtra20 = TextStyle( + fontFamily = SuitExtraBold, + fontSize = 20.sp, + lineHeight = 20.sp * 1.4 + ), + titleBold20 = TextStyle( + fontFamily = SuitBold, + fontSize = 20.sp, + lineHeight = 20.sp * 1.4 + ), + titleBold18 = TextStyle( + fontFamily = SuitBold, + fontSize = 18.sp, + lineHeight = 18.sp * 1.4 + ), + titleMed18 = TextStyle( + fontFamily = SuitMedium, + fontSize = 18.sp, + lineHeight = 18.sp * 1.4 + ), + + // Body + bodyBold17 = TextStyle( + fontFamily = SuitBold, + fontSize = 17.sp, + lineHeight = 17.sp * 1.4 + ), + bodySemi17 = TextStyle( + fontFamily = SuitSemiBold, + fontSize = 17.sp, + lineHeight = 17.sp * 1.4 + ), + bodyMed17 = TextStyle( + fontFamily = SuitMedium, + fontSize = 17.sp, + lineHeight = 17.sp * 1.4 + ), + bodyBold15 = TextStyle( + fontFamily = SuitBold, + fontSize = 15.sp, + lineHeight = 15.sp * 1.4 + ), + bodyBold15Course = TextStyle( + fontFamily = SuitBold, + fontSize = 15.sp, + lineHeight = 15.sp * 1.3 + ), + bodySemi15 = TextStyle( + fontFamily = SuitSemiBold, + fontSize = 15.sp, + lineHeight = 15.sp * 1.4 + ), + bodyMed15 = TextStyle( + fontFamily = SuitMedium, + fontSize = 15.sp, + lineHeight = 15.sp * 1.4 + ), + bodyBold13 = TextStyle( + fontFamily = SuitBold, + fontSize = 13.sp, + lineHeight = 13.sp * 1.4 + ), + bodySemi13 = TextStyle( + fontFamily = SuitSemiBold, + fontSize = 13.sp, + lineHeight = 13.sp * 1.4 + ), + bodyMed13 = TextStyle( + fontFamily = SuitMedium, + fontSize = 13.sp, + lineHeight = 13.sp * 1.4 + ), + bodyMed13Context = TextStyle( + fontFamily = SuitMedium, + fontSize = 13.sp, + lineHeight = 13.sp * 1.5 + ), + + // Caption + capBold11 = TextStyle( + fontFamily = SuitBold, + fontSize = 11.sp, + lineHeight = 11.sp * 1.4 + ), + capReg11 = TextStyle( + fontFamily = SuitRegular, + fontSize = 11.sp, + lineHeight = 11.sp * 1.4 + ) +) + +val LocalDateRoadTypography = staticCompositionLocalOf { defaultDateRoadTypography } diff --git a/app/src/main/res/drawable/bg_past_card.xml b/app/src/main/res/drawable/bg_past_card.xml new file mode 100644 index 000000000..5690290c7 --- /dev/null +++ b/app/src/main/res/drawable/bg_past_card.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/bg_timeline_card.xml b/app/src/main/res/drawable/bg_timeline_card.xml new file mode 100644 index 000000000..63d63e440 --- /dev/null +++ b/app/src/main/res/drawable/bg_timeline_card.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_timeline_detail.xml b/app/src/main/res/drawable/bg_timeline_detail.xml new file mode 100644 index 000000000..5d72c87c7 --- /dev/null +++ b/app/src/main/res/drawable/bg_timeline_detail.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/btn_course_detail_more_black.xml b/app/src/main/res/drawable/btn_course_detail_more_black.xml new file mode 100644 index 000000000..9bfa70561 --- /dev/null +++ b/app/src/main/res/drawable/btn_course_detail_more_black.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/btn_course_detail_more_white.xml b/app/src/main/res/drawable/btn_course_detail_more_white.xml new file mode 100644 index 000000000..9a75002b3 --- /dev/null +++ b/app/src/main/res/drawable/btn_course_detail_more_white.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/btn_enroll_delete_picture.xml b/app/src/main/res/drawable/btn_enroll_delete_picture.xml new file mode 100644 index 000000000..2e30f3775 --- /dev/null +++ b/app/src/main/res/drawable/btn_enroll_delete_picture.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/btn_look_button_arrow.xml b/app/src/main/res/drawable/btn_look_button_arrow.xml new file mode 100644 index 000000000..dbb6b0d19 --- /dev/null +++ b/app/src/main/res/drawable/btn_look_button_arrow.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/btn_my_profile_plus.xml b/app/src/main/res/drawable/btn_my_profile_plus.xml new file mode 100644 index 000000000..f3717731a --- /dev/null +++ b/app/src/main/res/drawable/btn_my_profile_plus.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_all_activity.xml b/app/src/main/res/drawable/ic_all_activity.xml new file mode 100644 index 000000000..09471a754 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_activity.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_alcohol.xml b/app/src/main/res/drawable/ic_all_alcohol.xml new file mode 100644 index 000000000..202509f50 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_alcohol.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_camera.xml b/app/src/main/res/drawable/ic_all_camera.xml new file mode 100644 index 000000000..a7d6a1911 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_camera.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_clock_12.xml b/app/src/main/res/drawable/ic_all_clock_12.xml new file mode 100644 index 000000000..c256accc2 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_clock_12.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_clock_14.xml b/app/src/main/res/drawable/ic_all_clock_14.xml new file mode 100644 index 000000000..0d704bb10 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_clock_14.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_close.xml b/app/src/main/res/drawable/ic_all_close.xml new file mode 100644 index 000000000..74526d187 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_close.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_all_craft_shop.xml b/app/src/main/res/drawable/ic_all_craft_shop.xml new file mode 100644 index 000000000..ec0a01798 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_craft_shop.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_drive.xml b/app/src/main/res/drawable/ic_all_drive.xml new file mode 100644 index 000000000..b63b2cd4e --- /dev/null +++ b/app/src/main/res/drawable/ic_all_drive.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_epicurism.xml b/app/src/main/res/drawable/ic_all_epicurism.xml new file mode 100644 index 000000000..d248619d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_epicurism.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_exhibition_pop_up.xml b/app/src/main/res/drawable/ic_all_exhibition_pop_up.xml new file mode 100644 index 000000000..de79a2df8 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_exhibition_pop_up.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_healing.xml b/app/src/main/res/drawable/ic_all_healing.xml new file mode 100644 index 000000000..d8678bb75 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_healing.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_indoor.xml b/app/src/main/res/drawable/ic_all_indoor.xml new file mode 100644 index 000000000..938d309fc --- /dev/null +++ b/app/src/main/res/drawable/ic_all_indoor.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_location_14.xml b/app/src/main/res/drawable/ic_all_location_14.xml new file mode 100644 index 000000000..2b8e441fe --- /dev/null +++ b/app/src/main/res/drawable/ic_all_location_14.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_money_12.xml b/app/src/main/res/drawable/ic_all_money_12.xml new file mode 100644 index 000000000..f2b27824a --- /dev/null +++ b/app/src/main/res/drawable/ic_all_money_12.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_money_14.xml b/app/src/main/res/drawable/ic_all_money_14.xml new file mode 100644 index 000000000..dfe5cf4ea --- /dev/null +++ b/app/src/main/res/drawable/ic_all_money_14.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_all_nature.xml b/app/src/main/res/drawable/ic_all_nature.xml new file mode 100644 index 000000000..14eb65e51 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_nature.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_performance_music.xml b/app/src/main/res/drawable/ic_all_performance_music.xml new file mode 100644 index 000000000..aaef018de --- /dev/null +++ b/app/src/main/res/drawable/ic_all_performance_music.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_all_plus_gray_300.xml b/app/src/main/res/drawable/ic_all_plus_gray_300.xml new file mode 100644 index 000000000..aeb416268 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_plus_gray_300.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_all_plus_white.xml b/app/src/main/res/drawable/ic_all_plus_white.xml new file mode 100644 index 000000000..75e4a07fa --- /dev/null +++ b/app/src/main/res/drawable/ic_all_plus_white.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_all_reset.xml b/app/src/main/res/drawable/ic_all_reset.xml new file mode 100644 index 000000000..36080857f --- /dev/null +++ b/app/src/main/res/drawable/ic_all_reset.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_all_shopping.xml b/app/src/main/res/drawable/ic_all_shopping.xml new file mode 100644 index 000000000..66adcf64c --- /dev/null +++ b/app/src/main/res/drawable/ic_all_shopping.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_area_dropdown.xml b/app/src/main/res/drawable/ic_area_dropdown.xml new file mode 100644 index 000000000..75f202dd0 --- /dev/null +++ b/app/src/main/res/drawable/ic_area_dropdown.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_sheet_close.xml b/app/src/main/res/drawable/ic_bottom_sheet_close.xml new file mode 100644 index 000000000..74526d187 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_sheet_close.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_coures_detail_heart_default.xml b/app/src/main/res/drawable/ic_coures_detail_heart_default.xml new file mode 100644 index 000000000..6952c6dc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_coures_detail_heart_default.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_course_detail_clock.xml b/app/src/main/res/drawable/ic_course_detail_clock.xml new file mode 100644 index 000000000..c256accc2 --- /dev/null +++ b/app/src/main/res/drawable/ic_course_detail_clock.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_course_detail_heart_selected.xml b/app/src/main/res/drawable/ic_course_detail_heart_selected.xml new file mode 100644 index 000000000..0449fbf77 --- /dev/null +++ b/app/src/main/res/drawable/ic_course_detail_heart_selected.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_date_schedule_delete_course.xml b/app/src/main/res/drawable/ic_date_schedule_delete_course.xml new file mode 100644 index 000000000..b46e89d55 --- /dev/null +++ b/app/src/main/res/drawable/ic_date_schedule_delete_course.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_date_schedule_move_course.xml b/app/src/main/res/drawable/ic_date_schedule_move_course.xml new file mode 100644 index 000000000..7af09318f --- /dev/null +++ b/app/src/main/res/drawable/ic_date_schedule_move_course.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dateroad_logo.xml b/app/src/main/res/drawable/ic_dateroad_logo.xml new file mode 100644 index 000000000..7ed593ff3 --- /dev/null +++ b/app/src/main/res/drawable/ic_dateroad_logo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_enroll_calendar.xml b/app/src/main/res/drawable/ic_enroll_calendar.xml new file mode 100644 index 000000000..a36c030b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_enroll_calendar.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_enroll_dropdown.xml b/app/src/main/res/drawable/ic_enroll_dropdown.xml new file mode 100644 index 000000000..f99c924a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_enroll_dropdown.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_enroll_time.xml b/app/src/main/res/drawable/ic_enroll_time.xml new file mode 100644 index 000000000..1f014f596 --- /dev/null +++ b/app/src/main/res/drawable/ic_enroll_time.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_plus_purple.xml b/app/src/main/res/drawable/ic_home_plus_purple.xml new file mode 100644 index 000000000..4c4579e34 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_plus_purple.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_home_right_arrow_purple.xml b/app/src/main/res/drawable/ic_home_right_arrow_purple.xml new file mode 100644 index 000000000..7c707047a --- /dev/null +++ b/app/src/main/res/drawable/ic_home_right_arrow_purple.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_kakao_logo.xml b/app/src/main/res/drawable/ic_kakao_logo.xml new file mode 100644 index 000000000..2ced19aaa --- /dev/null +++ b/app/src/main/res/drawable/ic_kakao_logo.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..32fe1a6a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_my_page_arrow.xml b/app/src/main/res/drawable/ic_my_page_arrow.xml new file mode 100644 index 000000000..86dffa3f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_my_page_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_my_page_pencil.xml b/app/src/main/res/drawable/ic_my_page_pencil.xml new file mode 100644 index 000000000..87fc17310 --- /dev/null +++ b/app/src/main/res/drawable/ic_my_page_pencil.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_my_page_point_record_arrow.xml b/app/src/main/res/drawable/ic_my_page_point_record_arrow.xml new file mode 100644 index 000000000..f5c73445d --- /dev/null +++ b/app/src/main/res/drawable/ic_my_page_point_record_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_home_default.xml b/app/src/main/res/drawable/ic_nav_home_default.xml new file mode 100644 index 000000000..0e388ed51 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_home_default.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_home_selected.xml b/app/src/main/res/drawable/ic_nav_home_selected.xml new file mode 100644 index 000000000..6c0bce268 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_home_selected.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_look_default.xml b/app/src/main/res/drawable/ic_nav_look_default.xml new file mode 100644 index 000000000..74f1c2529 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_look_default.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nav_look_selected.xml b/app/src/main/res/drawable/ic_nav_look_selected.xml new file mode 100644 index 000000000..d30a6814c --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_look_selected.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nav_my_page_default.xml b/app/src/main/res/drawable/ic_nav_my_page_default.xml new file mode 100644 index 000000000..32b1caa42 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_my_page_default.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nav_my_page_selected.xml b/app/src/main/res/drawable/ic_nav_my_page_selected.xml new file mode 100644 index 000000000..903894bca --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_my_page_selected.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_nav_read_default.xml b/app/src/main/res/drawable/ic_nav_read_default.xml new file mode 100644 index 000000000..238ba521e --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_read_default.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_read_selected.xml b/app/src/main/res/drawable/ic_nav_read_selected.xml new file mode 100644 index 000000000..172869d8f --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_read_selected.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_search_default.xml b/app/src/main/res/drawable/ic_nav_search_default.xml new file mode 100644 index 000000000..57b3dde4a --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_search_default.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_search_selected.xml b/app/src/main/res/drawable/ic_nav_search_selected.xml new file mode 100644 index 000000000..700b916a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_search_selected.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_nav_timeline_default.xml b/app/src/main/res/drawable/ic_nav_timeline_default.xml new file mode 100644 index 000000000..bbaa048d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_timeline_default.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_nav_timeline_selected.xml b/app/src/main/res/drawable/ic_nav_timeline_selected.xml new file mode 100644 index 000000000..7e3d7b229 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_timeline_selected.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tag_heart.xml b/app/src/main/res/drawable/ic_tag_heart.xml new file mode 100644 index 000000000..c11a6799c --- /dev/null +++ b/app/src/main/res/drawable/ic_tag_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_top_bar_back_black.xml b/app/src/main/res/drawable/ic_top_bar_back_black.xml new file mode 100644 index 000000000..0a56aa672 --- /dev/null +++ b/app/src/main/res/drawable/ic_top_bar_back_black.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_top_bar_back_white.xml b/app/src/main/res/drawable/ic_top_bar_back_white.xml new file mode 100644 index 000000000..62f51cd9d --- /dev/null +++ b/app/src/main/res/drawable/ic_top_bar_back_white.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_top_bar_share.xml b/app/src/main/res/drawable/ic_top_bar_share.xml new file mode 100644 index 000000000..3faae6899 --- /dev/null +++ b/app/src/main/res/drawable/ic_top_bar_share.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/img_course_detail_is_not_access.png b/app/src/main/res/drawable/img_course_detail_is_not_access.png new file mode 100644 index 000000000..23e6955d9 Binary files /dev/null and b/app/src/main/res/drawable/img_course_detail_is_not_access.png differ diff --git a/app/src/main/res/drawable/img_empty_envelope.xml b/app/src/main/res/drawable/img_empty_envelope.xml new file mode 100644 index 000000000..74db910f8 --- /dev/null +++ b/app/src/main/res/drawable/img_empty_envelope.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_look.xml b/app/src/main/res/drawable/img_empty_look.xml new file mode 100644 index 000000000..f78d33055 --- /dev/null +++ b/app/src/main/res/drawable/img_empty_look.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_point_history_gained_history.xml b/app/src/main/res/drawable/img_empty_point_history_gained_history.xml new file mode 100644 index 000000000..44d3f842c --- /dev/null +++ b/app/src/main/res/drawable/img_empty_point_history_gained_history.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_point_history_used_history.xml b/app/src/main/res/drawable/img_empty_point_history_used_history.xml new file mode 100644 index 000000000..935a2f4e8 --- /dev/null +++ b/app/src/main/res/drawable/img_empty_point_history_used_history.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_read.xml b/app/src/main/res/drawable/img_empty_read.xml new file mode 100644 index 000000000..0deffab1b --- /dev/null +++ b/app/src/main/res/drawable/img_empty_read.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_empty_running.xml b/app/src/main/res/drawable/img_empty_running.xml new file mode 100644 index 000000000..97c6c8712 --- /dev/null +++ b/app/src/main/res/drawable/img_empty_running.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_enroll_profile_default.xml b/app/src/main/res/drawable/img_enroll_profile_default.xml new file mode 100644 index 000000000..a6fb5b143 --- /dev/null +++ b/app/src/main/res/drawable/img_enroll_profile_default.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_error_server.xml b/app/src/main/res/drawable/img_error_server.xml new file mode 100644 index 000000000..170f6f3ee --- /dev/null +++ b/app/src/main/res/drawable/img_error_server.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_loading_server.xml b/app/src/main/res/drawable/img_loading_server.xml new file mode 100644 index 000000000..f237b6d37 --- /dev/null +++ b/app/src/main/res/drawable/img_loading_server.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_my_page_point_info_first.png b/app/src/main/res/drawable/img_my_page_point_info_first.png new file mode 100644 index 000000000..98ff7af8e Binary files /dev/null and b/app/src/main/res/drawable/img_my_page_point_info_first.png differ diff --git a/app/src/main/res/drawable/img_my_page_point_info_fourth.png b/app/src/main/res/drawable/img_my_page_point_info_fourth.png new file mode 100644 index 000000000..05bd8b81d Binary files /dev/null and b/app/src/main/res/drawable/img_my_page_point_info_fourth.png differ diff --git a/app/src/main/res/drawable/img_my_page_point_info_second.png b/app/src/main/res/drawable/img_my_page_point_info_second.png new file mode 100644 index 000000000..a9bf9b6a9 Binary files /dev/null and b/app/src/main/res/drawable/img_my_page_point_info_second.png differ diff --git a/app/src/main/res/drawable/img_my_page_point_info_third.png b/app/src/main/res/drawable/img_my_page_point_info_third.png new file mode 100644 index 000000000..c0693d314 Binary files /dev/null and b/app/src/main/res/drawable/img_my_page_point_info_third.png differ diff --git a/app/src/main/res/drawable/img_onboarding_background1.png b/app/src/main/res/drawable/img_onboarding_background1.png new file mode 100644 index 000000000..eba8f635d Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_background1.png differ diff --git a/app/src/main/res/drawable/img_onboarding_background2.png b/app/src/main/res/drawable/img_onboarding_background2.png new file mode 100644 index 000000000..a3eefeefd Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_background2.png differ diff --git a/app/src/main/res/drawable/img_onboarding_background3.png b/app/src/main/res/drawable/img_onboarding_background3.png new file mode 100644 index 000000000..1431078d7 Binary files /dev/null and b/app/src/main/res/drawable/img_onboarding_background3.png differ diff --git a/app/src/main/res/drawable/img_profile_default.xml b/app/src/main/res/drawable/img_profile_default.xml new file mode 100644 index 000000000..af9d6e85b --- /dev/null +++ b/app/src/main/res/drawable/img_profile_default.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_profile_small.xml b/app/src/main/res/drawable/img_profile_small.xml new file mode 100644 index 000000000..6ae0f4cad --- /dev/null +++ b/app/src/main/res/drawable/img_profile_small.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_splash_logo.xml b/app/src/main/res/drawable/img_splash_logo.xml new file mode 100644 index 000000000..2c5c90016 --- /dev/null +++ b/app/src/main/res/drawable/img_splash_logo.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/img_top_bar_profile.xml b/app/src/main/res/drawable/img_top_bar_profile.xml new file mode 100644 index 000000000..f710bb596 --- /dev/null +++ b/app/src/main/res/drawable/img_top_bar_profile.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/font/suit_bold.ttf b/app/src/main/res/font/suit_bold.ttf new file mode 100644 index 000000000..a8b36aa9a Binary files /dev/null and b/app/src/main/res/font/suit_bold.ttf differ diff --git a/app/src/main/res/font/suit_extrabold.ttf b/app/src/main/res/font/suit_extrabold.ttf new file mode 100644 index 000000000..dd801c26d Binary files /dev/null and b/app/src/main/res/font/suit_extrabold.ttf differ diff --git a/app/src/main/res/font/suit_extralight.ttf b/app/src/main/res/font/suit_extralight.ttf new file mode 100644 index 000000000..c43b9a50b Binary files /dev/null and b/app/src/main/res/font/suit_extralight.ttf differ diff --git a/app/src/main/res/font/suit_medium.ttf b/app/src/main/res/font/suit_medium.ttf new file mode 100644 index 000000000..c65be500c Binary files /dev/null and b/app/src/main/res/font/suit_medium.ttf differ diff --git a/app/src/main/res/font/suit_regular.ttf b/app/src/main/res/font/suit_regular.ttf new file mode 100644 index 000000000..9360e6ce2 Binary files /dev/null and b/app/src/main/res/font/suit_regular.ttf differ diff --git a/app/src/main/res/font/suit_semibold.ttf b/app/src/main/res/font/suit_semibold.ttf new file mode 100644 index 000000000..e96d8c381 Binary files /dev/null and b/app/src/main/res/font/suit_semibold.ttf differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..1a8b87942 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..f4f100b54 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..c9464792a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..80c75aa33 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..501b79a86 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..f284e582d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..2ce84fa40 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..f8297eb8e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..a8b0a4967 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..bf001b1de Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..865bf2145 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #5347F0 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..6f92ef7c9 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,325 @@ + + 데이트로드 + + + 로그아웃 + 탈퇴 + %1$d/%2$d + 지역 + 편집 + 삭제 + 완료 + 적용하기 + %s시간 + + + 취소 + 확인 + 확인했어요 + 삭제 + 신고 + 삭제된 일정은 복구하실 수 없어요 + + + 에디터픽 + 광고 + + + 드라이브 + 쇼핑 + 실내 + 힐링 + 알콜 + 식도락 + 공방 + 자연 + 액티비티 + 공연·음악 + 전시·팝업 + + + 아직 포인트 획득 내역이 없어요! + 아직 포인트 사용 내역이 없어요! + 다른 커플들의 데이트 코스를 열람해 보세요! + 아직 등록된 코스가 없어요! + 아직 연인과의 데이트 일정을 등록하지 않으셨나요? + 지난 데이트가 없어요! + 아직 열람한 코스가 없어요! + 아직 등록한 코스가 없어요! + + + 서버 오류가 발생했어요 + 마이페이지 문의하기를 통해 문의해 주시면\n빠르게 답변해 드릴게요 + + + 경기 전체 + 성남 + 수원 + 고양/파주 + 김포 + 용인/화성 + 안양/과천 + 포천/양주 + 남양주/의정부 + 광주/이천/여주 + 가평/양평 + 군포/의왕 + 하남/구리 + 시흥/광명 + 부천/안산 + 동두천/연천 + 평택/오산/안성 + + + 인천 전체 + + + 잠시만 기다려 주세요\n로딩 중이에요 + + + + 코스 둘러보기 + 데이트 일정 + 열람한 코스 + 마이페이지 + 데이트 찾기 + + + 10만원 초과 + 10만원 이하 + 5만원 이하 + 3만원 이하 + + + 데이트 코스를 등록하면\n 포인트를 얻을 수 있어요 + 내 데이트를 자랑하고 포인트를 받아보세요 + 처음 3번 무료로\n데이트 코스를 열람할 수 있어요 + 무료 찬스로 데이트를 열람하세요 + 쌓인 포인트로\n다양한 데이트 코스를 둘러보세요 + 다른 커플의 데이트, 궁금하지 않으세요? + 모인 포인트는 데이트 장소를\n예약할 때 현금처럼 사용 가능해요 + 추후 만들어질 기능을 기대해주세요 + + + 데이트로드는 포인트로\n데이트 코스를 열람할 수 있어요 + 최초 3회 무료 찬스로\n다른 사람의 데이트 코스를 구경하세요! + (이후에는 50포인트로 코스를 열람할 수 있어요) + 데이트 코스를 등록하면\n100 포인트를 얻을 수 있어요 + 내 연인과 함께한 데이트 코스를 자랑하고\n포인트를 받아보세요 + + 쌓인 포인트로\n다양한 데이트 코스를 둘러보세요 + 모인 포인트는 데이트 장소를 예약할 때\n현금처럼 사용 가능해요 + (추후 제공될 기능이에요) + + + 데이트 일정이 등록되었어요! + + + 코스 등록이 되었어요 + 100P가 적립되었어요! + 곧 오픈될 기능이에요! + 조금만 기다려주세요 :) + 데이트를 더 이상 등록할 수 없어요! + 데이트는 최대 5개까지만 등록 가능해요 + + + 획득 내역 + 사용 내역 + + + 데이트 코스를 등록하면\n포인트를 얻을 수 있어요 + 데이트 코스를 자랑하고 포인트를 받아보세요. + 처음 3번은 무료로\n데이트 코스를 열람할 수 있어요 + 무료 찬스를 사용해 다른 데이트를 열람하세요. + 쌓인 포인트로\n다양한 데이트 코스를 둘러보세요 + 다른 커플들의 데이트, 궁금하지 않으신가요? + 모인 포인트는 데이트 장소를\n예약할 때 현금처럼 사용 가능해요 + 추후 만들어질 기능을 기대해주세요! + + + 서울 + 경기 + 인천 + + + 서울 전체 + 강남/서초 + 잠실/송파/강동 + 건대/성수/왕십리 + 종로/중구 + 홍대/합정/마포 + 영등포/여의도 + 용산/이태원/한남 + 양천/강서 + 성북/노원/중랑 + 구로/관악/동작 + + + 데이트로드에서 카카오톡을 열려고 해요 + 열기 + 로그아웃 하시겠어요? + + + 50P를 사용해서 코스를 확인해 보시겠어요? + 구매 후 포인트는 환불되지 않아요 + 코스를 열람하기에 포인트가 부족해요 + 코스를 등록하고 포인트를 모아보아요 + 코스 등록하기 + 무료 열람 기회를 사용해 보시겠어요? + 무료 열람 기회는 한 번 사용하면 취소할 수 없어요 + 데이트 일정을 삭제하시겠어요? + 데이트 코스를 삭제하시겠어요? + 데이트 코스를 신고하시겠어요? + 지난 데이트를 삭제하시겠어요? + 정말로 탈퇴하시겠어요? + 삭제된 계정은 복구하실 수 없어요 + 삭제된 코스는 복구하실 수 없어요 + 신고된 게시물은 운영원칙에 따라 조치할 예정이에요 + + + + 포인트 내역 + 코스 등록하기 + 일정 등록하기 + 내가 열람한 코스 + 내가 등록한 코스 + 코스 둘러보기 + 지난 데이트 + 데이트 일정 + 포인트 제도 소개 + 불러오기 + 마이페이지 + 지난 데이트 + + + 카카오 로그인 + 카카오톡으로 공유하기 + + + %1$d자 / %2$d자 이상 + + + %1$s\님의 포인트 + %1$d P + 포인트 내역 보기 + + + 지역을 선택해주세요 + 적용하기 + 글 삭제 + 데이트 일정 설정 + + + 내가 등록한 코스 + 포인트 제도 소개 + 문의하기 + 로그아웃 + 탈퇴하기 + + + 나의 데이트 성향 (%1$d/%2$d) + 데이트코스와 어울리는 태그를 선택해 주세요 (%1$d/%2$d) + + + %1$.1f 시간 + %1f 원 + 무료 열람 기회 쓰기 (%1$d/3) + 포인트로 코스 열람하기 + 무료 열람 기회로 코스를 확인해보세요! + 50P로 코스를 확인해보세요! + 코스 정보가 궁금하신가요? + 코스 타임라인 + 내 일정에 추가하기 + 전체 비용 + 태그 + 데이트 코스 설정 + 글 삭제 + 닫기 + 신고하기 + 무지출 + + + 다가오는 데이트 일정이 없어요 + 일정을 등록하러 가볼까요? + %1$s월 %2$s일 + D-%s + %s 시작 + + + 프로필 등록하기 + 프로필 변경하기 + + + + 프로필 생성하기 + 다음 + + + 이미지를 삽입해 주세요\n(최소 1장, 최대 10장) + 장소명을 입력해주세요 + 소요 시간 + 다음 (%d/%d) + 다음 + 데이트 이름을 입력해 주세요 (필수) + 최소 5글자 이상 입력해주세요 + 방문일자를 선택해 주세요 (필수) + 미래 날짜를 선택하셨어요 + 데이트 시작 시간을 선택해 주세요 (필수) + 데이트 지역을 선택해 주세요 (필수) + 장소명을 입력해주세요 + 소요 시간 + 어떤 코스로 이동하셨나요? + 장소와 소요시간을 입력하여 코스를 추가해 주세요 + 최소 2개의 장소를 추가해 주세요 + 코스에 대한 설명을 적어 주세요 + 데이트 내용을 입력해 주세요\n예약 정보, 웨이팅 정보, 꿀팁 등을 작성해 주세요\n(최소 200자) + 총 비용을 입력해 주세요 + 데이트 예상 총 비용을 숫자로만 입력해 주세요 + 완료 + + + 지난 데이트 보기 + + + 내 프로필 + 프로필 변경 + 닉네임 + (한글, 영문, 숫자만 가능) + 닉네임을 입력해 주세요 + 최소 2글자를 입력해야 해요 + 사용 가능한 닉네임이에요 + 이미 사용중인 닉네임이에요 + 중복확인 + 프로필 사진 설정 + 취소 + 사진 등록 + 사진 삭제 + + + 포인트 제도 소개 + 데이트로드는 포인트로\n데이트 코스를 열람할 수 있어요. + 포인트는 데이트 코스를 등록하면 얻을 수 있어요. + + + 데이트 코스 올리고 100P 받기 + + + %s님이 지금까지\n열람한 데이트 코스\n%d개 + %s님,\n아직 열람한\n데이트 코스가 없어요 + 열람한 코스로 데이트를 짜보세요 + + 더보기 + + + %s님, 오늘은\n이런 데이트 코스 어떠세요? + 후기 보장 HOT 데이트 코스 둘러보기 + 새로 올라왔어요 + 가장 최근에 올라온 코스 보러가기 + %d P + %d/%d + + + 데이트 일정 설정 + 글 삭제 + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..893e426d7 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/org/sopt/dateroad/ExampleUnitTest.kt b/app/src/test/java/org/sopt/dateroad/ExampleUnitTest.kt new file mode 100644 index 000000000..de092c029 --- /dev/null +++ b/app/src/test/java/org/sopt/dateroad/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package org.sopt.dateroad + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..69d340121 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,20 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + dependencies { + classpath(libs.hilt.android.gradle.plugin) + classpath(libs.google.services) + classpath(libs.google.firebase.crashlytics.gradle) + } +} + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.google.firebase.crashlytics) apply false + alias(libs.plugins.dagger.hilt) apply false + alias(libs.plugins.ktlint) apply false + alias(libs.plugins.sentry) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..20e2a0152 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..c77c5d0b4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,179 @@ +[versions] +compileSdk = "34" +minSdk = "28" +targetSdk = "34" +versionCode = "1" +versionName = "1.0.0" +jvmTarget = "1.8" +kotlinCompilerExtensionVersion = "1.5.13" + +# Kotlin +agp = "8.5.2" +kotlin = "1.9.23" + +# Test +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" + +# AndroidX +coreSplashscreen = "1.0.1" +coreKtx = "1.13.1" +lifecycleRuntimeKtx = "2.8.6" +activityCompose = "1.9.2" +composeBom = "2024.09.02" +navigation = "2.8.1" +security = "1.1.0-alpha06" +lifecycleRuntimeComposeAndroid = "2.8.6" +pagingCommonAndroid = "3.3.2" + +# Google +googleServices = "4.4.2" +firebaseCrashlytics = "3.0.2" +firebaseBom = "33.3.0" + +# Accompanist +accompanistPager = "0.25.0" +accompanistWebview = "0.24.13-rc" + +# Third Party +okhttp = "4.11.0" +retrofit = "2.9.0" +retrofitKotlinSerializationConverter = "1.0.0" +kotlinxSerializationJson = "1.6.3" +daggerHilt = "2.51" +hiltNavigationCompose = "1.2.0" +ktlint = "11.5.1" +coil = "2.6.0" +timber = "5.0.1" +sentry = "4.1.1" +kakao = "2.20.3" +amplitude = "1.+" +lottieCompose = "6.1.0" + +[libraries] +# Test +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } + +# Debug +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } + +# AndroidX +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +androidx-security = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security" } +androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" } +androidx-paging-common-android = { group = "androidx.paging", name = "paging-common-android", version.ref = "pagingCommonAndroid" } + +# Google +google-services = { group = "com.google.gms", name = "google-services", version.ref = "googleServices" } +google-firebase-crashlytics-gradle = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlytics" } +google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +google-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } + +# Accompanist +accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanistPager" } +accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistPager" } +accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" } + +# Third Party +okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } +okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "daggerHilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "daggerHilt" } +hilt-android-gradle-plugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "daggerHilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } +kakao-v2-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } +kakao-share = { group = "com.kakao.sdk", name = "v2-share", version.ref = "kakao" } +kakao-talk = { group = "com.kakao.sdk", name = "v2-talk", version.ref = "kakao" } +kakao-friend = { group = "com.kakao.sdk", name = "v2-friend", version.ref = "kakao" } +kakao-navi = { group = "com.kakao.sdk", name = "v2-navi", version.ref = "kakao" } +kakao-cert = { group = "com.kakao.sdk", name = "v2-cert", version.ref = "kakao" } +amplitude = { group = "com.amplitude", name = "analytics-android", version.ref = "amplitude" } +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" } + + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +sentry = { id = "io.sentry.android.gradle", version.ref = "sentry" } + +[bundles] +androidx = [ + "androidx-core-splashscreen", + "androidx-core-ktx", + "androidx-lifecycle-runtime-ktx", + "androidx-activity-compose", + "androidx-ui", + "androidx-ui-graphics", + "androidx-ui-tooling-preview", + "androidx-material3", + "androidx-navigation", + "androidx-security", + "androidx-lifecycle-runtime-compose-android" +] + +test = [ + "androidx-junit", + "androidx-espresso-core", + "androidx-ui-test-junit4" +] + +debug = [ + "androidx-ui-tooling", + "androidx-ui-test-manifest" +] + +okhttp = [ + "okhttp", + "okhttp-logging-interceptor" +] + +retrofit = [ + "retrofit", + "retrofit-kotlin-serialization-converter" +] + +hilt = [ + "hilt", + "hilt-navigation-compose" +] + +kakao = [ + "kakao-v2-user", + "kakao-share", + "kakao-talk", + "kakao-friend", + "kakao-navi", + "kakao-cert" +] + +pager = [ + "androidx-paging-common-android", + "accompanist-pager", + "accompanist-pager-indicators" +] \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c5a251753 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jun 20 23:01:25 KST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..72a10a42d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + + // KakaoSDK repository + maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") + } +} + +rootProject.name = "DATEROAD" +include(":app")