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
-우리랑 더미데이트 하러 갈래? 🤍
+# 쉽고 빠른 데이트로 가는 지름길, 데이트로드 👩🏻❤️👨🏻
+
+
+데이트로드는 ‘장소 중심’이 아닌 ‘코스 중심’ 데이트 공유 서비스로 사용자가 직접 데이트 코스를 등록하고 공유합니다.
+
+
+
+## 💟 Contributors
+
+| [🐸배지현 Lead](https://github.com/jihyunniiii) | [신민석](https://github.com/t1nm1ksun) | [이현진](https://github.com/2hyunjinn) |
+|:-------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------:|
+|
|
|
|
+| `포인트내역`
`내가열람한코스+내가등록한코스`
`코스둘러보기`
`코스등록하기+일정등록하기`
`코스수정하기` | `온보딩/회원가입`
`스플래시`
`프로필등록하기`
`코스상세`
`마이페이지`
`웹뷰`
`관리자아카이빙` | `메인페이지`
`다가올 데이트일정`
`지난데이트일정`
`다가올데이트 상세페이지`
`지난데이트 상세페이지`
+
+
+---
+## 📷 **시연영상**
+|  |  |  |
+|:------------------------:|:--------------------------:|:----------------------:|
+| `온보딩/회원가입` | `메인스크린` | `코스상세+일정등록하기` |
+
+|  |  |
+|:------------------------:|:--------------------------:|
+| `데이트 일정` | `마이페이지` |
+
+
+
+## 👋 커밋 컨벤션
+[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회는 무료로 데이트 코스를 열람할 수 있습니다. 해당 코스대로 데이트를 떠나고 싶다면 내 일정에 추가하기 버튼을 눌러 내 데이트 일정으로 등록할 수도 있습니다.
+
+## 📝 문제상황 정의
+
+---
+
+
+
+
+- 기존 앱은 코스가 아닌 장소 중심, 이로 인해 데이트 코스를 찾기 위해 여러 앱을 쓰며 피로감을 느낌
+- 광고가 아닌 직접 방문한 사람의 후기를 기반으로 데이트 코스를 짜고 싶어 하는 니즈 존재
+
+## 🎯 핵심 타겟
+
+---
+
+- 센스 있게 데이트 코스를 짜고 싶은 여자/남자친구
+- 색다른 데이트 코스를 찾기 위해 인스타그램 등을 탐색하는 커플
+- 네이버 블로그, 인스타그램을 통해 여러 번 데이트 장소의 후기를 얻는 커플
+
+## 📍 주요 기능
+
+---
+
+### 1️⃣ 코스 등록하기 및 열람
+
+
+
+
+
+
+- 내가 한 데이트 코스를 등록하고 포인트를 획득할 수 있습니다.
+- 다른 커플들이 한 데이트를 포인트를 사용해 열람할 수 있습니다.
+- 코스 상세 페이지에서 ‘내 일정에 추가하기’ 버튼을 눌러 내 데이트 일정으로 불러올 수 있습니다.
+
+### 2️⃣ 일정 등록하기 및 열람
+
+
+
+
+
+- 내 데이트 일정을 등록할 수 있습니다.
+- 내 데이트 일정을 확인할 수 있습니다.
+- 지난 데이트는 코스 등록하기로 연동해 등록하고 포인트를 받을 수 있습니다.
+- 카카오톡 공유하기를 통해 데이트 일정을 연인에게 공유할 수 있습니다.
+
+## 💰 비즈니스 모델
+
+---
+
+> **포인트를 통한 수익 모델**
+>
+- 유저들은 데이트 코스를 등록하고 포인트를 획득해 제휴 매장에 할인받아 방문합니다.
+- 구글 애드센스를 연결하여 광고를 시청하면 포인트를 획득할 수 있습니다. 데이트로드는 광고 수익을 얻을 수 있습니다.
+
+> **입점처를 통한 수익 모델**
+>
+- 입점 가게는 매장을 홍보하고 유저 방문으로 매출을 증가시키며, 광고주는 유저에게 광고를 노출하여 제품이나 서비스를 홍보합니다. 데이트로드는 이를 통해 수익을 창출하고, 모든 참여자가 상호 이익을 얻는 생태계를 구축합니다.
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")