diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..7717b74a7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma=true +ij_kotlin_allow_trailing_comma_on_call_site=true +ktlint_disabled_rules = import-ordering diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..74519ded4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,32 @@ +name: Bug report +description: File a bug report +title: '[QA]: ' +labels: ['๐Ÿ”ŽQA๐Ÿ”Ž'] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! ๐Ÿ™ + - type: textarea + id: what-happened + attributes: + label: ์–ด๋–ค ์ผ์ด ๋ฐœ์ƒํ–ˆ๋‚˜์š”? ๐Ÿค” + description: ๋˜ํ•œ, ์–ด๋–ค ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋Œ€ํ–ˆ์—ˆ๋Š”์ง€ ์•Œ๋ ค์ฃผ์„ธ์š”. + placeholder: ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค... + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: ๊ด€๋ จ๋œ ์Šคํฌ๋ฆฐ ์ƒท์ด๋‚˜, ๋ฒ„๊ทธ ๋ฐœ์ƒ ์กฐ๊ฑด์„ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š” + description: ๋น ๋ฅด๊ฒŒ ์ดํ•ดํ• ์ˆ˜๋ก ๋น ๋ฅธ ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•ด์š” ! + placeholder: ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ 10๋ฒˆ ํด๋ฆญ์‹œ ํฌ๋ž˜์‰ฌ๊ฐ€ ๋ฐœ์ƒ + - type: checkboxes + id: terms + attributes: + label: ๋™์˜ ๐Ÿ‘ + description: + options: + - label: ๋‹ค๋ฅธ ์ด์Šˆ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. โœ… + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3d508255d..b6431641f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,9 @@ ## 1. ๐Ÿ“„ ๊ด€๋ จ๋œ ์ด์Šˆ ๋ฐ ์†Œ๊ฐœ -## 2. ๐Ÿ”ฅ๋ณ€๊ฒฝ๋œ ์  +## 2. ๐Ÿ”ฅ ๋ณ€๊ฒฝ๋œ ์  -## 3. ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท(์„ ํƒ) +## 3. โœ… ๊ผญ ํ™•์ธํ•ด์คฌ์œผ๋ฉด ํ•˜๋Š” ๋ถ€๋ถ„ -## 4. ๐Ÿ’ก์•Œ๊ฒŒ๋œ ํ˜น์€ ๊ถ๊ธˆํ•œ ์‚ฌํ•ญ๋“ค +## 4. ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท(์„ ํƒ) + +## 5. ๐Ÿ’ก์•Œ๊ฒŒ๋œ ํ˜น์€ ๊ถ๊ธˆํ•œ ์‚ฌํ•ญ๋“ค diff --git a/.github/workflows/opened-pr-notification.yml b/.github/workflows/opened-pr-notification.yml index 3addfa014..1cded89dd 100644 --- a/.github/workflows/opened-pr-notification.yml +++ b/.github/workflows/opened-pr-notification.yml @@ -21,6 +21,9 @@ jobs: distribution: zulu cache: gradle + - name: add google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json + - name: add local.properties run: | echo api_key=\"${{ secrets.API_KEY }}\" >> ./local.properties diff --git a/.github/workflows/released-app-distribution.yml b/.github/workflows/released-app-distribution.yml new file mode 100644 index 000000000..65b485b48 --- /dev/null +++ b/.github/workflows/released-app-distribution.yml @@ -0,0 +1,111 @@ +name: Released-App-Distribution +on: + push: + branches: + - 'release' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: zulu + cache: gradle + + - name: add google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json + + - name: add local.properties + run: | + echo api_key=\"${{ secrets.API_KEY }}\" >> ./local.properties + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: Setup build tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + + - name: Sign APK + id: sign_app + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.KEY_BASE_64_RELEASE }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} + + - name: Authenticate to Firebase + uses: google-github-actions/auth@v1 + with: + credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + + - name: Setup Firebase CLI + run: curl -sL https://firebase.tools | bash + + - name: upload artifact to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 + with: + appId: ${{secrets.FIREBASE_APP_ID}} + serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + groups: WAPP_QA + file: ${{steps.sign_app.outputs.signedReleaseFile}} + + - name: Send Success Message + if: ${{ success() }} + uses: Ilshidur/action-discord@0.3.2 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + DISCORD_USERNAME: WAPP_BOT + DISCORD_AVATAR: https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true + DISCORD_EMBEDS: | + [ + { + "author": { + "name": "WAPP Release", + "url": "https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true", + "icon_url": "https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true" + }, + "title": "๋ฆด๋ฆฌ์ฆˆ ์„ฑ๊ณต, ์ž ๋‘ ๊ณผ ์ž ~ ๐Ÿ”ฅ๐Ÿ”ฅ", + "color": 10478271, + "description": "๋ฉ”์ผ์— ์ƒˆ๋กœ์šด ๋ฆด๋ฆฌ์ฆˆ ์•ฑ ๋ฐฐ์†ก์™„๋ฃŒํ–ˆ์–ด์š”!" + } + ] + + - name: Send Failure Message + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.3.2 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + DISCORD_USERNAME: WAPP_BOT + DISCORD_AVATAR: https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true + DISCORD_EMBEDS: | + [ + { + "author": { + "name": "WAPP Release", + "url": "https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true", + "icon_url": "https://github.com/pknu-wap/WAPP/blob/main/image/icon.png?raw=true" + }, + "title": "๋ฆด๋ฆฌ์ฆˆ ์‹คํŒจ, ๋ˆ„๊ฐ€ ์ด๋ ‡๊ฒŒ ํ•˜๋ž˜. ๐Ÿ˜ญ๐Ÿ˜ญ", + "color": 13458524, + "description": "๋‹ค์‹œ ๋ฆด๋ฆฌ์ฆˆ ํ•ด์˜ค์„ธ์š”. ์‚๋น…" + } + ] diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..70efd0777 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +google-services.json \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..b589d56e9 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 000000000..2f0367c47 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..fc333ade3 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,53 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..103e00cbe --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 000000000..f8467b458 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 000000000..f8051a6f9 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..8978d23db --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index db8f360bd..436908272 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ์™€ํ”ผ - WAP Official App +# ์™€ํ”ผ - WAP ์ผ์ •ํ™•์ธ ๋ฐ ์„ค๋ฌธ์กฐ์‚ฌ ํ”Œ๋žซํผ ``` WAP ํ–‰์‚ฌ์ผ์ •์„ ์‰ฝ๊ฒŒ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! ํ•จ๊ป˜ํ•ด์š” ์™€ํ”ผ @@ -8,36 +8,148 @@ WAP ํ–‰์‚ฌ์ผ์ •์„ ์‰ฝ๊ฒŒ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! ํ•จ๊ป˜ํ•ด์š” ์™€ํ”ผ

-### ๊ธฐ๋Šฅ ์†Œ๊ฐœ +## ๐ŸŒฑ Feature Introduce + +#### ์ธํŠธ๋กœ ํ™”๋ฉด + + + +


#### ๊ณต์ง€์‚ฌํ•ญ - WAP ์ •๊ทœ ํ™œ๋™ ๋ฐ ํ–‰์‚ฌ๋ฅผ ๋‹ฌ๋ ฅ๊ณผ ๋ชฉ๋ก์„ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”. -- ์ง€์ •๋œ ํ–‰์‚ฌ ๋‚ ์งœ์— ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ํ†ตํ•ด ๋ฆฌ๋งˆ์ธ๋“œ ํ•ด๋“œ๋ ค์š”. + +
+ + + +


#### ์ถœ์„ - ํ–‰์‚ฌ๋งˆ๋‹ค ์ถœ์„์„ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์–ด์š”. - ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ถœ๊ฒฐ์ƒํ™ฉ์„ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์–ด์š”. +
+ + + +


+ #### ์„ค๋ฌธ - ํ–‰์‚ฌ๋งˆ๋‹ค ์„ค๋ฌธ ๋ฐ ํ”ผ๋“œ๋ฐฑ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š” - ์„ค๋ฌธ๊ณผ ํ”ผ๋“œ๋ฐฑ์„ ํ†ตํ•ด, ๋” ์ข‹์€ ํ–‰์‚ฌ๋กœ ๋ฐœ์ „ํ•  ์ˆ˜ ์žˆ์–ด์š”. -## ๐ŸŽ Contributors ๐ŸŒ +
+ + + +


+ +#### ์šด์˜์ง„ + +- ํšŒ์›๋“ค์˜ ์„ค๋ฌธ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”. +- ๊ณต์ง€์‚ฌํ•ญ ๋ฐ ์„ค๋ฌธ์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์–ด์š”. +- ์ถœ์„์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์–ด์š” + +
+ + + +


+ +#### ๊ทธ ์™ธ + +- ํ”„๋กœํ•„ +- ํ”„๋กœํ•„ ๋” ๋ณด๊ธฐ + +
+ + + +


+ +## ๐Ÿ—๏ธ Module Dependency Graph +

+ +

+ +
+ +## ๐Ÿšฉ Android Tech Stack +

+
+

+

+

+ Version Catalog +

+

+ +
+## ๐ŸŽ Contributors ๐ŸŒ - - + + - - + +

JinHo Jeong

๐Ÿ’ป

Tgyuu An

๐Ÿ’ป

JinHo Jeong

๐Ÿ’ป

Tgyuu An

๐Ÿ’ป
์•ˆ๋“œ๋กœ์ด๋“œ ์•ˆ๋“œ๋กœ์ด๋“œ
๋กœ๊ทธ์ธ, ์ถœ์„์ฒดํฌ, ๋งˆ์ดํŽ˜์ด์ง€๊ณต์ง€์‚ฌํ•ญ, ๋‹ฌ๋ ฅ, ์„ค๋ฌธ์กฐ์‚ฌ๋กœ๊ทธ์ธ, ์šด์˜์ง„ํŽ˜์ด์ง€, ์„ค๋ฌธ, ์ผ์ •๊ณต์ง€์‚ฌํ•ญ, ๋‹ฌ๋ ฅ, ํ”„๋กœํ•„, ์ถœ์„, ์• ๋‹ˆ๋ฉ”์ด์…˜
+

+ ํŒ€์› ์†Œ๊ฐœ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ +

+
+ +## ๐Ÿš€ Trouble Shooting +``` +ํ”„๋กœ์ ํŠธ ์ค‘ ๋ฐœ์ƒํ•œ ์ด์Šˆ์— ๋Œ€ํ•ด ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…์„ ๊ธฐ๋กํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค. +``` +[WAPP ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…](https://discovered-trust-803.notion.site/WAPP-238f82deeac44721a3321665573c9f76?pvs=4) + +
+ +## ๐Ÿƒโ€โ™‚๏ธ Sprint +``` +๋งค์ฃผ ์ˆ˜์š”์ผ, ์Šคํ”„๋ฆฐํŠธ์— ํ• ๋‹นํ•  ์ด์Šˆ๋ฅผ ์ง€๋ผ์™€ ๊นƒํ—™์— ๋“ฑ๋กํ•œ๋‹ค. ์Šคํ”„๋ฆฐํŠธ ๋‹จ์œ„๋Š” ์ผ์ฃผ์ผ์ด๋ฉฐ, ๊ฐœ๋ฐœ ์ผ์ •์„ ๋”ฐ๋ฅธ๋‹ค. +๋ชฉ์š”์ผ ์˜คํ›„ 11์‹œ 30๋ถ„๊นŒ์ง€ ๋ชป ๋๋‚ธ ์ด์Šˆ ํ•˜๋‚˜๋‹น ์Šคํƒ ํ•˜๋‚˜๋กœ ๊ฐ„์ฃผํ•˜๋ฉฐ, ์Šคํƒ ์„ธ ๊ฐœ๊ฐ€ ๋ชจ์˜€์„ ๋•Œ ๋ฐฅ ํ•œ ๋ผ๋ฅผ ์‚ฌ์•ผ ํ•œ๋‹ค. +``` +[์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก](https://www.notion.so/79134caa75394435a221a15c53226726?v=c3640a0dae5f4bac8ecf51d61aae4acf) + +
+ +## ๐ŸŽจ UI/UX + +![image](https://github.com/pknu-wap/WAPP/assets/116813010/ccb60d3b-ec63-41dc-8eca-d0a8b735c1fb) + +[ํ”ผ๊ทธ๋งˆ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ](https://www.figma.com/file/ldfJcNruLXpb7e41P7LK3O/WAPP?type=design&node-id=0%3A1&mode=design&t=Q2vI9pGnu1OcsFWP-1) + +
+ +## ๐Ÿ’ป Code Convention + +[WAPP ์•ˆ๋“œ๋กœ์ด๋“œ ์ฝ”๋“œ ์ปจ๋ฒค์…˜](https://github.com/pknu-wap/WAPP/wiki/%F0%9F%A6%92%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BB%A8%EB%B2%A4%EC%85%98%F0%9F%A6%92) + +
+ +## โ›“๏ธ Git Convention & Git Flow ์ „๋žต +[WAPP ๊นƒ ์ปจ๋ฒค์…˜](https://github.com/pknu-wap/WAPP/wiki/%F0%9F%90%B1%EA%B9%83-%EC%BB%A8%EB%B2%A4%EC%85%98%F0%9F%90%B1) +``` kotlin +1. Issue๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. +2. feature Branch๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. +3. Add - Commit - Push - Pull Request ์˜ ๊ณผ์ •์„ ๊ฑฐ์นœ๋‹ค. +4. Pull Request๊ฐ€ ์ž‘์„ฑ๋˜๋ฉด ์ž‘์„ฑ์ž ์ด์™ธ์˜ ๋‹ค๋ฅธ ํŒ€์›์ด Code Review๋ฅผ ํ•œ๋‹ค. +5. Code Review๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด Pull Request ์ž‘์„ฑ์ž๊ฐ€ develop Branch๋กœ merge ํ•œ๋‹ค. +6. merge๋œ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ, ๋‹ค๋ฅธ ๋ธŒ๋žœ์น˜์—์„œ ์ž‘์—…์„ ์ง„ํ–‰ ์ค‘์ด๋˜ ๊ฐœ๋ฐœ์ž๋Š” ๋ณธ์ธ์˜ ๋ธŒ๋žœ์น˜๋กœ merge๋œ ์ž‘์—…์„ Pull ๋ฐ›์•„์˜จ๋‹ค. +7. ์ข…๋ฃŒ๋œ Issue์™€ Pull Request์˜ Label๊ณผ Project๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค. +``` +
diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..b46299f7e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +/google-services.json \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..7488122a0 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + id("com.wap.wapp.application") + id("com.wap.wapp.firebase") + id("com.wap.wapp.compose") + id("com.wap.wapp.hilt") + id("com.wap.wapp.navigation") +} + +android { + namespace = "com.wap.wapp" + + defaultConfig { + applicationId = "com.wap.wapp" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation(project(":feature:auth")) + implementation(project(":feature:notice")) + implementation(project(":feature:survey")) + implementation(project(":feature:survey-check")) + implementation(project(":feature:profile")) + implementation(project(":feature:attendance")) + implementation(project(":feature:management")) + implementation(project(":feature:management-survey")) + implementation(project(":feature:management-event")) + implementation(project(":feature:splash")) + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} + +// tasks.getByPath(":app:preBuild").dependsOn("installGitHook") +// +// tasks.register("installGitHook") { +// dependsOn("deletePreviousGitHook") +// from("${rootProject.rootDir}/script/pre-commit") +// into("${rootProject.rootDir}/.git/hooks") +// eachFile { +// fileMode = 777 +// } +// } +// +// tasks.register("deletePreviousGitHook") { +// +// val prePush = "${rootProject.rootDir}/.git/hooks/pre-commit" +// if (file(prePush).exists()) { +// delete(prePush) +// } +// } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/wap/wapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wap/wapp/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..2845a4bc7 --- /dev/null +++ b/app/src/androidTest/java/com/wap/wapp/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp + +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("com.wap.wapp", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4ded0df9c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..b6a52692f Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/wap/wapp/MainActivity.kt b/app/src/main/java/com/wap/wapp/MainActivity.kt new file mode 100644 index 000000000..04fcb5230 --- /dev/null +++ b/app/src/main/java/com/wap/wapp/MainActivity.kt @@ -0,0 +1,136 @@ +package com.wap.wapp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Scaffold +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.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.wap.designsystem.WappTheme +import com.wap.wapp.component.WappBottomBar +import com.wap.wapp.core.domain.usecase.auth.SignInUseCase +import com.wap.wapp.feature.attendance.management.navigation.attendanceManagementNavigationRoute +import com.wap.wapp.feature.auth.signin.navigation.signInNavigationRoute +import com.wap.wapp.feature.auth.signup.navigation.signUpNavigationRoute +import com.wap.wapp.feature.management.event.navigation.eventEditNavigationRoute +import com.wap.wapp.feature.management.event.navigation.eventRegistrationNavigationRoute +import com.wap.wapp.feature.management.survey.navigation.ManagementSurveyRoute +import com.wap.wapp.feature.profile.profilesetting.navigation.profileSettingNavigationRoute +import com.wap.wapp.feature.splash.navigation.splashNavigationRoute +import com.wap.wapp.feature.survey.check.navigation.SurveyCheckRoute +import com.wap.wapp.feature.survey.check.navigation.SurveyCheckRoute.surveyCheckRoute +import com.wap.wapp.feature.survey.navigation.SurveyRoute +import com.wap.wapp.navigation.TopLevelDestination +import com.wap.wapp.navigation.WappNavHost +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + @Inject + lateinit var signInUseCase: SignInUseCase + + override fun onCreate(savedInstanceState: Bundle?) { + setSystemBarStyle() + super.onCreate(savedInstanceState) + setContent { + WappTheme { + val navController = rememberNavController() + + Scaffold( + containerColor = WappTheme.colors.backgroundBlack, + bottomBar = { + val navBackStackEntry by + navController.currentBackStackEntryAsState() + + val currentRoute = navBackStackEntry?.destination?.route + var bottomBarState by rememberSaveable { mutableStateOf(false) } + + handleBottomBarState( + currentRoute, + setBottomBarState = { boolean -> + bottomBarState = boolean + }, + ) + + WappBottomBar( + currentRoute = currentRoute, + bottomBarState = bottomBarState, + onNavigateToDestination = { destination -> + navigateToTopLevelDestination( + navController, + destination, + ) + }, + modifier = Modifier.height(70.dp), + ) + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .fillMaxSize(), + ) { innerPadding -> + WappNavHost( + signInUseCase = signInUseCase, + navController = navController, + modifier = Modifier.padding(innerPadding), + ) + } + } + } + } +} + +private fun ComponentActivity.setSystemBarStyle() = enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light(getColor(R.color.yellow34), getColor(R.color.yellow34)), + navigationBarStyle = SystemBarStyle.light(getColor(R.color.black25), getColor(R.color.black25)), +) + +private fun handleBottomBarState( + currentRoute: String?, + setBottomBarState: (Boolean) -> Unit, +): Unit = when (currentRoute) { + null -> setBottomBarState(false) + signInNavigationRoute -> setBottomBarState(false) + signUpNavigationRoute -> setBottomBarState(false) + splashNavigationRoute -> setBottomBarState(false) + profileSettingNavigationRoute -> setBottomBarState(false) + attendanceManagementNavigationRoute -> setBottomBarState(false) + ManagementSurveyRoute.surveyFormRegistrationRoute -> setBottomBarState(false) + ManagementSurveyRoute.surveyFormEditRoute("{id}") -> setBottomBarState(false) + eventRegistrationNavigationRoute -> setBottomBarState(false) + eventEditNavigationRoute -> setBottomBarState(false) + SurveyRoute.answerRoute("{id}") -> setBottomBarState(false) + surveyCheckRoute -> setBottomBarState(false) + SurveyCheckRoute.surveyDetailRoute("{id}", "{backStack}") -> setBottomBarState(false) + else -> setBottomBarState(true) +} + +private fun navigateToTopLevelDestination( + navController: NavController, + destination: TopLevelDestination, +) { + navController.navigate(route = destination.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } +} diff --git a/app/src/main/java/com/wap/wapp/WappApplication.kt b/app/src/main/java/com/wap/wapp/WappApplication.kt new file mode 100644 index 000000000..c99cbd413 --- /dev/null +++ b/app/src/main/java/com/wap/wapp/WappApplication.kt @@ -0,0 +1,7 @@ +package com.wap.wapp + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class WappApplication : Application() diff --git a/app/src/main/java/com/wap/wapp/component/WappBottomBar.kt b/app/src/main/java/com/wap/wapp/component/WappBottomBar.kt new file mode 100644 index 000000000..c5606df4a --- /dev/null +++ b/app/src/main/java/com/wap/wapp/component/WappBottomBar.kt @@ -0,0 +1,72 @@ +package com.wap.wapp.component + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.with +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.navigation.TopLevelDestination + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun WappBottomBar( + modifier: Modifier = Modifier, + currentRoute: String?, + onNavigateToDestination: (TopLevelDestination) -> Unit, + bottomBarState: Boolean, +) { + AnimatedContent( + targetState = bottomBarState, + label = "", + transitionSpec = { + slideInVertically { height -> height } with + slideOutVertically { height -> height } + }, + ) { isVisible -> + if (isVisible) { + BottomNavigation( + backgroundColor = WappTheme.colors.black25, + modifier = modifier, + ) { + TopLevelDestination.entries.forEach { destination -> + val isSelect = currentRoute == destination.route + BottomNavigationItem( + selected = isSelect, + onClick = { onNavigateToDestination(destination) }, + selectedContentColor = WappTheme.colors.yellow34, + unselectedContentColor = WappTheme.colors.grayA2, + icon = { + Icon( + painter = painterResource(id = destination.iconDrawableId), + contentDescription = null, + ) + }, + label = { + val labelColor = if (isSelect) { + WappTheme.colors.yellow34 + } else { + WappTheme.colors.grayA2 + } + + Text( + text = stringResource(id = destination.labelTextId), + style = WappTheme.typography.labelMedium.copy(fontSize = 10.sp), + color = labelColor, + ) + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/wap/wapp/navigation/TopLevelDestination.kt b/app/src/main/java/com/wap/wapp/navigation/TopLevelDestination.kt new file mode 100644 index 000000000..fa4761848 --- /dev/null +++ b/app/src/main/java/com/wap/wapp/navigation/TopLevelDestination.kt @@ -0,0 +1,43 @@ +package com.wap.wapp.navigation + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.wap.wapp.R +import com.wap.wapp.core.designresource.R.string +import com.wap.wapp.feature.attendance.navigation.attendanceNavigationRoute +import com.wap.wapp.feature.management.navigation.managementNavigationRoute +import com.wap.wapp.feature.notice.navigation.noticeNavigationRoute +import com.wap.wapp.feature.profile.navigation.profileNavigationRoute +import com.wap.wapp.feature.survey.navigation.SurveyRoute + +enum class TopLevelDestination( + val route: String, + @DrawableRes val iconDrawableId: Int, + @StringRes val labelTextId: Int, +) { + NOTICE( + route = noticeNavigationRoute, + iconDrawableId = R.drawable.ic_notice, + labelTextId = string.notice, + ), + SURVEY( + route = SurveyRoute.route, + iconDrawableId = R.drawable.ic_survey, + labelTextId = string.survey, + ), + ATTENDANCE( + route = attendanceNavigationRoute, + iconDrawableId = com.wap.wapp.core.designresource.R.drawable.ic_check, + labelTextId = com.wap.wapp.feature.attendance.R.string.attendance, + ), + PROFILE( + route = profileNavigationRoute, + iconDrawableId = R.drawable.ic_profile, + labelTextId = string.profile, + ), + MANAGEMENT( + route = managementNavigationRoute, + iconDrawableId = R.drawable.ic_management, + labelTextId = string.management, + ), +} diff --git a/app/src/main/java/com/wap/wapp/navigation/WappNavHost.kt b/app/src/main/java/com/wap/wapp/navigation/WappNavHost.kt new file mode 100644 index 000000000..e2da0bd70 --- /dev/null +++ b/app/src/main/java/com/wap/wapp/navigation/WappNavHost.kt @@ -0,0 +1,159 @@ +package com.wap.wapp.navigation + +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.navOptions +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.domain.usecase.auth.SignInUseCase +import com.wap.wapp.feature.attendance.management.navigation.attendanceManagementScreen +import com.wap.wapp.feature.attendance.management.navigation.navigateToAttendanceManagement +import com.wap.wapp.feature.attendance.navigation.attendanceNavigationRoute +import com.wap.wapp.feature.attendance.navigation.attendanceScreen +import com.wap.wapp.feature.attendance.navigation.navigateToAttendance +import com.wap.wapp.feature.auth.signin.navigation.navigateToSignIn +import com.wap.wapp.feature.auth.signin.navigation.signInNavigationRoute +import com.wap.wapp.feature.auth.signin.navigation.signInScreen +import com.wap.wapp.feature.auth.signup.navigation.navigateToSignUp +import com.wap.wapp.feature.auth.signup.navigation.signUpScreen +import com.wap.wapp.feature.management.event.navigation.managementEventNavGraph +import com.wap.wapp.feature.management.event.navigation.navigateToEventEdit +import com.wap.wapp.feature.management.event.navigation.navigateToEventRegistration +import com.wap.wapp.feature.management.navigation.managementScreen +import com.wap.wapp.feature.management.navigation.navigateToManagement +import com.wap.wapp.feature.management.survey.navigation.managementSurveyNavGraph +import com.wap.wapp.feature.management.survey.navigation.navigateToSurveyFormEdit +import com.wap.wapp.feature.management.survey.navigation.navigateToSurveyFormRegistration +import com.wap.wapp.feature.notice.navigation.navigateToNotice +import com.wap.wapp.feature.notice.navigation.noticeScreen +import com.wap.wapp.feature.profile.navigation.navigateToProfile +import com.wap.wapp.feature.profile.navigation.profileNavigationRoute +import com.wap.wapp.feature.profile.navigation.profileScreen +import com.wap.wapp.feature.profile.profilesetting.navigation.navigateToProfileSetting +import com.wap.wapp.feature.profile.profilesetting.navigation.profileSettingNavigationRoute +import com.wap.wapp.feature.profile.profilesetting.navigation.profileSettingScreen +import com.wap.wapp.feature.splash.navigation.splashNavigationRoute +import com.wap.wapp.feature.splash.navigation.splashScreen +import com.wap.wapp.feature.survey.check.navigation.SurveyCheckRoute.surveyCheckRoute +import com.wap.wapp.feature.survey.check.navigation.SurveyDetailBackStack +import com.wap.wapp.feature.survey.check.navigation.navigateToSurveyCheck +import com.wap.wapp.feature.survey.check.navigation.navigateToSurveyDetail +import com.wap.wapp.feature.survey.check.navigation.surveyCheckNavGraph +import com.wap.wapp.feature.survey.navigation.navigateToSurvey +import com.wap.wapp.feature.survey.navigation.navigateToSurveyAnswer +import com.wap.wapp.feature.survey.navigation.surveyNavGraph + +@Composable +fun WappNavHost( + navController: NavHostController, + modifier: Modifier = Modifier, + signInUseCase: SignInUseCase, + startDestination: String = splashNavigationRoute, +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier.background(WappTheme.colors.black25), + ) { + splashScreen( + navigateToAuth = { + navController.navigateToSignIn( + navOptions { + popUpTo(splashNavigationRoute) { inclusive = true } + }, + ) + }, + navigateToNotice = { + navController.navigateToNotice( + navOptions { popUpTo(splashNavigationRoute) { inclusive = true } }, + ) + }, + ) + signInScreen( + signInUseCase = signInUseCase, + navigateToNotice = navController::navigateToNotice, + navigateToSignUp = navController::navigateToSignUp, + ) + signUpScreen( + navigateToNotice = { + navController.navigateToNotice( + navOptions { popUpTo(navController.graph.id) { inclusive = true } }, + ) + }, + navigateToSignIn = navController::navigateToSignIn, + ) + noticeScreen() + surveyNavGraph( + navigateToSurvey = navController::navigateToSurvey, + navigateToSurveyAnswer = navController::navigateToSurveyAnswer, + navigateToSignIn = navController::navigateToSignIn, + navigateToSurveyCheck = navController::navigateToSurveyCheck, + ) + surveyCheckNavGraph( + navigateToSurveyCheck = { + navController.navigateToSurveyCheck( + navOptions { popUpTo(surveyCheckRoute) { inclusive = true } }, + ) + }, + navigateToSurveyDetail = navController::navigateToSurveyDetail, + navigateToSurvey = { + navController.navigateToSurvey( + navOptions { popUpTo(surveyCheckRoute) { inclusive = true } }, + ) + }, + navigateToProfile = navController::navigateToProfile, + ) + managementSurveyNavGraph( + navigateToManagement = navController::navigateToManagement, + ) + managementEventNavGraph( + navigateToManagement = navController::navigateToManagement, + ) + profileScreen( + navigateToProfileSetting = navController::navigateToProfileSetting, + navigateToAttendance = navController::navigateToAttendance, + navigateToSignIn = { + navController.navigateToSignIn(navOptions { popUpTo(profileNavigationRoute) }) + }, + navigateToSurveyDetail = { surveyId -> + navController.navigateToSurveyDetail( + surveyId = surveyId, + backStack = SurveyDetailBackStack.PROFILE, + navOptions = navOptions { popUpTo(profileNavigationRoute) }, + ) + }, + ) + attendanceScreen( + navigateToSignIn = { + navController.navigateToSignIn(navOptions { popUpTo(attendanceNavigationRoute) }) + }, + navigateToAttendanceManagement = navController::navigateToAttendanceManagement, + ) + attendanceManagementScreen(navigateToAttendance = navController::navigateToAttendance) + profileSettingScreen( + navigateToSignIn = { + navController.navigateToSignIn( + navOptions { + popUpTo(signInNavigationRoute) { inclusive = true } + }, + ) + }, + navigateToProfile = { + navController.navigateToProfile( + navOptions { + popUpTo(profileSettingNavigationRoute) { inclusive = true } + }, + ) + }, + ) + managementScreen( + navigateToSurveyRegistration = navController::navigateToSurveyFormRegistration, + navigateToEventRegistration = navController::navigateToEventRegistration, + navigateToEventEdit = navController::navigateToEventEdit, + navigateToSurveyFormEdit = navController::navigateToSurveyFormEdit, + navigateToSignIn = navController::navigateToSignIn, + ) + } +} 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..ca3826a46 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..2b068d114 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_management.xml b/app/src/main/res/drawable/ic_management.xml new file mode 100644 index 000000000..c7f63fc14 --- /dev/null +++ b/app/src/main/res/drawable/ic_management.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notice.xml b/app/src/main/res/drawable/ic_notice.xml new file mode 100644 index 000000000..e9e90a937 --- /dev/null +++ b/app/src/main/res/drawable/ic_notice.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 000000000..b75592523 --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_survey.xml b/app/src/main/res/drawable/ic_survey.xml new file mode 100644 index 000000000..c879550e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_survey.xml @@ -0,0 +1,9 @@ + + + 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..c4a603d4c --- /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..c4a603d4c --- /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..e060f1c4e 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_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..14899e84c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.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..8e82cb0d2 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..a34418d04 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_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..a3c069e7f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.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..244936489 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..f6cc81192 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_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..7d36d0f35 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.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..03f2913d5 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..e053305a8 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_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..d238d0ccc Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.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..9edf632f9 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..b71ca9092 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_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..ea4e5e26b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.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..79f87fe72 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-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..34844f3a2 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..667fddc68 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF000000 + #FFFFFFFF + #FFFBCF34 + #FF252424 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..2a9197597 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + WAPP + \ 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..0ed774e88 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,15 @@ + + + + + + + + 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/com/wap/wapp/ExampleUnitTest.kt b/app/src/test/java/com/wap/wapp/ExampleUnitTest.kt new file mode 100644 index 000000000..c1daaaa99 --- /dev/null +++ b/app/src/test/java/com/wap/wapp/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp + +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-logic/convention/.gitignore b/build-logic/convention/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/build-logic/convention/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 000000000..1234dd2b2 --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,52 @@ +plugins{ + `kotlin-dsl` +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of("17")) + } +} + +dependencies { + compileOnly(libs.android.build) + compileOnly(libs.kotlin.gradle) +} + +gradlePlugin { + plugins { + create("androidApplication") { + id = "com.wap.wapp.application" + implementationClass = "com.wap.wapp.plugin.AndroidApplicationPlugin" + } + create("androidLibrary") { + id = "com.wap.wapp.library" + implementationClass = "com.wap.wapp.plugin.AndroidLibraryPlugin" + } + create("androidFirebase") { + id = "com.wap.wapp.firebase" + implementationClass = "com.wap.wapp.plugin.AndroidApplicationFirebasePlugin" + } + create("androidFeatureConvention") { + id = "com.wap.wapp.feature" + implementationClass = "com.wap.wapp.plugin.AndroidFeatureConventionPlugin" + } + create("androidCompose") { + id = "com.wap.wapp.compose" + implementationClass = "com.wap.wapp.plugin.AndroidComposePlugin" + } + create("androidHilt") { + id = "com.wap.wapp.hilt" + implementationClass = "com.wap.wapp.plugin.AndroidHiltPlugin" + } + create("androidNavigation") { + id = "com.wap.wapp.navigation" + implementationClass = "com.wap.wapp.plugin.AndroidNavigationPlugin" + } + } +} diff --git a/build-logic/convention/consumer-rules.pro b/build-logic/convention/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/build-logic/convention/proguard-rules.pro b/build-logic/convention/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/build-logic/convention/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationFirebasePlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationFirebasePlugin.kt new file mode 100644 index 000000000..9da01a7ef --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationFirebasePlugin.kt @@ -0,0 +1,26 @@ +package com.wap.wapp.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class AndroidApplicationFirebasePlugin: Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager){ + apply("com.google.gms.google-services") + apply("com.google.firebase.crashlytics") + } + + val libs = extensions.getByType().named("libs") + + dependencies { + "implementation"(platform(libs.findLibrary("firebase-bom").get())) + "implementation"(libs.findLibrary("firebase-analytics").get()) + "implementation"(libs.findLibrary("firebase-crashlytics").get()) + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationPlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationPlugin.kt new file mode 100644 index 000000000..334271feb --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidApplicationPlugin.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.plugin + +import com.wap.wapp.plugin.configure.configureApplicationVersion +import com.wap.wapp.plugin.configure.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidApplicationPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + } + configureKotlinAndroid() + configureApplicationVersion() + } + } +} diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidComposePlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidComposePlugin.kt new file mode 100644 index 000000000..a5b62798b --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidComposePlugin.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.plugin + +import com.wap.wapp.plugin.configure.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.dependencies + +class AndroidComposePlugin: Plugin { + override fun apply(target: Project) { + with(target){ + val libs = extensions.getByType().named("libs") + + dependencies{ + "implementation"(libs.findBundle("compose").get()) + "debugImplementation"(libs.findLibrary("compose-ui-tooling").get()) + } + configureAndroidCompose() + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidFeatureConventionPlugin.kt new file mode 100644 index 000000000..f0343fc69 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidFeatureConventionPlugin.kt @@ -0,0 +1,17 @@ +package com.wap.wapp.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidFeatureConventionPlugin: Plugin{ + override fun apply(target: Project) { + with(target){ + with(pluginManager){ + apply("com.wap.wapp.library") + apply("com.wap.wapp.compose") + apply("com.wap.wapp.hilt") + apply("com.wap.wapp.navigation") + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidHiltPlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidHiltPlugin.kt new file mode 100644 index 000000000..39aab5d6b --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidHiltPlugin.kt @@ -0,0 +1,23 @@ +package com.wap.wapp.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class AndroidHiltPlugin: Plugin { + override fun apply(target: Project) { + with(target){ + pluginManager.apply("com.google.dagger.hilt.android") + pluginManager.apply("com.google.devtools.ksp") + + val libs = extensions.getByType().named("libs") + + dependencies { + "implementation"(libs.findLibrary("hilt").get()) + "ksp"(libs.findLibrary("hilt.ksp").get()) + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidLibraryPlugin.kt new file mode 100644 index 000000000..812979f86 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidLibraryPlugin.kt @@ -0,0 +1,17 @@ +package com.wap.wapp.plugin + +import com.wap.wapp.plugin.configure.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidLibraryPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + configureKotlinAndroid() + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidNavigationPlugin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidNavigationPlugin.kt new file mode 100644 index 000000000..e201c6e19 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/AndroidNavigationPlugin.kt @@ -0,0 +1,25 @@ +package com.wap.wapp.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class AndroidNavigationPlugin: Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("androidx.navigation.safeargs") + } + + val libs = extensions.getByType().named("libs") + + dependencies{ + "implementation"(libs.findLibrary("androidx-navigation-fragment-ktx").get()) + "implementation"(libs.findLibrary("androidx-navigation-ui-ktx").get()) + "implementation"(libs.findLibrary("androidx-navigation-compose").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidCompose.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidCompose.kt new file mode 100644 index 000000000..69d5c4912 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidCompose.kt @@ -0,0 +1,17 @@ +package com.wap.wapp.plugin.configure + +import com.android.build.gradle.BaseExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +internal fun Project.configureAndroidCompose(){ + val libs = extensions.getByType().named("libs") + + extensions.getByType().apply { + buildFeatures.compose = true + + composeOptions.kotlinCompilerExtensionVersion = + libs.findVersion("compose-compiler").get().toString() + } +} diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidKotlin.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidKotlin.kt new file mode 100644 index 000000000..718f088c5 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidKotlin.kt @@ -0,0 +1,32 @@ +package com.wap.wapp.plugin.configure + +import com.android.build.gradle.BaseExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.plugins.ExtensionAware +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions + +internal fun Project.configureKotlinAndroid(){ + val libs = extensions.getByType().named("libs") + + extensions.getByType().apply { + setCompileSdkVersion(libs.findVersion("compileSdk").get().requiredVersion.toInt()) + + defaultConfig { + minSdk = libs.findVersion("minSdk").get().requiredVersion.toInt() + targetSdk = libs.findVersion("targetSdk").get().requiredVersion.toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + (this as ExtensionAware).configure { + jvmTarget = "17" + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidVersion.kt b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidVersion.kt new file mode 100644 index 000000000..5f83d1cf3 --- /dev/null +++ b/build-logic/convention/src/main/java/com/wap/wapp/plugin/configure/AndroidVersion.kt @@ -0,0 +1,17 @@ +package com.wap.wapp.plugin.configure + +import com.android.build.gradle.BaseExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +internal fun Project.configureApplicationVersion() { + val libs = extensions.getByType().named("libs") + + extensions.getByType().apply { + defaultConfig { + versionCode = libs.findVersion("versionCode").get().requiredVersion.toInt() + versionName = libs.findVersion("versionName").get().requiredVersion + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 000000000..2907fbfb8 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,14 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..87f258520 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,41 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath(libs.android.build) + classpath(libs.kotlin.gradle) + classpath(libs.androidx.navigation.safeargs) + classpath(libs.hilt.gradle) + classpath(libs.google.services.gradle) + classpath(libs.firebase.crashlytics.gradle) + } +} + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.androidx.navigation.safeargs) apply false + alias(libs.plugins.dagger.hilt) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.firebase.crashlytics) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.ktlint) +} + +allprojects { + apply { + plugin(rootProject.libs.plugins.ktlint.get().pluginId) + } + + ktlint { + android.set(true) + verbose.set(true) + outputToConsole.set(true) + reporters { + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN) + } + } +} diff --git a/core/build/intermediates/ktLint/reporterProviders.bin b/core/build/intermediates/ktLint/reporterProviders.bin new file mode 100644 index 000000000..11e8bd367 Binary files /dev/null and b/core/build/intermediates/ktLint/reporterProviders.bin differ diff --git a/core/build/intermediates/ktLint/reporters.bin b/core/build/intermediates/ktLint/reporters.bin new file mode 100644 index 000000000..d5a0d6164 Binary files /dev/null and b/core/build/intermediates/ktLint/reporters.bin differ diff --git a/core/common/.gitignore b/core/common/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 000000000..b0380e5f1 --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.wap.wapp.feature") +} + +android { + namespace = "com.wap.wapp.core.base" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(libs.bundles.androidx) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.auth) + implementation(libs.firebase.firestore) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/core/common/consumer-rules.pro b/core/common/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/common/proguard-rules.pro b/core/common/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/common/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/common/src/androidTest/java/com/wap/wapp/core/base/ExampleInstrumentedTest.kt b/core/common/src/androidTest/java/com/wap/wapp/core/base/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..358e2475e --- /dev/null +++ b/core/common/src/androidTest/java/com/wap/wapp/core/base/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.base + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.core.base.test", appContext.packageName) + } +} diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/common/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ContextExtensions.kt b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ContextExtensions.kt new file mode 100644 index 000000000..e2c81e41f --- /dev/null +++ b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ContextExtensions.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.commmon.extensions + +import android.app.Activity +import android.widget.Toast + +fun Activity.showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun Activity.showLongToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() +} diff --git a/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/CoroutinesExtension.kt b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/CoroutinesExtension.kt new file mode 100644 index 000000000..0d79ada7f --- /dev/null +++ b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/CoroutinesExtension.kt @@ -0,0 +1,14 @@ +package com.wap.wapp.core.commmon.extensions + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) + } +} diff --git a/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ThrowableExtensions.kt b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ThrowableExtensions.kt new file mode 100644 index 000000000..be1606f21 --- /dev/null +++ b/core/common/src/main/java/com/wap/wapp/core/commmon/extensions/ThrowableExtensions.kt @@ -0,0 +1,37 @@ +package com.wap.wapp.core.commmon.extensions + +import com.google.firebase.auth.FirebaseAuthException +import com.google.firebase.firestore.FirebaseFirestoreException +import java.net.UnknownHostException + +fun Throwable.toSupportingText(): String { + return when (this) { + is UnknownHostException -> "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์ด ์›ํ™œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + + is FirebaseAuthException -> this.toSupportingText() + + is FirebaseFirestoreException -> this.toSupportingText() + + is IllegalStateException -> this.message.toString() + + else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค." + } +} + +fun FirebaseAuthException.toSupportingText(): String { + return when (this.errorCode) { + "ERROR_WEB_CONTEXT_CANCELED", "ERROR_USER_CANCELLED" -> "๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”." + + else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค." + } +} + +fun FirebaseFirestoreException.toSupportingText(): String { + return when (this.code.value()) { + 7 -> "์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค." + + 16 -> "ํšŒ์›์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธ ํ•ด์ฃผ์„ธ์š”." + + else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค." + } +} diff --git a/core/common/src/main/java/com/wap/wapp/core/commmon/util/DateUtil.kt b/core/common/src/main/java/com/wap/wapp/core/commmon/util/DateUtil.kt new file mode 100644 index 000000000..0860bf954 --- /dev/null +++ b/core/common/src/main/java/com/wap/wapp/core/commmon/util/DateUtil.kt @@ -0,0 +1,44 @@ +package com.wap.wapp.core.commmon.util + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +object DateUtil { + + fun generateNowTime(zoneId: ZoneId = ZoneId.of("Asia/Seoul")): LocalTime = LocalTime.now(zoneId) + + fun generateNowDate(zoneId: ZoneId = ZoneId.of("Asia/Seoul")): LocalDate = LocalDate.now(zoneId) + + fun generateNowDateTime(zoneId: ZoneId = ZoneId.of("Asia/Seoul")): LocalDateTime = + LocalDateTime.now(zoneId) + + const val YEAR_MONTH_START_INDEX = 0 + const val YEAR_MONTH_END_INDEX = 7 + const val MONTH_DATE_START_INDEX = 5 + const val DAYS_IN_WEEK = 7 + + // ํ˜„์žฌ ๋‚ ์งœ์—์„œ ์‹œ๊ฐ„, ๋ถ„๋งŒ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ํฌ๋งท ex 19:00 + val HHmmFormatter = DateTimeFormatter.ofPattern("HH:mm") + + // ํ˜„์žฌ ๋‚ ์งœ์—์„œ ์ผ๋งŒ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ํฌ๋งท ex 2023-11-20 -> 20 + val ddFormatter = DateTimeFormatter.ofPattern("dd") + + // ํ˜„์žฌ ๋‚ ์งœ๋ฅผ ๋…„-์›”-์ผ ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ํฌ๋งท. ex 2023-11-20 + val yyyyMMddFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + + // ํ˜„์žฌ ๋‚ ์งœ๋ฅผ ์›”-์ผ ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ํฌ๋งท. ex 11์›” 20์ผ + val MMddFormatter = DateTimeFormatter.ofPattern("MM์›” dd์ผ") + + enum class DaysOfWeek(val displayName: String) { + SUNDAY("์ผ"), + MONDAY("์›”"), + TUESDAY("ํ™”"), + WEDNESDAY("์ˆ˜"), + THURSDAY("๋ชฉ"), + FRIDAY("๊ธˆ"), + SATURDAY("ํ† "), + } +} diff --git a/core/common/src/test/java/com/wap/wapp/core/base/ExampleUnitTest.kt b/core/common/src/test/java/com/wap/wapp/core/base/ExampleUnitTest.kt new file mode 100644 index 000000000..4e26d482f --- /dev/null +++ b/core/common/src/test/java/com/wap/wapp/core/base/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.base + +import org.junit.Assert.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/core/data/.gitignore b/core/data/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 000000000..36e452b4e --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("com.wap.wapp.library") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.core.data" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:model")) + implementation(project(":core:network")) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/core/data/consumer-rules.pro b/core/data/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/data/proguard-rules.pro b/core/data/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/data/src/androidTest/java/com/wap/wapp/core/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/wap/wapp/core/data/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..3b7ac9386 --- /dev/null +++ b/core/data/src/androidTest/java/com/wap/wapp/core/data/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.data + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.core.data.test", appContext.packageName) + } +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/di/DataModule.kt b/core/data/src/main/java/com/wap/wapp/core/data/di/DataModule.kt new file mode 100644 index 000000000..cdd681ece --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/di/DataModule.kt @@ -0,0 +1,75 @@ +package com.wap.wapp.core.data.di + +import com.wap.wapp.core.data.repository.attendance.AttendanceRepository +import com.wap.wapp.core.data.repository.attendance.AttendanceRepositoryImpl +import com.wap.wapp.core.data.repository.attendancestatus.AttendanceStatusRepository +import com.wap.wapp.core.data.repository.attendancestatus.AttendanceStatusRepositoryImpl +import com.wap.wapp.core.data.repository.auth.AuthRepository +import com.wap.wapp.core.data.repository.auth.AuthRepositoryImpl +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.data.repository.event.EventRepositoryImpl +import com.wap.wapp.core.data.repository.management.ManagementRepository +import com.wap.wapp.core.data.repository.management.ManagementRepositoryImpl +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import com.wap.wapp.core.data.repository.survey.SurveyFormRepositoryImpl +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.data.repository.survey.SurveyRepositoryImpl +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.data.repository.user.UserRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataModule { + @Binds + @Singleton + abstract fun bindsAuthRepository( + authRepositoryImpl: AuthRepositoryImpl, + ): AuthRepository + + @Binds + @Singleton + abstract fun bindsUserRepository( + userRepositoryImpl: UserRepositoryImpl, + ): UserRepository + + @Binds + @Singleton + abstract fun bindsManagementRepository( + managementRepositoryImpl: ManagementRepositoryImpl, + ): ManagementRepository + + @Binds + @Singleton + abstract fun bindsSurveyRepository( + surveyRepositoryImpl: SurveyRepositoryImpl, + ): SurveyRepository + + @Binds + @Singleton + abstract fun bindsSurveyFormRepository( + surveyFormRepositoryImpl: SurveyFormRepositoryImpl, + ): SurveyFormRepository + + @Binds + @Singleton + abstract fun bindsEventRepository( + eventRepositoryImpl: EventRepositoryImpl, + ): EventRepository + + @Binds + @Singleton + abstract fun bindsAttendanceRepository( + attendanceRepositoryImpl: AttendanceRepositoryImpl, + ): AttendanceRepository + + @Binds + @Singleton + abstract fun bindsAttendanceStatusRepository( + attendanceStatusRepositoryImpl: AttendanceStatusRepositoryImpl, + ): AttendanceStatusRepository +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/di/SignInRepositoryModule.kt b/core/data/src/main/java/com/wap/wapp/core/data/di/SignInRepositoryModule.kt new file mode 100644 index 000000000..ae47ce820 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/di/SignInRepositoryModule.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.data.di + +import com.wap.wapp.core.data.repository.auth.SignInRepository +import com.wap.wapp.core.data.repository.auth.SignInRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +abstract class SignInRepositoryModule { + @Binds + @ActivityScoped + abstract fun bindsAuthRepository( + signInRepositoryImpl: SignInRepositoryImpl, + ): SignInRepository +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepository.kt new file mode 100644 index 000000000..4fdcb48b6 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepository.kt @@ -0,0 +1,14 @@ +package com.wap.wapp.core.data.repository.attendance + +import com.wap.wapp.core.model.attendance.Attendance +import java.time.LocalDateTime + +interface AttendanceRepository { + suspend fun getAttendance(eventId: String): Result + + suspend fun postAttendance( + eventId: String, + code: String, + deadline: LocalDateTime, + ): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepositoryImpl.kt new file mode 100644 index 000000000..c087b4d9c --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendance/AttendanceRepositoryImpl.kt @@ -0,0 +1,27 @@ +package com.wap.wapp.core.data.repository.attendance + +import com.wap.wapp.core.data.utils.toISOLocalDateTimeString +import com.wap.wapp.core.model.attendance.Attendance +import com.wap.wapp.core.network.source.attendance.AttendanceDataSource +import java.time.LocalDateTime +import javax.inject.Inject + +class AttendanceRepositoryImpl @Inject constructor( + private val attendanceDataSource: AttendanceDataSource, +) : AttendanceRepository { + override suspend fun getAttendance(eventId: String): Result = + attendanceDataSource.getAttendance(eventId).mapCatching { attendanceResponse -> + attendanceResponse.toDomain() + } + + override suspend fun postAttendance( + eventId: String, + code: String, + deadline: LocalDateTime, + ): Result = + attendanceDataSource.postAttendance( + eventId = eventId, + code = code, + deadline = deadline.toISOLocalDateTimeString(), + ) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepository.kt new file mode 100644 index 000000000..1c48c6fe2 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepository.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.data.repository.attendancestatus + +import com.wap.wapp.core.model.attendancestatus.AttendanceStatus + +interface AttendanceStatusRepository { + suspend fun getAttendanceStatus(eventId: String, userId: String): Result + + suspend fun postAttendance( + eventId: String, + userId: String, + ): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepositoryImpl.kt new file mode 100644 index 000000000..4050ca10c --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/attendancestatus/AttendanceStatusRepositoryImpl.kt @@ -0,0 +1,20 @@ +package com.wap.wapp.core.data.repository.attendancestatus + +import com.wap.wapp.core.model.attendancestatus.AttendanceStatus +import com.wap.wapp.core.network.source.attendancestatus.AttendanceStatusDataSource +import javax.inject.Inject + +class AttendanceStatusRepositoryImpl @Inject constructor( + private val attendanceStatusDataSource: AttendanceStatusDataSource, +) : AttendanceStatusRepository { + override suspend fun getAttendanceStatus( + eventId: String, + userId: String, + ): Result = attendanceStatusDataSource.getAttendanceStatus( + eventId = eventId, + userId = userId, + ).mapCatching { it.toDomain() } + + override suspend fun postAttendance(eventId: String, userId: String): Result = + attendanceStatusDataSource.postAttendanceStatus(eventId = eventId, userId = userId) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepository.kt new file mode 100644 index 000000000..ac988f856 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepository.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.data.repository.auth + +interface AuthRepository { + suspend fun signOut(): Result + + suspend fun deleteUser(): Result + + suspend fun isUserSignIn(): Result + + suspend fun checkMemberCode(code: String): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepositoryImpl.kt new file mode 100644 index 000000000..d79a21bab --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/AuthRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.wap.wapp.core.data.repository.auth + +import com.wap.wapp.core.network.source.auth.AuthDataSource +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor( + private val authDataSource: AuthDataSource, +) : AuthRepository { + override suspend fun signOut(): Result = authDataSource.signOut() + + override suspend fun deleteUser(): Result = authDataSource.deleteUser() + + override suspend fun isUserSignIn(): Result = authDataSource.isUserSignIn() + + override suspend fun checkMemberCode(code: String): Result = + authDataSource.checkMemberCode(code) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepository.kt new file mode 100644 index 000000000..fc4418bca --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepository.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.data.repository.auth + +interface SignInRepository { + suspend fun signIn(email: String): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepositoryImpl.kt new file mode 100644 index 000000000..aa3157116 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/auth/SignInRepositoryImpl.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.data.repository.auth + +import com.wap.wapp.core.network.source.auth.SignInDataSource +import javax.inject.Inject + +class SignInRepositoryImpl @Inject constructor( + private val signInDataSource: SignInDataSource, +) : SignInRepository { + override suspend fun signIn(email: String): Result = signInDataSource.signIn(email) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepository.kt new file mode 100644 index 000000000..0ac05a60b --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepository.kt @@ -0,0 +1,36 @@ +package com.wap.wapp.core.data.repository.event + +import com.wap.wapp.core.model.event.Event +import java.time.LocalDate +import java.time.LocalDateTime + +interface EventRepository { + suspend fun getMonthEventList(date: LocalDate): Result> + + suspend fun getDateEventList(date: LocalDate): Result> + + suspend fun getEventListFromDate(date: LocalDate): Result> + + suspend fun getEventList(): Result> + + suspend fun getEvent(eventId: String): Result + + suspend fun deleteEvent(eventId: String): Result + + suspend fun postEvent( + title: String, + content: String, + location: String, + startDateTime: LocalDateTime, + endDateTime: LocalDateTime, + ): Result + + suspend fun updateEvent( + eventId: String, + title: String, + content: String, + location: String, + startDateTime: LocalDateTime, + endDateTime: LocalDateTime, + ): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepositoryImpl.kt new file mode 100644 index 000000000..0c05b2513 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/event/EventRepositoryImpl.kt @@ -0,0 +1,78 @@ +package com.wap.wapp.core.data.repository.event + +import com.wap.wapp.core.data.utils.toISOLocalDateTimeString +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.network.source.event.EventDataSource +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class EventRepositoryImpl @Inject constructor( + private val eventDataSource: EventDataSource, +) : EventRepository { + override suspend fun getMonthEventList(date: LocalDate): Result> = + eventDataSource.getMonthEventList(date).mapCatching { eventResponses -> + eventResponses.map { eventResponse -> + eventResponse.toDomain() + }.sortedBy { it.startDateTime } + } + + override suspend fun getEventList(): Result> = + eventDataSource.getEventList().mapCatching { eventResponses -> + eventResponses.map { eventResponse -> + eventResponse.toDomain() + }.sortedBy { it.startDateTime } + } + + override suspend fun getDateEventList(date: LocalDate): Result> = + eventDataSource.getDateEventList(date).mapCatching { eventResponses -> + eventResponses.map { eventResponse -> + eventResponse.toDomain() + }.sortedBy { it.startDateTime } + } + + override suspend fun getEventListFromDate(date: LocalDate): Result> = + eventDataSource.getEventListFromDate(date).mapCatching { eventResponses -> + eventResponses.map { eventResponse -> + eventResponse.toDomain() + }.sortedBy { it.startDateTime } + } + + override suspend fun getEvent(eventId: String): Result = + eventDataSource.getEvent(eventId).mapCatching { eventResponse -> eventResponse.toDomain() } + + override suspend fun deleteEvent(eventId: String): Result = + eventDataSource.deleteEvent(eventId) + + override suspend fun postEvent( + title: String, + content: String, + location: String, + startDateTime: LocalDateTime, + endDateTime: LocalDateTime, + ): Result = + eventDataSource.postEvent( + title = title, + content = content, + location = location, + startDateTime = startDateTime.toISOLocalDateTimeString(), + endDateTime = endDateTime.toISOLocalDateTimeString(), + ) + + override suspend fun updateEvent( + eventId: String, + title: String, + content: String, + location: String, + startDateTime: LocalDateTime, + endDateTime: LocalDateTime, + ): Result = + eventDataSource.updateEvent( + eventId = eventId, + title = title, + content = content, + location = location, + startDateTime = startDateTime.toISOLocalDateTimeString(), + endDateTime = endDateTime.toISOLocalDateTimeString(), + ) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepository.kt new file mode 100644 index 000000000..8c6141546 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepository.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.data.repository.management + +interface ManagementRepository { + suspend fun isManager(userId: String): Result + + suspend fun postManager(userId: String): Result + + suspend fun checkManagementCode(code: String): Result + + suspend fun deleteManager(userId: String): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepositoryImpl.kt new file mode 100644 index 000000000..440ae1566 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/management/ManagementRepositoryImpl.kt @@ -0,0 +1,20 @@ +package com.wap.wapp.core.data.repository.management + +import com.wap.wapp.core.network.source.management.ManagementDataSource +import javax.inject.Inject + +class ManagementRepositoryImpl @Inject constructor( + private val managementDataSource: ManagementDataSource, +) : ManagementRepository { + override suspend fun isManager(userId: String): Result = + managementDataSource.isManager(userId) + + override suspend fun postManager(userId: String): Result = + managementDataSource.postManager(userId) + + override suspend fun checkManagementCode(code: String): Result = + managementDataSource.checkManagementCode(code) + + override suspend fun deleteManager(userId: String): Result = + managementDataSource.deleteManager(userId) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepository.kt new file mode 100644 index 000000000..44bd85301 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepository.kt @@ -0,0 +1,25 @@ +package com.wap.wapp.core.data.repository.survey + +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.model.survey.SurveyQuestion +import java.time.LocalDateTime + +interface SurveyFormRepository { + suspend fun getSurveyForm(surveyFormId: String): Result + + suspend fun getSurveyFormList(): Result> + + suspend fun getSurveyFormListByEventId(eventId: String): Result> + + suspend fun deleteSurveyForm(surveyFormId: String): Result + + suspend fun postSurveyForm( + eventId: String, + title: String, + content: String, + surveyQuestionList: List, + deadline: LocalDateTime, + ): Result + + suspend fun updateSurveyForm(surveyForm: SurveyForm): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepositoryImpl.kt new file mode 100644 index 000000000..0f11583a6 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyFormRepositoryImpl.kt @@ -0,0 +1,62 @@ +package com.wap.wapp.core.data.repository.survey + +import com.wap.wapp.core.data.utils.toISOLocalDateTimeString +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.core.network.model.survey.form.SurveyFormRequest +import com.wap.wapp.core.network.source.survey.SurveyFormDataSource +import java.time.LocalDateTime +import javax.inject.Inject + +class SurveyFormRepositoryImpl @Inject constructor( + private val surveyFormDataSource: SurveyFormDataSource, +) : SurveyFormRepository { + override suspend fun getSurveyForm(surveyFormId: String): Result = + surveyFormDataSource.getSurveyForm(surveyFormId).mapCatching { surveyFormResponse -> + surveyFormResponse.toDomain() + } + + override suspend fun getSurveyFormList(): Result> = + surveyFormDataSource.getSurveyFormList().mapCatching { surveyFormResponseList -> + surveyFormResponseList.map { surveyFormResponse -> + surveyFormResponse.toDomain() + } + } + + override suspend fun deleteSurveyForm(surveyFormId: String): Result = + surveyFormDataSource.deleteSurveyForm(surveyFormId) + + override suspend fun getSurveyFormListByEventId(eventId: String): Result> = + surveyFormDataSource.getSurveyFormListByEventId(eventId) + .mapCatching { surveyFormResponseList -> + surveyFormResponseList.map { surveyFormResponse -> + surveyFormResponse.toDomain() + } + } + + override suspend fun postSurveyForm( + eventId: String, + title: String, + content: String, + surveyQuestionList: List, + deadline: LocalDateTime, + ): Result = surveyFormDataSource.postSurveyForm( + eventId = eventId, + title = title, + content = content, + surveyQuestionList = surveyQuestionList, + deadline = deadline.toISOLocalDateTimeString(), + ) + + override suspend fun updateSurveyForm(surveyForm: SurveyForm): Result = + surveyFormDataSource.updateSurveyForm( + surveyFormRequest = SurveyFormRequest( + surveyFormId = surveyForm.surveyFormId, + eventId = surveyForm.eventId, + title = surveyForm.title, + content = surveyForm.content, + surveyQuestionList = surveyForm.surveyQuestionList, + deadline = surveyForm.deadline.toISOLocalDateTimeString(), + ), + ) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepository.kt new file mode 100644 index 000000000..b48a6f944 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepository.kt @@ -0,0 +1,31 @@ +package com.wap.wapp.core.data.repository.survey + +import com.wap.wapp.core.model.survey.Survey +import com.wap.wapp.core.model.survey.SurveyAnswer +import java.time.LocalDateTime + +interface SurveyRepository { + suspend fun getSurveyList(): Result> + + suspend fun getSurveyListByEventId(eventId: String): Result> + + suspend fun getSurveyListBySurveyFormId(surveyFormId: String): Result> + + suspend fun getUserRespondedSurveyList(userId: String): Result> + + suspend fun getSurvey(surveyId: String): Result + + suspend fun deleteSurvey(surveyId: String): Result + + suspend fun postSurvey( + surveyFormId: String, + eventId: String, + userId: String, + title: String, + content: String, + surveyAnswerList: List, + surveyedAt: LocalDateTime, + ): Result + + suspend fun isSubmittedSurvey(surveyFormId: String, userId: String): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepositoryImpl.kt new file mode 100644 index 000000000..562ca57de --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/survey/SurveyRepositoryImpl.kt @@ -0,0 +1,123 @@ +package com.wap.wapp.core.data.repository.survey + +import com.wap.wapp.core.data.utils.toISOLocalDateTimeString +import com.wap.wapp.core.model.survey.Survey +import com.wap.wapp.core.model.survey.SurveyAnswer +import com.wap.wapp.core.network.source.event.EventDataSource +import com.wap.wapp.core.network.source.survey.SurveyDataSource +import com.wap.wapp.core.network.source.user.UserDataSource +import java.time.LocalDateTime +import javax.inject.Inject + +class SurveyRepositoryImpl @Inject constructor( + private val surveyDataSource: SurveyDataSource, + private val userDataSource: UserDataSource, + private val eventDataSource: EventDataSource, +) : SurveyRepository { + override suspend fun getSurveyList(): Result> = + surveyDataSource.getSurveyList().mapCatching { surveyList -> + surveyList.map { surveyResponse -> + userDataSource.getUserProfile(userId = surveyResponse.userId) + .mapCatching { userProfileResponse -> + val userName = userProfileResponse.toDomain().userName + + eventDataSource.getEvent(eventId = surveyResponse.eventId) + .mapCatching { eventResponse -> + val eventName = eventResponse.toDomain().title + + surveyResponse.toDomain(userName = userName, eventName = eventName) + }.getOrThrow() + }.getOrThrow() + } + } + + override suspend fun getSurveyListByEventId(eventId: String): Result> = + surveyDataSource.getSurveyListByEventId(eventId).mapCatching { surveyList -> + surveyList.map { surveyResponse -> + userDataSource.getUserProfile(userId = surveyResponse.userId) + .mapCatching { userProfileResponse -> + val userName = userProfileResponse.toDomain().userName + + eventDataSource.getEvent(eventId = eventId) + .mapCatching { eventResponse -> + val eventName = eventResponse.toDomain().title + + surveyResponse.toDomain(userName = userName, eventName = eventName) + }.getOrThrow() + }.getOrThrow() + } + } + + override suspend fun getSurveyListBySurveyFormId(surveyFormId: String): Result> = + surveyDataSource.getSurveyListByEventId(surveyFormId).mapCatching { surveyList -> + surveyList.map { surveyResponse -> + userDataSource.getUserProfile(userId = surveyResponse.userId) + .mapCatching { userProfileResponse -> + val userName = userProfileResponse.toDomain().userName + + eventDataSource.getEvent(eventId = surveyResponse.eventId) + .mapCatching { eventResponse -> + val eventName = eventResponse.toDomain().title + + surveyResponse.toDomain(userName = userName, eventName = eventName) + }.getOrThrow() + }.getOrThrow() + } + } + + override suspend fun getUserRespondedSurveyList(userId: String): Result> = + surveyDataSource.getUserRespondedSurveyList(userId).mapCatching { surveyList -> + surveyList.map { surveyResponse -> + userDataSource.getUserProfile(userId = surveyResponse.userId) + .mapCatching { userProfileResponse -> + val userName = userProfileResponse.toDomain().userName + + eventDataSource.getEvent(eventId = surveyResponse.eventId) + .mapCatching { eventResponse -> + val eventName = eventResponse.toDomain().title + + surveyResponse.toDomain(userName = userName, eventName = eventName) + }.getOrThrow() + }.getOrThrow() + } + } + + override suspend fun getSurvey(surveyId: String): Result = + surveyDataSource.getSurvey(surveyId).mapCatching { surveyResponse -> + userDataSource.getUserProfile(userId = surveyResponse.userId) + .mapCatching { userProfileResponse -> + val userName = userProfileResponse.toDomain().userName + + eventDataSource.getEvent(eventId = surveyResponse.eventId) + .mapCatching { eventResponse -> + val eventName = eventResponse.toDomain().title + + surveyResponse.toDomain(userName = userName, eventName = eventName) + }.getOrThrow() + }.getOrThrow() + } + + override suspend fun deleteSurvey(surveyId: String): Result = + surveyDataSource.deleteSurvey(surveyId) + + override suspend fun postSurvey( + surveyFormId: String, + eventId: String, + userId: String, + title: String, + content: String, + surveyAnswerList: List, + surveyedAt: LocalDateTime, + ): Result = surveyDataSource.postSurvey( + surveyFormId = surveyFormId, + eventId = eventId, + userId = userId, + title = title, + content = content, + surveyAnswerList = surveyAnswerList, + surveyedAt = surveyedAt.toISOLocalDateTimeString(), + ) + + override suspend fun isSubmittedSurvey(surveyFormId: String, userId: String): Result = + surveyDataSource.isSubmittedSurvey(surveyFormId, userId) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepository.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepository.kt new file mode 100644 index 000000000..973161632 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepository.kt @@ -0,0 +1,18 @@ +package com.wap.wapp.core.data.repository.user + +import com.wap.wapp.core.model.user.UserProfile + +interface UserRepository { + suspend fun getUserProfile(userId: String): Result + + suspend fun getUserId(): Result + + suspend fun postUserProfile( + userId: String, + userName: String, + studentId: String, + registeredAt: String, + ): Result + + suspend fun deleteUserProfile(userId: String): Result +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepositoryImpl.kt b/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepositoryImpl.kt new file mode 100644 index 000000000..ae1e0f260 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/repository/user/UserRepositoryImpl.kt @@ -0,0 +1,34 @@ +package com.wap.wapp.core.data.repository.user + +import com.wap.wapp.core.model.user.UserProfile +import com.wap.wapp.core.network.model.user.UserProfileRequest +import com.wap.wapp.core.network.source.user.UserDataSource +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val userDataSource: UserDataSource, +) : UserRepository { + override suspend fun getUserProfile(userId: String): Result = + userDataSource.getUserProfile(userId).mapCatching { response -> + response.toDomain() + } + + override suspend fun getUserId(): Result = userDataSource.getUserId() + + override suspend fun postUserProfile( + userId: String, + userName: String, + studentId: String, + registeredAt: String, + ): Result = userDataSource.postUserProfile( + UserProfileRequest( + userId = userId, + userName = userName, + studentId = studentId, + registeredAt = registeredAt, + ), + ) + + override suspend fun deleteUserProfile(userId: String): Result = + userDataSource.deleteUserProfile(userId) +} diff --git a/core/data/src/main/java/com/wap/wapp/core/data/utils/LocalDateTime.kt b/core/data/src/main/java/com/wap/wapp/core/data/utils/LocalDateTime.kt new file mode 100644 index 000000000..b80ec5bc4 --- /dev/null +++ b/core/data/src/main/java/com/wap/wapp/core/data/utils/LocalDateTime.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.core.data.utils + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +internal fun LocalDateTime.toISOLocalDateTimeString(): String = + this.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) diff --git a/core/data/src/test/java/com/wap/wapp/core/data/ExampleUnitTest.kt b/core/data/src/test/java/com/wap/wapp/core/data/ExampleUnitTest.kt new file mode 100644 index 000000000..4315f1728 --- /dev/null +++ b/core/data/src/test/java/com/wap/wapp/core/data/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.data + +import org.junit.Assert.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/core/designresource/.gitignore b/core/designresource/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/designresource/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designresource/build.gradle.kts b/core/designresource/build.gradle.kts new file mode 100644 index 000000000..f46ee1bae --- /dev/null +++ b/core/designresource/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.wap.wapp.library") +} + +android { + namespace = "com.wap.wapp.core.designresource" + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(libs.material) +} diff --git a/core/designresource/consumer-rules.pro b/core/designresource/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/designresource/proguard-rules.pro b/core/designresource/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/designresource/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/designresource/src/main/res/drawable/ic_absent.xml b/core/designresource/src/main/res/drawable/ic_absent.xml new file mode 100644 index 000000000..ed7c59bcf --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_absent.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_account_setting.xml b/core/designresource/src/main/res/drawable/ic_account_setting.xml new file mode 100644 index 000000000..fcccf4456 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_account_setting.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_add_question.xml b/core/designresource/src/main/res/drawable/ic_add_question.xml new file mode 100644 index 000000000..27bcc74f6 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_add_question.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_attendance.xml b/core/designresource/src/main/res/drawable/ic_attendance.xml new file mode 100644 index 000000000..fe6be009f --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_attendance.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_back.xml b/core/designresource/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..0aaf65619 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_balloon.xml b/core/designresource/src/main/res/drawable/ic_balloon.xml new file mode 100644 index 000000000..096fddade --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_balloon.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/designresource/src/main/res/drawable/ic_check.xml b/core/designresource/src/main/res/drawable/ic_check.xml new file mode 100644 index 000000000..bf50854fc --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_close.xml b/core/designresource/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..28133fae8 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_forward.xml b/core/designresource/src/main/res/drawable/ic_forward.xml new file mode 100644 index 000000000..9b7d6a0a5 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_forward_yellow.xml b/core/designresource/src/main/res/drawable/ic_forward_yellow.xml new file mode 100644 index 000000000..204021024 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_forward_yellow.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_github.xml b/core/designresource/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..95615b46a --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_github.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_green_circle.xml b/core/designresource/src/main/res/drawable/ic_green_circle.xml new file mode 100644 index 000000000..edb8d2259 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_green_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_guest_cat.xml b/core/designresource/src/main/res/drawable/ic_guest_cat.xml new file mode 100644 index 000000000..6af02ac75 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_guest_cat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/designresource/src/main/res/drawable/ic_guest_github.xml b/core/designresource/src/main/res/drawable/ic_guest_github.xml new file mode 100644 index 000000000..bb72150db --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_guest_github.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_magnifier.xml b/core/designresource/src/main/res/drawable/ic_magnifier.xml new file mode 100644 index 000000000..70296bddf --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_magnifier.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_manager_cat.xml b/core/designresource/src/main/res/drawable/ic_manager_cat.xml new file mode 100644 index 000000000..34ec0ac8a --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_manager_cat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/designresource/src/main/res/drawable/ic_manager_github.xml b/core/designresource/src/main/res/drawable/ic_manager_github.xml new file mode 100644 index 000000000..24dba7830 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_manager_github.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_normal_cat.xml b/core/designresource/src/main/res/drawable/ic_normal_cat.xml new file mode 100644 index 000000000..48f17bc98 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_normal_cat.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/designresource/src/main/res/drawable/ic_normal_github.xml b/core/designresource/src/main/res/drawable/ic_normal_github.xml new file mode 100644 index 000000000..3a4a3309f --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_normal_github.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designresource/src/main/res/drawable/ic_profile.xml b/core/designresource/src/main/res/drawable/ic_profile.xml new file mode 100644 index 000000000..8394a2082 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_profile_more.xml b/core/designresource/src/main/res/drawable/ic_profile_more.xml new file mode 100644 index 000000000..81a49075d --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_profile_more.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/core/designresource/src/main/res/drawable/ic_red_circle.xml b/core/designresource/src/main/res/drawable/ic_red_circle.xml new file mode 100644 index 000000000..697979309 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_red_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_return.xml b/core/designresource/src/main/res/drawable/ic_return.xml new file mode 100644 index 000000000..6ff659c14 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_return.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_small_right_arrow.xml b/core/designresource/src/main/res/drawable/ic_small_right_arrow.xml new file mode 100644 index 000000000..fd6714f57 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_small_right_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/designresource/src/main/res/drawable/ic_yellow_check.xml b/core/designresource/src/main/res/drawable/ic_yellow_check.xml new file mode 100644 index 000000000..db3bc57a6 --- /dev/null +++ b/core/designresource/src/main/res/drawable/ic_yellow_check.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/designresource/src/main/res/drawable/img_white_cat.png b/core/designresource/src/main/res/drawable/img_white_cat.png new file mode 100644 index 000000000..139c7a3ef Binary files /dev/null and b/core/designresource/src/main/res/drawable/img_white_cat.png differ diff --git a/core/designresource/src/main/res/font/notosanskr_bold.ttf b/core/designresource/src/main/res/font/notosanskr_bold.ttf new file mode 100644 index 000000000..6cf639eb7 Binary files /dev/null and b/core/designresource/src/main/res/font/notosanskr_bold.ttf differ diff --git a/core/designresource/src/main/res/font/notosanskr_medium.ttf b/core/designresource/src/main/res/font/notosanskr_medium.ttf new file mode 100644 index 000000000..5311c8a31 Binary files /dev/null and b/core/designresource/src/main/res/font/notosanskr_medium.ttf differ diff --git a/core/designresource/src/main/res/font/notosanskr_regular.ttf b/core/designresource/src/main/res/font/notosanskr_regular.ttf new file mode 100644 index 000000000..1b14d3247 Binary files /dev/null and b/core/designresource/src/main/res/font/notosanskr_regular.ttf differ diff --git a/core/designresource/src/main/res/values/colors.xml b/core/designresource/src/main/res/values/colors.xml new file mode 100644 index 000000000..268270d15 --- /dev/null +++ b/core/designresource/src/main/res/values/colors.xml @@ -0,0 +1,17 @@ + + + #131313 + + #252424 + #424242 + #828282 + + #A2A2A2 + #F4F4F4 + + #FBCF34 + + #FFFFFF + diff --git a/core/designresource/src/main/res/values/strings.xml b/core/designresource/src/main/res/values/strings.xml new file mode 100644 index 000000000..0815e2586 --- /dev/null +++ b/core/designresource/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + ๊ณต์ง€์‚ฌํ•ญ + ์„ค๋ฌธ + ํ”„๋กœํ•„ + ๊ด€๋ฆฌ + diff --git a/core/designresource/src/main/res/values/styles.xml b/core/designresource/src/main/res/values/styles.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/core/designresource/src/main/res/values/styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/designresource/src/main/res/values/typography.xml b/core/designresource/src/main/res/values/typography.xml new file mode 100644 index 000000000..7be008269 --- /dev/null +++ b/core/designresource/src/main/res/values/typography.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 000000000..11cb14aae --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("com.wap.wapp.library") + id("com.wap.wapp.compose") +} + +android { + namespace = "com.wap.wapp.core.designsystem" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.material) + implementation(libs.lottie.compose) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/core/designsystem/consumer-rules.pro b/core/designsystem/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/designsystem/proguard-rules.pro b/core/designsystem/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/designsystem/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/designsystem/src/androidTest/java/com/wap/designsystem/ExampleInstrumentedTest.kt b/core/designsystem/src/androidTest/java/com/wap/designsystem/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..93a5c5b34 --- /dev/null +++ b/core/designsystem/src/androidTest/java/com/wap/designsystem/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.designsystem + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.designsystem.test", appContext.packageName) + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/Color.kt b/core/designsystem/src/main/java/com/wap/designsystem/Color.kt new file mode 100644 index 000000000..2caf011d9 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/Color.kt @@ -0,0 +1,107 @@ +package com.wap.designsystem + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color + +val White = Color(0xFFFFFFFF) +val GrayF4 = Color(0xFFF4F4F4) +val GrayBD = Color(0xFFBDBDBD) +val Gray95 = Color(0xFF959595) +val Gray82 = Color(0xFF828282) +val Gray7C = Color(0xFF7C7C7C) +val Gray4A = Color(0xFF49494A) +val Black20 = Color(0xFF202022) +val Black25 = Color(0xFF252424) +val Black42 = Color(0xFF424242) +val Black = Color(0xFF000000) +val BackgroundBlack = Color(0xFF131313) + +val YellowA4 = Color(0xFFFEEBA4) +val Yellow3C = Color(0xFFFFBD3C) +val Yellow34 = Color(0xFFFBCF34) + +val GreenAB = Color(0xFF83F8AB) + +val Red = Color(0xFFE7475D) + +val Blue1FF = Color(0xFFEEF1FF) +val Blue4FF = Color(0xFFAAC4FF) +val Blue2FF = Color(0xFFB1B2FF) +val BlueA3 = Color(0xFF2F3EA3) + +@Stable +class WappColor( + white: Color = Color(0xFFFFFFFF), + black: Color = Color(0xFF000000), + backgroundBlack: Color = Color(0xFF131313), + black20: Color = Color(0xFF202022), + black25: Color = Color(0xFF252424), + black42: Color = Color(0xFF424242), + gray95: Color = Color(0xFF959595), + black82: Color = Color(0xFF828282), + grayA2: Color = Color(0xFFA2A2A2), + grayF4: Color = Color(0xFFF4F4F4), + grayBD: Color = Color(0xFFBDBDBD), + gray82: Color = Color(0xFF828282), + gray7C: Color = Color(0xFF7C7C7C), + gray4A: Color = Color(0xFF49494A), + yellow34: Color = Color(0xFFFBCF34), + yellow3C: Color = Color(0xFFFFBD3C), + yellowA4: Color = Color(0xFFFEEBA4), + greenAB: Color = Color(0xFF83F8AB), + red: Color = Color(0xFFE7475D), + blueA3: Color = Color(0xFF2F3EA3), + blue2FF: Color = Color(0xFFB1B2FF), + blue4FF: Color = Color(0xFFAAC4FF), + blue1FF: Color = Color(0xFFEEF1FF), +) { + var white by mutableStateOf(white) + private set + var black by mutableStateOf(black) + private set + var backgroundBlack by mutableStateOf(backgroundBlack) + private set + var black20 by mutableStateOf(black20) + private set + var black25 by mutableStateOf(black25) + private set + var black42 by mutableStateOf(black42) + private set + var gray95 by mutableStateOf(gray95) + private set + var black82 by mutableStateOf(black82) + private set + var grayA2 by mutableStateOf(grayA2) + private set + var grayF4 by mutableStateOf(grayF4) + private set + var gray4A by mutableStateOf(gray4A) + private set + var grayBD by mutableStateOf(grayBD) + private set + var gray82 by mutableStateOf(gray82) + private set + var gray7C by mutableStateOf(gray7C) + private set + var yellow34 by mutableStateOf(yellow34) + private set + var yellow3C by mutableStateOf(yellow3C) + private set + var yellowA4 by mutableStateOf(yellowA4) + private set + var greenAB by mutableStateOf(greenAB) + private set + var red by mutableStateOf(red) + private set + var blueA3 by mutableStateOf(blueA3) + private set + var blue2FF by mutableStateOf(blue2FF) + private set + var blue4FF by mutableStateOf(blue4FF) + private set + var blue1FF by mutableStateOf(blue1FF) + private set +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/Theme.kt b/core/designsystem/src/main/java/com/wap/designsystem/Theme.kt new file mode 100644 index 000000000..e8dc2ab6b --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/Theme.kt @@ -0,0 +1,33 @@ +package com.wap.designsystem + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf + +@Composable +fun WappTheme( + content: @Composable () -> Unit, +) { + val colors = WappColor() + val typography = WappTypography() + CompositionLocalProvider( + LocalWappColors provides colors, + LocalWappTypography provides typography, + ) { + MaterialTheme(content = content) + } +} + +object WappTheme { + val colors: WappColor @Composable get() = LocalWappColors.current + val typography: WappTypography @Composable get() = LocalWappTypography.current +} + +private val LocalWappColors = staticCompositionLocalOf { + error("Any WappColors Did Not Provided") +} + +private val LocalWappTypography = staticCompositionLocalOf { + error("Any WappTypography Did Not Provided") +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/Type.kt b/core/designsystem/src/main/java/com/wap/designsystem/Type.kt new file mode 100644 index 000000000..03c67b8ec --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/Type.kt @@ -0,0 +1,173 @@ +package com.wap.designsystem + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.wap.wapp.core.designsystem.R + +val NotoSansRegular: FontFamily = + FontFamily( + Font( + resId = R.font.notosanskr_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal, + ), + ) + +val NotoSansMedium: FontFamily = + FontFamily( + Font( + resId = R.font.notosanskr_medium, + weight = FontWeight.Medium, + style = FontStyle.Normal, + ), + ) + +val NotoSansBold: FontFamily = + FontFamily( + Font( + resId = R.font.notosanskr_bold, + weight = FontWeight.Bold, + style = FontStyle.Normal, + ), + ) + +@Stable +class WappTypography internal constructor( + titleBold: TextStyle, + titleMedium: TextStyle, + titleRegular: TextStyle, + contentBold: TextStyle, + contentMedium: TextStyle, + contentRegular: TextStyle, + labelBold: TextStyle, + labelMedium: TextStyle, + labelRegular: TextStyle, + captionBold: TextStyle, + captionMedium: TextStyle, + captionRegular: TextStyle, +) { + var titleBold: TextStyle by mutableStateOf(titleBold) + private set + var titleMedium: TextStyle by mutableStateOf(titleMedium) + private set + var titleRegular: TextStyle by mutableStateOf(titleRegular) + private set + var contentBold: TextStyle by mutableStateOf(contentBold) + private set + var contentMedium: TextStyle by mutableStateOf(contentMedium) + private set + var contentRegular: TextStyle by mutableStateOf(contentRegular) + private set + var labelBold: TextStyle by mutableStateOf(labelBold) + private set + var labelMedium: TextStyle by mutableStateOf(labelMedium) + private set + var labelRegular: TextStyle by mutableStateOf(labelRegular) + private set + var captionBold: TextStyle by mutableStateOf(captionBold) + private set + var captionMedium: TextStyle by mutableStateOf(captionMedium) + private set + var captionRegular: TextStyle by mutableStateOf(captionRegular) + private set +} + +@Composable +fun WappTypography(): WappTypography { + return WappTypography( + titleBold = TextStyle( + fontSize = 18.sp, + fontFamily = NotoSansBold, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + titleMedium = TextStyle( + fontSize = 18.sp, + fontFamily = NotoSansMedium, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + titleRegular = TextStyle( + fontSize = 18.sp, + fontFamily = NotoSansRegular, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + contentBold = TextStyle( + fontSize = 16.sp, + fontFamily = NotoSansBold, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + contentMedium = TextStyle( + fontSize = 16.sp, + fontFamily = NotoSansMedium, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + contentRegular = TextStyle( + fontSize = 16.sp, + fontFamily = NotoSansRegular, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + labelBold = TextStyle( + fontSize = 14.sp, + fontFamily = NotoSansBold, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + labelMedium = TextStyle( + fontSize = 14.sp, + fontFamily = NotoSansMedium, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + labelRegular = TextStyle( + fontSize = 14.sp, + fontFamily = NotoSansRegular, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + captionBold = TextStyle( + fontSize = 12.sp, + fontFamily = NotoSansBold, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + captionMedium = TextStyle( + fontSize = 12.sp, + fontFamily = NotoSansMedium, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + captionRegular = TextStyle( + fontSize = 12.sp, + fontFamily = NotoSansRegular, + platformStyle = PlatformTextStyle( + includeFontPadding = false, + ), + ), + ) +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/Button.kt new file mode 100644 index 000000000..367bb75c3 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/Button.kt @@ -0,0 +1,44 @@ +package com.wap.designsystem.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +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 com.wap.designsystem.WappTheme +import com.wap.wapp.core.designsystem.R + +@Composable +fun WappButton( + modifier: Modifier = Modifier, + @StringRes textRes: Int = R.string.done, + onClick: () -> Unit, + isEnabled: Boolean = true, +) { + Button( + modifier = modifier + .fillMaxWidth() + .height(48.dp), + onClick = { onClick() }, + enabled = isEnabled, + colors = ButtonDefaults.buttonColors( + contentColor = WappTheme.colors.white, + containerColor = WappTheme.colors.yellow34, + disabledContentColor = WappTheme.colors.white, + disabledContainerColor = WappTheme.colors.grayA2, + ), + shape = RoundedCornerShape(10.dp), + content = { + Text( + text = stringResource(textRes), + style = WappTheme.typography.contentRegular, + ) + }, + ) +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/Card.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/Card.kt new file mode 100644 index 000000000..05847bae6 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/Card.kt @@ -0,0 +1,21 @@ +package com.wap.designsystem.component + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme + +@Composable +fun WappCard( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Card( + shape = RoundedCornerShape(10.dp), + modifier = modifier, + backgroundColor = WappTheme.colors.black25, + content = content, + ) +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/CircleLoader.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/CircleLoader.kt new file mode 100644 index 000000000..418dcbe27 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/CircleLoader.kt @@ -0,0 +1,29 @@ +package com.wap.designsystem.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.wap.wapp.core.designsystem.R + +@Composable +fun CircleLoader( + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.Center, +) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.raw_loading)) + Box( + modifier = modifier, + contentAlignment = contentAlignment, + ) { + LottieAnimation( + composition = composition, + iterations = LottieConstants.IterateForever, + ) + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/MainTopBar.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/MainTopBar.kt new file mode 100644 index 000000000..b8e6904b9 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/MainTopBar.kt @@ -0,0 +1,173 @@ +package com.wap.designsystem.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.layout.wrapContentHeight +import androidx.compose.material3.Surface +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.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.designsystem.R + +@Composable +fun WappLeftMainTopBar( + @StringRes titleRes: Int, + @StringRes contentRes: Int, + showSettingButton: Boolean = false, + onClickSettingButton: () -> Unit = {}, + @StringRes settingButtonDescriptionRes: Int = R.string.setting_button, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.Start, + modifier = modifier + .padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 40.dp) + .fillMaxWidth() + .wrapContentHeight(), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(id = titleRes), + color = WappTheme.colors.white, + style = WappTheme.typography.titleBold.copy(fontSize = 24.sp), + modifier = Modifier.align(Alignment.CenterStart), + ) + + if (showSettingButton) { + Image( + painter = + painterResource(id = R.drawable.ic_subtract), + contentDescription = stringResource(id = settingButtonDescriptionRes), + modifier = Modifier + .align(Alignment.CenterEnd) + .clickable { onClickSettingButton() }, + ) + } + } + Text( + text = stringResource(id = contentRes), + color = WappTheme.colors.white, + style = WappTheme.typography.contentRegular, + ) + } +} + +@Composable +fun WappRightMainTopBar( + @StringRes titleRes: Int, + @StringRes contentRes: Int, + showBackButton: Boolean = false, + onClickBackButton: () -> Unit = {}, + @StringRes settingButtonDescriptionRes: Int = R.string.back_button, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.End, + modifier = modifier + .padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 40.dp) + .fillMaxWidth() + .wrapContentHeight(), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(id = titleRes), + color = WappTheme.colors.white, + style = WappTheme.typography.titleBold.copy(fontSize = 24.sp), + modifier = Modifier.align(Alignment.CenterEnd), + ) + + if (showBackButton) { + Image( + painter = + painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = settingButtonDescriptionRes), + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { onClickBackButton() }, + ) + } + } + Text( + text = stringResource(id = contentRes), + color = WappTheme.colors.white, + style = WappTheme.typography.contentRegular, + ) + } +} + +@Preview("without Button Left TopBar") +@Composable +fun WappLeftMainTopBarWithoutButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappLeftMainTopBar( + titleRes = R.string.notice, + contentRes = R.string.notice, + ) + } + } +} + +@Preview("with Button Left TopBar") +@Composable +fun WappLeftMainTopBarWithButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappLeftMainTopBar( + titleRes = R.string.notice, + contentRes = R.string.notice, + showSettingButton = true, + ) + } + } +} + +@Preview("without Button Right TopBar") +@Composable +fun WappRightMainTopBarWithoutButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappRightMainTopBar( + titleRes = R.string.notice, + contentRes = R.string.notice, + ) + } + } +} + +@Preview("with Button Right TopBar") +@Composable +fun WappRightMainTopBarWithButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappRightMainTopBar( + titleRes = R.string.notice, + contentRes = R.string.notice, + showBackButton = true, + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/NothingToShow.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/NothingToShow.kt new file mode 100644 index 000000000..a31981dd8 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/NothingToShow.kt @@ -0,0 +1,38 @@ +package com.wap.designsystem.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme + +@Composable +fun NothingToShow(@StringRes title: Int) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = stringResource(id = title), + style = WappTheme.typography.contentRegular.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + + Spacer(modifier = Modifier.weight(1f)) + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/RowBar.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/RowBar.kt new file mode 100644 index 000000000..736d97f67 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/RowBar.kt @@ -0,0 +1,51 @@ +package com.wap.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.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 com.wap.designsystem.WappTheme +import com.wap.wapp.core.designsystem.R + +@Composable +fun WappRowBar( + title: String, + onClicked: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onClicked() } + .padding(horizontal = 20.dp, vertical = 15.dp), + ) { + Text( + text = title, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier + .align(Alignment.CenterStart), + ) + Image( + painter = painterResource(id = R.drawable.ic_right_arrow_yellow), + contentDescription = stringResource(id = R.string.right_yellow_arrow_description), + modifier = Modifier + .align(Alignment.CenterEnd), + ) + } +} + +@Preview +@Composable +fun PreviewWappRowBar() { + WappRowBar(title = "์•Œ๋ฆผ ์„ค์ •") +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/SubTopBar.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/SubTopBar.kt new file mode 100644 index 000000000..ade79e523 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/SubTopBar.kt @@ -0,0 +1,143 @@ +package com.wap.designsystem.component + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +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 com.wap.designsystem.WappTheme +import com.wap.wapp.core.designsystem.R + +@Composable +fun WappSubTopBar( + @StringRes titleRes: Int, + modifier: Modifier = Modifier, + showLeftButton: Boolean = false, + showRightButton: Boolean = false, + onClickLeftButton: () -> Unit = {}, + onClickRightButton: () -> Unit = {}, + @StringRes leftButtonDescriptionRes: Int = R.string.back_button, + @DrawableRes leftButtonDrawableRes: Int = R.drawable.ic_back, +) { + Box( + modifier = modifier.fillMaxWidth(), + ) { + if (showLeftButton) { + Icon( + painter = painterResource(leftButtonDrawableRes), + contentDescription = stringResource(leftButtonDescriptionRes), + tint = WappTheme.colors.white, + modifier = Modifier + .padding(start = 20.dp) + .size(20.dp) + .clickable { onClickLeftButton() } + .align(Alignment.CenterStart), + ) + } + + Text( + text = stringResource(titleRes), + textAlign = TextAlign.Center, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.align(Alignment.Center), + ) + + if (showRightButton) { + Text( + text = stringResource(id = R.string.delete), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.yellow34, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 20.dp) + .clickable { onClickRightButton() }, + ) + } + } +} + +@Preview("without Button TopBar") +@Composable +fun WappSubTopBarWithoutButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappSubTopBar( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + titleRes = R.string.notice, + ) + } + } +} + +@Preview("with Right Button TopBar") +@Composable +fun WappSubTopBarWithRightButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappSubTopBar( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + titleRes = R.string.notice, + showRightButton = true, + ) + } + } +} + +@Preview("with Left Button TopBar") +@Composable +fun WappSubTopBarWithLeftButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappSubTopBar( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + titleRes = R.string.notice, + showLeftButton = true, + ) + } + } +} + +@Preview("with Both Button TopBar") +@Composable +fun WappSubTopBarWithBothButton() { + WappTheme { + Surface( + color = WappTheme.colors.backgroundBlack, + ) { + WappSubTopBar( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + titleRes = R.string.notice, + showRightButton = true, + showLeftButton = true, + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/TextField.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/TextField.kt new file mode 100644 index 000000000..aa117d198 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/TextField.kt @@ -0,0 +1,93 @@ +package com.wap.designsystem.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun WappTextField( + value: String, + onValueChanged: (String) -> Unit, + @StringRes label: Int, + isError: Boolean = false, + supportingText: String = "", +) { + val keyboardController = LocalSoftwareKeyboardController.current + + TextField( + value = value, + onValueChange = { newValue -> onValueChanged(newValue) }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = WappTheme.colors.black25, + focusedContainerColor = WappTheme.colors.black25, + errorContainerColor = WappTheme.colors.black25, + unfocusedLabelColor = WappTheme.colors.white, + focusedLabelColor = WappTheme.colors.white, + unfocusedTextColor = WappTheme.colors.white, + focusedTextColor = WappTheme.colors.white, + errorTextColor = WappTheme.colors.white, + unfocusedSupportingTextColor = WappTheme.colors.white, + focusedSupportingTextColor = WappTheme.colors.white, + focusedIndicatorColor = WappTheme.colors.yellow34, + cursorColor = WappTheme.colors.yellow34, + ), + label = { + Text(text = stringResource(label)) + }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + isError = isError, + supportingText = { + Text(text = supportingText) + }, + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun WappRoundedTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + @StringRes placeholder: Int, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + colors = TextFieldDefaults.colors( + focusedTextColor = WappTheme.colors.white, + unfocusedTextColor = WappTheme.colors.white, + focusedContainerColor = WappTheme.colors.black25, + unfocusedContainerColor = WappTheme.colors.black25, + focusedIndicatorColor = WappTheme.colors.black25, + unfocusedIndicatorColor = WappTheme.colors.black25, + cursorColor = WappTheme.colors.yellow34, + ), + placeholder = { + androidx.compose.material.Text( + text = stringResource(id = placeholder), + color = WappTheme.colors.gray82, + ) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + shape = RoundedCornerShape(10.dp), + ) +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/component/Title.kt b/core/designsystem/src/main/java/com/wap/designsystem/component/Title.kt new file mode 100644 index 000000000..4b994da0b --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/component/Title.kt @@ -0,0 +1,39 @@ +package com.wap.designsystem.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme + +@Composable +fun WappTitle( + title: String, + content: String, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier.fillMaxWidth(), + ) { + Text( + text = title, + style = WappTheme.typography.titleBold, + fontSize = 22.sp, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + Text( + text = content, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + } +} diff --git a/core/designsystem/src/main/java/com/wap/designsystem/modifier/ModifierUtil.kt b/core/designsystem/src/main/java/com/wap/designsystem/modifier/ModifierUtil.kt new file mode 100644 index 000000000..2152086a1 --- /dev/null +++ b/core/designsystem/src/main/java/com/wap/designsystem/modifier/ModifierUtil.kt @@ -0,0 +1,17 @@ +package com.wap.designsystem.modifier + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.input.pointer.pointerInput + +fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier { + return this.pointerInput(Unit) { + detectTapGestures( + onTap = { + doOnClear() + focusManager.clearFocus() + }, + ) + } +} diff --git a/core/designsystem/src/main/res/drawable/ic_back.xml b/core/designsystem/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..0aaf65619 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_close.xml b/core/designsystem/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..844b6b62e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_right_arrow_yellow.xml b/core/designsystem/src/main/res/drawable/ic_right_arrow_yellow.xml new file mode 100644 index 000000000..8a52c1dc6 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_right_arrow_yellow.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_subtract.xml b/core/designsystem/src/main/res/drawable/ic_subtract.xml new file mode 100644 index 000000000..8b9e47088 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_subtract.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/designsystem/src/main/res/font/notosanskr_bold.ttf b/core/designsystem/src/main/res/font/notosanskr_bold.ttf new file mode 100644 index 000000000..6cf639eb7 Binary files /dev/null and b/core/designsystem/src/main/res/font/notosanskr_bold.ttf differ diff --git a/core/designsystem/src/main/res/font/notosanskr_medium.ttf b/core/designsystem/src/main/res/font/notosanskr_medium.ttf new file mode 100644 index 000000000..5311c8a31 Binary files /dev/null and b/core/designsystem/src/main/res/font/notosanskr_medium.ttf differ diff --git a/core/designsystem/src/main/res/font/notosanskr_regular.ttf b/core/designsystem/src/main/res/font/notosanskr_regular.ttf new file mode 100644 index 000000000..1b14d3247 Binary files /dev/null and b/core/designsystem/src/main/res/font/notosanskr_regular.ttf differ diff --git a/core/designsystem/src/main/res/raw/raw_loading.json b/core/designsystem/src/main/res/raw/raw_loading.json new file mode 100644 index 000000000..1ed540c44 --- /dev/null +++ b/core/designsystem/src/main/res/raw/raw_loading.json @@ -0,0 +1 @@ +{"nm":"loading_6","ddd":0,"h":300,"w":300,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":300,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"s":[360],"t":60}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[300,300],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"c":{"a":0,"k":[0.9843,0.8118,0.2039,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[1],"t":0},{"s":[100],"t":50}],"ix":2},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"s":[3],"t":60}],"ix":3},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":10},{"s":[99],"t":60}],"ix":1},"m":1}],"ind":1},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":300,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":30,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[300,300],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"c":{"a":0,"k":[1,1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2}],"v":"5.8.1","fr":30,"op":60,"ip":0,"assets":[]} diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml new file mode 100644 index 000000000..59b6bf47e --- /dev/null +++ b/core/designsystem/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ + ์„ค์ • ๋ฒ„ํŠผ + ๋‹ซ๊ธฐ ๋ฒ„ํŠผ + ๊ณต์ง€์‚ฌํ•ญ + ์™„๋ฃŒ + ์‚ญ์ œ + ํ•ด๋‹น ๊ธฐ๋Šฅ์œผ๋กœ ์ด๋™ํ•˜๋Š” ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ฝˆ๋กํ•œ ๋…ธ๋ž€์ƒ‰ ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค. + diff --git a/core/designsystem/src/test/java/com/wap/designsystem/ExampleUnitTest.kt b/core/designsystem/src/test/java/com/wap/designsystem/ExampleUnitTest.kt new file mode 100644 index 000000000..dd0419f5b --- /dev/null +++ b/core/designsystem/src/test/java/com/wap/designsystem/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.designsystem + +import org.junit.Assert.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/core/domain/.gitignore b/core/domain/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts new file mode 100644 index 000000000..effdcc047 --- /dev/null +++ b/core/domain/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("com.wap.wapp.library") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.core.domain" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:data")) + implementation(project(":core:model")) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/core/domain/consumer-rules.pro b/core/domain/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/domain/proguard-rules.pro b/core/domain/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/domain/src/androidTest/java/com/wap/wapp/core/domain/ExampleInstrumentedTest.kt b/core/domain/src/androidTest/java/com/wap/wapp/core/domain/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..406dfe371 --- /dev/null +++ b/core/domain/src/androidTest/java/com/wap/wapp/core/domain/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.domain + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.core.domain.test", appContext.packageName) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/model/AuthState.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/model/AuthState.kt new file mode 100644 index 000000000..e603e17dd --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/model/AuthState.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.domain.model + +enum class AuthState { + SIGN_IN, SIGN_UP +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/model/CodeValidation.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/model/CodeValidation.kt new file mode 100644 index 000000000..c8693f8aa --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/model/CodeValidation.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.domain.model + +enum class CodeValidation { + INVALID, VALID +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetAttendanceUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetAttendanceUseCase.kt new file mode 100644 index 000000000..0edbbff8b --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetAttendanceUseCase.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.domain.usecase.attendance + +import com.wap.wapp.core.data.repository.attendance.AttendanceRepository +import javax.inject.Inject + +class GetAttendanceUseCase @Inject constructor( + private val attendanceRepository: AttendanceRepository, +) { + suspend operator fun invoke(eventId: String) = attendanceRepository.getAttendance(eventId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetEventListAttendanceUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetEventListAttendanceUseCase.kt new file mode 100644 index 000000000..abc915475 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/GetEventListAttendanceUseCase.kt @@ -0,0 +1,27 @@ +package com.wap.wapp.core.domain.usecase.attendance + +import com.wap.wapp.core.data.repository.attendance.AttendanceRepository +import com.wap.wapp.core.model.attendance.Attendance +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class GetEventListAttendanceUseCase @Inject constructor( + private val attendanceRepository: AttendanceRepository, +) { + // ์˜ค๋Š˜ ์ผ์ • ์ค‘, ์ถœ์„์ด ์‹œ์ž‘๋œ ์ผ์ •๋“ค์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + suspend operator fun invoke(eventIdList: List): Result> = + runCatching { + // eventIdList๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ถœ์„์ด ์—ด๋ ธ๋Š”์ง€ ๋ณ‘๋ ฌ์ ์œผ๋กœ ๋ชจ๋‘ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + coroutineScope { + val deferredList = eventIdList.map { eventId -> + async { attendanceRepository.getAttendance(eventId = eventId) } + } + + deferredList.awaitAll() + .map { it.getOrThrow() } + .filter { it.isBeforeEndTime() } + } + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/PostAttendanceUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/PostAttendanceUseCase.kt new file mode 100644 index 000000000..966c0596c --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/PostAttendanceUseCase.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.domain.usecase.attendance + +import com.wap.wapp.core.data.repository.attendance.AttendanceRepository +import java.time.LocalDateTime +import javax.inject.Inject + +class PostAttendanceUseCase @Inject constructor( + private val attendanceRepository: AttendanceRepository, +) { + suspend operator fun invoke( + eventId: String, + code: String, + deadline: LocalDateTime, + ): Result = + attendanceRepository.postAttendance(eventId = eventId, code = code, deadline = deadline) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/ValidationAttendanceCodeUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/ValidationAttendanceCodeUseCase.kt new file mode 100644 index 000000000..05925e8dd --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendance/ValidationAttendanceCodeUseCase.kt @@ -0,0 +1,15 @@ +package com.wap.wapp.core.domain.usecase.attendance + +import com.wap.wapp.core.data.repository.attendance.AttendanceRepository +import javax.inject.Inject + +class ValidationAttendanceCodeUseCase @Inject constructor( + private val attendanceRepository: AttendanceRepository, +) { + suspend operator fun invoke(eventId: String, attendanceCode: String): Result = + runCatching { + attendanceRepository.getAttendance(eventId).map { + it.code == attendanceCode + }.getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventAttendanceStatusUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventAttendanceStatusUseCase.kt new file mode 100644 index 000000000..5f1a14b15 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventAttendanceStatusUseCase.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.domain.usecase.attendancestatus + +import com.wap.wapp.core.data.repository.attendancestatus.AttendanceStatusRepository +import javax.inject.Inject + +class GetEventAttendanceStatusUseCase @Inject constructor( + private val attendanceStatusRepository: AttendanceStatusRepository, +) { + suspend operator fun invoke(eventId: String, userId: String) = + attendanceStatusRepository.getAttendanceStatus(eventId = eventId, userId = userId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventListAttendanceStatusUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventListAttendanceStatusUseCase.kt new file mode 100644 index 000000000..923013b66 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/GetEventListAttendanceStatusUseCase.kt @@ -0,0 +1,33 @@ +package com.wap.wapp.core.domain.usecase.attendancestatus + +import com.wap.wapp.core.data.repository.attendancestatus.AttendanceStatusRepository +import com.wap.wapp.core.model.attendancestatus.AttendanceStatus +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class GetEventListAttendanceStatusUseCase @Inject constructor( + private val attendanceStatusRepository: AttendanceStatusRepository, +) { + // eventIdList๋ฅผ ๋ฐ›์•„์„œ, ํ•ด๋‹น ์ผ์ •์— ์‚ฌ์šฉ์ž๊ฐ€ ์ถœ์„์„ ํ–ˆ๋Š”์ง€, ํ•˜์ง€ ์•Š์•˜๋Š” ์ง€ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + suspend operator fun invoke( + eventIdList: List, + userId: String, + ): Result> = runCatching { + coroutineScope { + val deferredList = eventIdList.map { eventId -> + async { + attendanceStatusRepository.getAttendanceStatus( + eventId = eventId, + userId = userId, + ) + } + } + + deferredList.awaitAll().map { + it.getOrThrow() + } + } + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/PostAttendanceStatusUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/PostAttendanceStatusUseCase.kt new file mode 100644 index 000000000..75de717d9 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/attendancestatus/PostAttendanceStatusUseCase.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.domain.usecase.attendancestatus + +import com.wap.wapp.core.data.repository.attendancestatus.AttendanceStatusRepository +import javax.inject.Inject + +class PostAttendanceStatusUseCase @Inject constructor( + private val attendanceStatusRepository: AttendanceStatusRepository, +) { + suspend operator fun invoke(eventId: String, userId: String): Result = + attendanceStatusRepository.postAttendance(eventId = eventId, userId = userId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/CheckMemberCodeUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/CheckMemberCodeUseCase.kt new file mode 100644 index 000000000..0dcf0d647 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/CheckMemberCodeUseCase.kt @@ -0,0 +1,21 @@ +package com.wap.wapp.core.domain.usecase.auth + +import com.wap.wapp.core.data.repository.auth.AuthRepository +import com.wap.wapp.core.domain.model.CodeValidation +import javax.inject.Inject + +class CheckMemberCodeUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(code: String): Result = runCatching { + authRepository.checkMemberCode(code).fold( + onSuccess = { isValid -> + if (isValid) { + return@fold CodeValidation.VALID + } + CodeValidation.INVALID + }, + onFailure = { CodeValidation.INVALID }, + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/DeleteUserUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/DeleteUserUseCase.kt new file mode 100644 index 000000000..90219486e --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/DeleteUserUseCase.kt @@ -0,0 +1,35 @@ +package com.wap.wapp.core.domain.usecase.auth + +import com.wap.wapp.core.data.repository.auth.AuthRepository +import com.wap.wapp.core.data.repository.management.ManagementRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.domain.usecase.user.GetUserRoleUseCase +import com.wap.wapp.core.model.user.UserRole +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DeleteUserUseCase @Inject constructor( + private val authRepository: AuthRepository, + private val userRepository: UserRepository, + private val managementRepository: ManagementRepository, + private val getUserRoleUseCase: GetUserRoleUseCase, +) { + suspend operator fun invoke(userId: String): Result = runCatching { + val userRole = getUserRoleUseCase().getOrThrow() + when (userRole) { + UserRole.GUEST -> { return@runCatching } + + UserRole.MEMBER -> { + userRepository.deleteUserProfile(userId) + } + + UserRole.MANAGER -> { + userRepository.deleteUserProfile(userId) + managementRepository.deleteManager(userId) + } + } + + authRepository.deleteUser().getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/IsUserSignInUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/IsUserSignInUseCase.kt new file mode 100644 index 000000000..04f910257 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/IsUserSignInUseCase.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.domain.usecase.auth + +import com.wap.wapp.core.data.repository.auth.AuthRepository +import javax.inject.Inject + +class IsUserSignInUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(): Result = authRepository.isUserSignIn() +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignInUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignInUseCase.kt new file mode 100644 index 000000000..511c159cd --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignInUseCase.kt @@ -0,0 +1,33 @@ +package com.wap.wapp.core.domain.usecase.auth + +import com.wap.wapp.core.data.repository.auth.SignInRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.domain.model.AuthState +import com.wap.wapp.core.domain.model.AuthState.SIGN_IN +import com.wap.wapp.core.domain.model.AuthState.SIGN_UP +import dagger.hilt.android.scopes.ActivityScoped +import javax.inject.Inject + +@ActivityScoped +class SignInUseCase @Inject constructor( + private val signInRepository: SignInRepository, + private val userRepository: UserRepository, +) { + suspend operator fun invoke(email: String): Result = runCatching { + val userId = signInRepository.signIn(email) + .getOrThrow() + + userRepository.getUserProfile(userId) + .fold( + onFailure = { exception -> + // ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์ธ ๊ฒฝ์šฐ + if (exception is IllegalStateException) { + return@fold SIGN_UP + } + // ๊ทธ ์™ธ์˜ ์˜ˆ์™ธ์ธ ๊ฒฝ์šฐ + throw (exception) + }, + onSuccess = { SIGN_IN }, + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignOutUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignOutUseCase.kt new file mode 100644 index 000000000..eae7758be --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/auth/SignOutUseCase.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.domain.usecase.auth + +import com.wap.wapp.core.data.repository.auth.AuthRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SignOutUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(): Result = + authRepository.signOut() +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/DeleteEventAndRelatedSurveysUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/DeleteEventAndRelatedSurveysUseCase.kt new file mode 100644 index 000000000..fc4970cdb --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/DeleteEventAndRelatedSurveysUseCase.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import javax.inject.Inject + +class DeleteEventAndRelatedSurveysUseCase @Inject constructor( + private val eventRepository: EventRepository, + private val surveyFormRepository: SurveyFormRepository, + private val surveyRepository: SurveyRepository, +) { + // ์ผ์ •์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€๋กœ ์ด์™€ ๊ด€๋ จ๋œ ์„ค๋ฌธ ํผ, ์ž‘์„ฑ๋œ ์„ค๋ฌธ๋“ค์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + suspend operator fun invoke(eventId: String): Result = runCatching { + eventRepository.deleteEvent(eventId).getOrThrow() + + surveyFormRepository.getSurveyFormListByEventId(eventId).map { surveyFormList -> + surveyFormList.map { surveyForm -> + surveyFormRepository.deleteSurveyForm(surveyForm.surveyFormId) + } + }.getOrThrow() + + surveyRepository.getSurveyListByEventId(eventId).map { surveyList -> + surveyList.map { survey -> + surveyRepository.deleteSurvey(survey.surveyId) + } + }.getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetDateEventListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetDateEventListUseCase.kt new file mode 100644 index 000000000..a37684209 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetDateEventListUseCase.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.model.event.Event +import java.time.LocalDate +import javax.inject.Inject + +class GetDateEventListUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke(date: LocalDate): Result> = + eventRepository.getDateEventList(date) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventListUseCase.kt new file mode 100644 index 000000000..8521d36a5 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventListUseCase.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.model.event.Event +import javax.inject.Inject + +class GetEventListUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke(): Result> = eventRepository.getEventList() +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventUseCase.kt new file mode 100644 index 000000000..adbea66df --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetEventUseCase.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import javax.inject.Inject + +class GetEventUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke(eventId: String) = eventRepository.getEvent(eventId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetMonthEventListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetMonthEventListUseCase.kt new file mode 100644 index 000000000..e7fb265d2 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetMonthEventListUseCase.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.model.event.Event +import java.time.LocalDate +import javax.inject.Inject + +class GetMonthEventListUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke(date: LocalDate): Result> = + eventRepository.getMonthEventList(date) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetRecentEventListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetRecentEventListUseCase.kt new file mode 100644 index 000000000..7cd5445d3 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/GetRecentEventListUseCase.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import com.wap.wapp.core.model.event.Event +import java.time.LocalDate +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import javax.inject.Inject + +class GetRecentEventListUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke(registrationDate: LocalDate): Result> { + val currentDate = LocalDate.now(ZoneId.of("Asia/Seoul")) + val minimumDate = currentDate.minus(3, ChronoUnit.MONTHS) + val selectedDate = maxOf(registrationDate, minimumDate) + return eventRepository.getEventListFromDate(selectedDate) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/PostEventUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/PostEventUseCase.kt new file mode 100644 index 000000000..b0a286acc --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/PostEventUseCase.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +class PostEventUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke( + eventTitle: String, + eventContent: String, + eventLocation: String, + eventStartDate: LocalDate, + eventStartTime: LocalTime, + eventEndDate: LocalDate, + eventEndTime: LocalTime, + ): Result = runCatching { + eventRepository.postEvent( + title = eventTitle, + content = eventContent, + location = eventLocation, + startDateTime = LocalDateTime.of(eventStartDate, eventStartTime), + endDateTime = LocalDateTime.of(eventEndDate, eventEndTime), + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/UpdateEventUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/UpdateEventUseCase.kt new file mode 100644 index 000000000..9c209253a --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/event/UpdateEventUseCase.kt @@ -0,0 +1,31 @@ +package com.wap.wapp.core.domain.usecase.event + +import com.wap.wapp.core.data.repository.event.EventRepository +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +class UpdateEventUseCase @Inject constructor( + private val eventRepository: EventRepository, +) { + suspend operator fun invoke( + eventId: String, + eventTitle: String, + eventContent: String, + eventLocation: String, + eventStartDate: LocalDate, + eventStartTime: LocalTime, + eventEndDate: LocalDate, + eventEndTime: LocalTime, + ): Result = runCatching { + eventRepository.updateEvent( + eventId = eventId, + title = eventTitle, + content = eventContent, + location = eventLocation, + startDateTime = LocalDateTime.of(eventStartDate, eventStartTime), + endDateTime = LocalDateTime.of(eventEndDate, eventEndTime), + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/management/CheckManagementCodeUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/management/CheckManagementCodeUseCase.kt new file mode 100644 index 000000000..999cdd644 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/management/CheckManagementCodeUseCase.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.core.domain.usecase.management + +import com.wap.wapp.core.data.repository.management.ManagementRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.domain.model.CodeValidation +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CheckManagementCodeUseCase @Inject constructor( + private val managementRepository: ManagementRepository, + private val userRepository: UserRepository, +) { + suspend operator fun invoke(code: String): Result = runCatching { + managementRepository.checkManagementCode(code) + .onSuccess { isValid -> + if (isValid.not()) { // ์ฝ”๋“œ๊ฐ€ ํ‹€๋ ธ์„ ๊ฒฝ์šฐ + return@runCatching CodeValidation.INVALID + } + } + + // ์šด์˜์ง„ ๋“ฑ๋ก + val userId = userRepository.getUserId().getOrThrow() + managementRepository.postManager(userId).fold( + onSuccess = { CodeValidation.VALID }, + onFailure = { exception -> throw (exception) }, + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyFormAndRelatedSurveysUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyFormAndRelatedSurveysUseCase.kt new file mode 100644 index 000000000..4547e5980 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyFormAndRelatedSurveysUseCase.kt @@ -0,0 +1,20 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import javax.inject.Inject + +class DeleteSurveyFormAndRelatedSurveysUseCase @Inject constructor( + private val surveyFormRepository: SurveyFormRepository, + private val surveyRepository: SurveyRepository, +) { + // ์„ค๋ฌธ ํผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€๋กœ ์ด์™€ ๊ด€๋ จ๋œ ์ž‘์„ฑ๋œ ์„ค๋ฌธ๋“ค์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + suspend operator fun invoke(surveyFormId: String): Result = runCatching { + surveyFormRepository.deleteSurveyForm(surveyFormId).getOrThrow() + surveyRepository.getSurveyListBySurveyFormId(surveyFormId).map { surveyList -> + surveyList.map { survey -> + surveyRepository.deleteSurvey(survey.surveyId) + } + }.getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyUseCase.kt new file mode 100644 index 000000000..57190bc89 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/DeleteSurveyUseCase.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import javax.inject.Inject + +class DeleteSurveyUseCase @Inject constructor( + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke(surveyId: String): Result = + surveyRepository.deleteSurvey(surveyId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormListUseCase.kt new file mode 100644 index 000000000..898087130 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormListUseCase.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import javax.inject.Inject + +class GetSurveyFormListUseCase @Inject constructor( + private val surveyFormRepository: SurveyFormRepository, +) { + suspend operator fun invoke() = surveyFormRepository.getSurveyFormList() + + suspend operator fun invoke(eventId: String) = + surveyFormRepository.getSurveyFormListByEventId(eventId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormUseCase.kt new file mode 100644 index 000000000..424be5deb --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyFormUseCase.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import javax.inject.Inject + +class GetSurveyFormUseCase @Inject constructor( + private val surveyFormRepository: SurveyFormRepository, +) { + suspend operator fun invoke(surveyFormId: String) = + surveyFormRepository.getSurveyForm(surveyFormId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyListUseCase.kt new file mode 100644 index 000000000..be3cbf8dc --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyListUseCase.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.model.survey.Survey +import javax.inject.Inject + +class GetSurveyListUseCase @Inject constructor( + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke(): Result> = + surveyRepository.getSurveyList() +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyUseCase.kt new file mode 100644 index 000000000..6400d782f --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetSurveyUseCase.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.model.survey.Survey +import javax.inject.Inject + +class GetSurveyUseCase @Inject constructor( + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke(surveyId: String): Result = + surveyRepository.getSurvey(surveyId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetUserRespondedSurveyListUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetUserRespondedSurveyListUseCase.kt new file mode 100644 index 000000000..d4697af5f --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/GetUserRespondedSurveyListUseCase.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.model.survey.Survey +import javax.inject.Inject + +class GetUserRespondedSurveyListUseCase @Inject constructor( + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke(userId: String): Result> = + surveyRepository.getUserRespondedSurveyList(userId) +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/IsSubmittedSurveyUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/IsSubmittedSurveyUseCase.kt new file mode 100644 index 000000000..f2fca1b67 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/IsSubmittedSurveyUseCase.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import javax.inject.Inject + +class IsSubmittedSurveyUseCase @Inject constructor( + private val userRepository: UserRepository, + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke(surveyFormId: String): Result = runCatching { + val userId = userRepository.getUserId().getOrThrow() + + surveyRepository.isSubmittedSurvey( + userId = userId, + surveyFormId = surveyFormId, + ).getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyFormUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyFormUseCase.kt new file mode 100644 index 000000000..6fcfca3d8 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyFormUseCase.kt @@ -0,0 +1,30 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.SurveyQuestion +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +class PostSurveyFormUseCase @Inject constructor( + private val surveyFormRepository: SurveyFormRepository, +) { + suspend operator fun invoke( + event: Event, + title: String, + content: String, + surveyQuestionList: List, + deadlineDate: LocalDate, + deadlineTime: LocalTime, + ): Result = runCatching { + surveyFormRepository.postSurveyForm( + eventId = event.eventId, + title = title, + content = content, + surveyQuestionList = surveyQuestionList, + deadline = LocalDateTime.of(deadlineDate, deadlineTime), + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyUseCase.kt new file mode 100644 index 000000000..981ca5682 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/PostSurveyUseCase.kt @@ -0,0 +1,32 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.model.survey.SurveyAnswer +import java.time.LocalDateTime +import javax.inject.Inject + +class PostSurveyUseCase @Inject constructor( + private val userRepository: UserRepository, + private val surveyRepository: SurveyRepository, +) { + suspend operator fun invoke( + surveyFormId: String, + eventId: String, + title: String, + content: String, + surveyAnswerList: List, + ): Result = runCatching { + val userId = userRepository.getUserId().getOrThrow() + + surveyRepository.postSurvey( + surveyFormId = surveyFormId, + userId = userId, + eventId = eventId, + title = title, + content = content, + surveyAnswerList = surveyAnswerList, + surveyedAt = LocalDateTime.now(), + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/UpdateSurveyFormUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/UpdateSurveyFormUseCase.kt new file mode 100644 index 000000000..c88af0b1b --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/survey/UpdateSurveyFormUseCase.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.domain.usecase.survey + +import com.wap.wapp.core.data.repository.survey.SurveyFormRepository +import com.wap.wapp.core.model.survey.SurveyForm +import javax.inject.Inject + +class UpdateSurveyFormUseCase @Inject constructor( + private val surveyFormRepository: SurveyFormRepository, +) { + suspend operator fun invoke(surveyForm: SurveyForm): Result = runCatching { + surveyFormRepository.updateSurveyForm(surveyForm) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserProfileUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserProfileUseCase.kt new file mode 100644 index 000000000..3df73744c --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserProfileUseCase.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.domain.usecase.user + +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.model.user.UserProfile +import javax.inject.Inject + +class GetUserProfileUseCase @Inject constructor(private val userRepository: UserRepository) { + suspend operator fun invoke(): Result = runCatching { + val userId = userRepository.getUserId().getOrThrow() + + userRepository.getUserProfile(userId).fold( + onSuccess = { userProfile -> userProfile }, + onFailure = { throw it }, + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserRoleUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserRoleUseCase.kt new file mode 100644 index 000000000..878acfe52 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/GetUserRoleUseCase.kt @@ -0,0 +1,35 @@ +package com.wap.wapp.core.domain.usecase.user + +import com.wap.wapp.core.data.repository.management.ManagementRepository +import com.wap.wapp.core.data.repository.user.UserRepository +import com.wap.wapp.core.model.user.UserRole +import javax.inject.Inject + +class GetUserRoleUseCase @Inject constructor( + private val userRepository: UserRepository, + private val managementRepository: ManagementRepository, +) { + suspend operator fun invoke(): Result = + runCatching { + val userId = userRepository.getUserId() + .getOrElse { exception -> + if (exception is IllegalStateException) { // ํšŒ์›์ด ์•„๋‹Œ ๊ฒฝ์šฐ + return@runCatching UserRole.GUEST + } + throw exception + } + + managementRepository.isManager(userId) + .fold( + onSuccess = { isManager -> + if (isManager) { // ๋งค๋‹ˆ์ €์ธ ๊ฒฝ์šฐ + return@fold UserRole.MANAGER + } + UserRole.MEMBER + }, + onFailure = { exception -> + throw exception + }, + ) + } +} diff --git a/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/PostUserProfileUseCase.kt b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/PostUserProfileUseCase.kt new file mode 100644 index 000000000..d6c9b71a0 --- /dev/null +++ b/core/domain/src/main/java/com/wap/wapp/core/domain/usecase/user/PostUserProfileUseCase.kt @@ -0,0 +1,25 @@ +package com.wap.wapp.core.domain.usecase.user + +import com.wap.wapp.core.data.repository.user.UserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PostUserProfileUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + suspend operator fun invoke( + userName: String, + studentId: String, + registeredAt: String, + ): Result = runCatching { + val userId = userRepository.getUserId().getOrThrow() + + userRepository.postUserProfile( + userId = userId, + userName = userName, + studentId = studentId, + registeredAt = registeredAt, + ) + } +} diff --git a/core/domain/src/test/java/com/wap/wapp/core/domain/ExampleUnitTest.kt b/core/domain/src/test/java/com/wap/wapp/core/domain/ExampleUnitTest.kt new file mode 100644 index 000000000..a899a99bd --- /dev/null +++ b/core/domain/src/test/java/com/wap/wapp/core/domain/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.domain + +import org.junit.Assert.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/core/model/.gitignore b/core/model/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/model/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts new file mode 100644 index 000000000..62383a1a4 --- /dev/null +++ b/core/model/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("com.wap.wapp.library") +} + +android { + namespace = "com.wap.wapp.core.model" + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} diff --git a/core/model/consumer-rules.pro b/core/model/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/model/proguard-rules.pro b/core/model/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/model/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/model/src/main/java/com/wap/wapp/core/model/attendance/Attendance.kt b/core/model/src/main/java/com/wap/wapp/core/model/attendance/Attendance.kt new file mode 100644 index 000000000..ef0c5f5a3 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/attendance/Attendance.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.core.model.attendance + +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId +import kotlin.time.toKotlinDuration + +data class Attendance( + val eventId: String, + val code: String, + val deadline: LocalDateTime, +) { + fun calculateDeadline(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + val duration = Duration.between(currentDateTime, deadline) + + val leftMinutes = duration.toKotlinDuration().inWholeMinutes.toString() + return "${leftMinutes}๋ถ„ ํ›„ ๋งˆ๊ฐ" + } + + fun isBeforeEndTime(): Boolean { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + return currentDateTime.isBefore(deadline) + } + + companion object { + val TIME_ZONE_SEOUL = ZoneId.of("Asia/Seoul") + } +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/attendancestatus/AttendanceStatus.kt b/core/model/src/main/java/com/wap/wapp/core/model/attendancestatus/AttendanceStatus.kt new file mode 100644 index 000000000..a5f47b66e --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/attendancestatus/AttendanceStatus.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.core.model.attendancestatus + +data class AttendanceStatus( + val attendanceDateTime: String = "", +) { + fun isAttendance() = attendanceDateTime.isNotEmpty() +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/event/Event.kt b/core/model/src/main/java/com/wap/wapp/core/model/event/Event.kt new file mode 100644 index 000000000..b39957b6d --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/event/Event.kt @@ -0,0 +1,72 @@ +package com.wap.wapp.core.model.event + +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +data class Event( + val content: String, + val eventId: String, + val location: String, + val title: String, + val startDateTime: LocalDateTime, + val endDateTime: LocalDateTime, +) { + fun getCalculatedTime(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + if (startDateTime >= currentDateTime) { + return calculateStartTime() + } + + if (endDateTime >= currentDateTime) { + return calculateDeadline() + } + + return "๋งˆ๊ฐ" + } + + private fun calculateStartTime(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + val duration = Duration.between(currentDateTime, startDateTime) + + if (duration.toMinutes() < 60) { + val leftMinutes = duration.toMinutes().toString() + return leftMinutes + "๋ถ„ ํ›„ ์‹œ์ž‘" + } + + if (duration.toHours() < 24) { + val leftHours = duration.toHours().toString() + return leftHours + "์‹œ๊ฐ„ ํ›„ ์‹œ์ž‘" + } + + return endDateTime.format(yyyyMMddFormatter) + " ์‹œ์ž‘" + } + + private fun calculateDeadline(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + val duration = Duration.between(currentDateTime, endDateTime) + + if (duration.toMinutes() < 60) { + val leftMinutes = duration.toMinutes().toString() + return leftMinutes + "๋ถ„ ํ›„ ์ข…๋ฃŒ" + } + + if (duration.toHours() < 24) { + val leftHours = duration.toHours().toString() + return leftHours + "์‹œ๊ฐ„ ํ›„ ์ข…๋ฃŒ" + } + + return endDateTime.format(yyyyMMddFormatter) + " ๋งˆ๊ฐ" + } + + fun isBeforeEndTime(): Boolean { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + return currentDateTime.isBefore(endDateTime) + } + + companion object { + val yyyyMMddFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + val TIME_ZONE_SEOUL = ZoneId.of("Asia/Seoul") + } +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/survey/Rating.kt b/core/model/src/main/java/com/wap/wapp/core/model/survey/Rating.kt new file mode 100644 index 000000000..2a024b501 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/survey/Rating.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.model.survey + +enum class Rating { + GOOD, MEDIOCRE, BAD +} + +data class RatingDescription( + val title: String, + val content: String, +) + +fun Rating.toDescription(): RatingDescription = when (this) { + Rating.GOOD -> { + RatingDescription("์ข‹์Œ", "ํ–‰์‚ฌ ์ง„ํ–‰์ด ์™„๋ฒฝํ•˜๊ณ , \nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋งŽ์•˜์Œ.") + } + Rating.MEDIOCRE -> { + RatingDescription("๋ณดํ†ต", "ํ–‰์‚ฌ ์ง„ํ–‰์ด ์›ํ™œํ•˜๊ณ , \nํ–‰์‚ฌ ๋‚ด์šฉ์ด ์ ๋‹นํ•จ.") + } + Rating.BAD -> { + RatingDescription("๋‚˜์จ", "ํ–‰์‚ฌ ์ง„ํ–‰์ด ์•„์‰ฌ์› ๊ณ , \nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋ถ€์กฑํ•จ.") + } +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/survey/Survey.kt b/core/model/src/main/java/com/wap/wapp/core/model/survey/Survey.kt new file mode 100644 index 000000000..3f900b7a5 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/survey/Survey.kt @@ -0,0 +1,50 @@ +package com.wap.wapp.core.model.survey + +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +/* +ํšŒ์›์ด ์ž‘์„ฑํ•˜๋Š” ์„ค๋ฌธ ๋ชจ๋ธ +*/ + +data class Survey( + val surveyId: String, + val surveyFormId: String, + val eventName: String, + val userName: String, + val title: String, + val content: String, + val surveyAnswerList: List, + val surveyedAt: LocalDateTime, +) { + fun calculateSurveyedAt(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + val duration = Duration.between(surveyedAt, currentDateTime) + + if (duration.toMinutes() == 0L) { + return "๋ฐฉ๊ธˆ" + } else if (duration.toMinutes() < 60) { + val leftMinutes = duration.toMinutes().toString() + return leftMinutes + "๋ถ„ ์ „" + } + + if (duration.toHours() < 24) { + val leftHours = duration.toHours().toString() + return leftHours + "์‹œ๊ฐ„ ์ „" + } + + if (duration.toDays() < 31) { + val leftDays = duration.toDays().toString() + return leftDays + "์ผ ์ „" + } + + return surveyedAt.format(yyyyMMddFormatter) + } + + companion object { + val yyyyMMddFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + val TIME_ZONE_SEOUL = ZoneId.of("Asia/Seoul") + } +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyAnswer.kt b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyAnswer.kt new file mode 100644 index 000000000..6ff6c5358 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyAnswer.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.core.model.survey + +data class SurveyAnswer( + val questionTitle: String, + val questionAnswer: String, + val questionType: QuestionType, +) diff --git a/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyForm.kt b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyForm.kt new file mode 100644 index 000000000..b2f831847 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyForm.kt @@ -0,0 +1,57 @@ +package com.wap.wapp.core.model.survey + +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +// ์šด์˜์ง„์ด ๋“ฑ๋กํ•˜๋Š” ์„ค๋ฌธ ๋ชจ๋ธ +data class SurveyForm( + val surveyFormId: String, + val eventId: String, + val title: String, + val content: String, + val surveyQuestionList: List, + val deadline: LocalDateTime, +) { + constructor() : this( + "", + "", + "", + "", + emptyList(), + LocalDateTime.MIN, + ) + + fun calculateDeadline(): String { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + val duration = Duration.between(currentDateTime, deadline) + + if (duration.toMinutes() < 60) { + val leftMinutes = duration.toMinutes().toString() + return leftMinutes + "๋ถ„ ํ›„ ๋งˆ๊ฐ" + } + + if (duration.toHours() < 24) { + val leftHours = duration.toHours().toString() + return leftHours + "์‹œ๊ฐ„ ํ›„ ๋งˆ๊ฐ" + } + + if (duration.toDays() < 31) { + val leftDays = duration.toDays().toString() + return leftDays + "์ผ ํ›„ ๋งˆ๊ฐ" + } + + return deadline.format(yyyyMMddFormatter) + " ๋งˆ๊ฐ" + } + + fun isBeforeDeadline(): Boolean { + val currentDateTime = LocalDateTime.now(TIME_ZONE_SEOUL) + return currentDateTime.isBefore(deadline) + } + + companion object { + val yyyyMMddFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + val TIME_ZONE_SEOUL = ZoneId.of("Asia/Seoul") + } +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyQuestion.kt b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyQuestion.kt new file mode 100644 index 000000000..318dc30e5 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/survey/SurveyQuestion.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.model.survey + +data class SurveyQuestion( + val questionTitle: String, + val questionType: QuestionType, +) + +enum class QuestionType { + SUBJECTIVE, OBJECTIVE +} diff --git a/core/model/src/main/java/com/wap/wapp/core/model/user/UserProfile.kt b/core/model/src/main/java/com/wap/wapp/core/model/user/UserProfile.kt new file mode 100644 index 000000000..dfa5cd616 --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/user/UserProfile.kt @@ -0,0 +1,8 @@ +package com.wap.wapp.core.model.user + +data class UserProfile( + val userId: String, + val userName: String, + val studentId: String, + val registeredAt: String, +) diff --git a/core/model/src/main/java/com/wap/wapp/core/model/user/UserRole.kt b/core/model/src/main/java/com/wap/wapp/core/model/user/UserRole.kt new file mode 100644 index 000000000..a159c242d --- /dev/null +++ b/core/model/src/main/java/com/wap/wapp/core/model/user/UserRole.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.model.user + +enum class UserRole { + GUEST, MEMBER, MANAGER +} diff --git a/core/network/.gitignore b/core/network/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 000000000..072c7ddb5 --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("com.wap.wapp.library") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.core.network" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:model")) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.auth) + implementation(libs.firebase.firestore) +} diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/network/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/core/network/src/androidTest/java/com/wap/wapp/core/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/wap/wapp/core/network/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..5a0aedeae --- /dev/null +++ b/core/network/src/androidTest/java/com/wap/wapp/core/network/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.network + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.core.network.test", appContext.packageName) + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/constant/FirebaseConstant.kt b/core/network/src/main/java/com/wap/wapp/core/network/constant/FirebaseConstant.kt new file mode 100644 index 000000000..6b63686b7 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/constant/FirebaseConstant.kt @@ -0,0 +1,13 @@ +package com.wap.wapp.core.network.constant + +/* +Firestore Collection, Document Id +*/ +const val USER_COLLECTION = "users" +const val MANAGER_COLLECTION = "managers" +const val CODES_COLLECTION = "codes" +const val SURVEY_COLLECTION = "surveys" +const val EVENT_COLLECTION = "events" +const val SURVEY_FORM_COLLECTION = "surveyForms" +const val ATTENDANCE_COLLECTION = "attendances" +const val ATTENDANCE_STATUS_COLLECTION = "attendanceStatus" diff --git a/core/network/src/main/java/com/wap/wapp/core/network/di/FirebaseModule.kt b/core/network/src/main/java/com/wap/wapp/core/network/di/FirebaseModule.kt new file mode 100644 index 000000000..d47105c58 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/di/FirebaseModule.kt @@ -0,0 +1,24 @@ +package com.wap.wapp.core.network.di + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.ktx.auth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FirebaseModule { + @Provides + @Singleton + fun providesFirebaseAuth(): FirebaseAuth = Firebase.auth + + @Provides + @Singleton + fun providesFirestore(): FirebaseFirestore = Firebase.firestore +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/wap/wapp/core/network/di/NetworkModule.kt new file mode 100644 index 000000000..b5ce3ec86 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/di/NetworkModule.kt @@ -0,0 +1,76 @@ +package com.wap.wapp.core.network.di + +import com.wap.wapp.core.network.source.attendance.AttendanceDataSource +import com.wap.wapp.core.network.source.attendance.AttendanceDataSourceImpl +import com.wap.wapp.core.network.source.attendancestatus.AttendanceStatusDataSource +import com.wap.wapp.core.network.source.attendancestatus.AttendanceStatusDataSourceImpl +import com.wap.wapp.core.network.source.auth.AuthDataSource +import com.wap.wapp.core.network.source.auth.AuthDataSourceImpl +import com.wap.wapp.core.network.source.event.EventDataSource +import com.wap.wapp.core.network.source.event.EventDataSourceImpl +import com.wap.wapp.core.network.source.management.ManagementDataSource +import com.wap.wapp.core.network.source.management.ManagementDataSourceImpl +import com.wap.wapp.core.network.source.survey.SurveyDataSource +import com.wap.wapp.core.network.source.survey.SurveyDataSourceImpl +import com.wap.wapp.core.network.source.survey.SurveyFormDataSource +import com.wap.wapp.core.network.source.survey.SurveyFormDataSourceImpl +import com.wap.wapp.core.network.source.user.UserDataSource +import com.wap.wapp.core.network.source.user.UserDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class NetworkModule { + + @Binds + @Singleton + abstract fun bindsAuthDataSource( + authDataSourceImpl: AuthDataSourceImpl, + ): AuthDataSource + + @Binds + @Singleton + abstract fun bindsUserDataSource( + userDataSourceImpl: UserDataSourceImpl, + ): UserDataSource + + @Binds + @Singleton + abstract fun bindsManagementDataSource( + managementDataSourceImpl: ManagementDataSourceImpl, + ): ManagementDataSource + + @Binds + @Singleton + abstract fun bindsSurveyDataSoruce( + surveyDataSourceImpl: SurveyDataSourceImpl, + ): SurveyDataSource + + @Binds + @Singleton + abstract fun bindsSurveyFormDateSource( + surveyFormDataSourceImpl: SurveyFormDataSourceImpl, + ): SurveyFormDataSource + + @Binds + @Singleton + abstract fun bindsEventDataSource( + eventDataSourceImpl: EventDataSourceImpl, + ): EventDataSource + + @Binds + @Singleton + abstract fun bindsAttendanceDataSource( + attendanceDataSourceImpl: AttendanceDataSourceImpl, + ): AttendanceDataSource + + @Binds + @Singleton + abstract fun bindsAttendanceStatusDataSource( + attendanceStatueDataSourceImpl: AttendanceStatusDataSourceImpl, + ): AttendanceStatusDataSource +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/di/SignInDataSourceModule.kt b/core/network/src/main/java/com/wap/wapp/core/network/di/SignInDataSourceModule.kt new file mode 100644 index 000000000..29c8acc27 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/di/SignInDataSourceModule.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.network.di + +import com.wap.wapp.core.network.source.auth.SignInDataSource +import com.wap.wapp.core.network.source.auth.SignInDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +abstract class SignInDataSourceModule { + @Binds + @ActivityScoped + abstract fun bindsSignInDataSource( + signInDataSourceImpl: SignInDataSourceImpl, + ): SignInDataSource +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceRequest.kt new file mode 100644 index 000000000..401133cd9 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceRequest.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.core.network.model.attendance + +data class AttendanceRequest( + val eventId: String = "", + val code: String = "", + val deadline: String = "", +) diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceResponse.kt new file mode 100644 index 000000000..7f1497e18 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/attendance/AttendanceResponse.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.network.model.attendance + +import com.wap.wapp.core.model.attendance.Attendance +import com.wap.wapp.core.network.utils.toISOLocalDateTime + +data class AttendanceResponse( + val eventId: String = "", + val code: String = "", + val deadline: String = "", +) { + fun toDomain() = Attendance( + eventId = eventId, + code = code, + deadline = deadline.toISOLocalDateTime(), + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusRequest.kt new file mode 100644 index 000000000..c84bc97e3 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusRequest.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.network.model.attendancestatus + +data class AttendanceStatusRequest( + val attendanceDateTime: String = "", +) diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusResponse.kt new file mode 100644 index 000000000..f464a7006 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/attendancestatus/AttendanceStatusResponse.kt @@ -0,0 +1,9 @@ +package com.wap.wapp.core.network.model.attendancestatus + +import com.wap.wapp.core.model.attendancestatus.AttendanceStatus + +data class AttendanceStatusResponse( + val attendanceDateTime: String = "", +) { + fun toDomain() = AttendanceStatus(attendanceDateTime) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventRequest.kt new file mode 100644 index 000000000..fde307bd2 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventRequest.kt @@ -0,0 +1,10 @@ +package com.wap.wapp.core.network.model.event + +data class EventRequest( + val title: String = "", + val content: String = "", + val location: String = "", + val startDateTime: String = "", + val endDateTime: String = "", + val eventId: String = "", +) diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventResponse.kt new file mode 100644 index 000000000..45a9f74ac --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/event/EventResponse.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.core.network.model.event + +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.network.utils.toISOLocalDateTime + +data class EventResponse( + val content: String = "", + val eventId: String = "", + val location: String = "", + val title: String = "", + val startDateTime: String = "", + val endDateTime: String = "", +) { + fun toDomain() = Event( + content = content, + eventId = eventId, + location = location, + title = title, + startDateTime = startDateTime.toISOLocalDateTime(), + endDateTime = endDateTime.toISOLocalDateTime(), + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyAnswerResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyAnswerResponse.kt new file mode 100644 index 000000000..819d94b0a --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyAnswerResponse.kt @@ -0,0 +1,31 @@ +package com.wap.wapp.core.network.model.survey + +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.core.model.survey.SurveyAnswer + +data class SurveyAnswerResponse( + val questionTitle: String, + val questionAnswer: String, + val questionType: QuestionTypeResponse, +) { + constructor() : this( + "", + "", + QuestionTypeResponse.SUBJECTIVE, + ) + + fun toDomain() = SurveyAnswer( + questionTitle = questionTitle, + questionAnswer = questionAnswer, + questionType = questionType.toDomain(), + ) +} + +enum class QuestionTypeResponse { + OBJECTIVE, SUBJECTIVE +} + +internal fun QuestionTypeResponse.toDomain(): QuestionType = when (this) { + QuestionTypeResponse.SUBJECTIVE -> { QuestionType.SUBJECTIVE } + QuestionTypeResponse.OBJECTIVE -> { QuestionType.OBJECTIVE } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyRequest.kt new file mode 100644 index 000000000..2e97ab2e7 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyRequest.kt @@ -0,0 +1,14 @@ +package com.wap.wapp.core.network.model.survey + +import com.wap.wapp.core.model.survey.SurveyAnswer + +data class SurveyRequest( + val surveyId: String, + val surveyFormId: String, + val eventId: String, + val userId: String, + val title: String, + val content: String, + val surveyAnswerList: List, + val surveyedAt: String, +) diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyResponse.kt new file mode 100644 index 000000000..59d548a9b --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/SurveyResponse.kt @@ -0,0 +1,37 @@ +package com.wap.wapp.core.network.model.survey + +import com.wap.wapp.core.model.survey.Survey +import com.wap.wapp.core.network.utils.toISOLocalDateTime + +data class SurveyResponse( + val surveyId: String, + val surveyFormId: String, + val eventId: String, + val userId: String, + val title: String, + val content: String, + val surveyAnswerList: List, + val surveyedAt: String, +) { + constructor() : this( + "", + "", + "", + "", + "", + "", + emptyList(), + "", + ) + + fun toDomain(eventName: String, userName: String): Survey = Survey( + surveyId = surveyId, + surveyFormId = surveyFormId, + eventName = eventName, + userName = userName, + title = this.title, + content = this.content, + surveyAnswerList = this.surveyAnswerList.map { answer -> answer.toDomain() }, + surveyedAt = surveyedAt.toISOLocalDateTime(), + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormRequest.kt new file mode 100644 index 000000000..a35507e61 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormRequest.kt @@ -0,0 +1,21 @@ +package com.wap.wapp.core.network.model.survey.form + +import com.wap.wapp.core.model.survey.SurveyQuestion + +data class SurveyFormRequest( + val surveyFormId: String, + val eventId: String, + val title: String, + val content: String, + val surveyQuestionList: List, + val deadline: String, +) { + constructor() : this( + "", + "", + "", + "", + emptyList(), + "", + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormResponse.kt new file mode 100644 index 000000000..c29171e68 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyFormResponse.kt @@ -0,0 +1,33 @@ +package com.wap.wapp.core.network.model.survey.form + +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.network.utils.toISOLocalDateTime + +data class SurveyFormResponse( + val surveyFormId: String, + val eventId: String, + val userId: String, + val title: String, + val content: String, + val surveyQuestionList: List, + val deadline: String, +) { + constructor() : this( + "", + "", + "", + "", + "", + emptyList(), + "", + ) + + fun toDomain(): SurveyForm = SurveyForm( + surveyFormId = surveyFormId, + eventId = eventId, + title = title, + content = content, + surveyQuestionList = surveyQuestionList.map { it.toDomain() }, + deadline = deadline.toISOLocalDateTime(), + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyQuestionResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyQuestionResponse.kt new file mode 100644 index 000000000..c919e8ab1 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/survey/form/SurveyQuestionResponse.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.network.model.survey.form + +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.core.network.model.survey.QuestionTypeResponse +import com.wap.wapp.core.network.model.survey.toDomain + +data class SurveyQuestionResponse( + val questionTitle: String, + val questionType: QuestionTypeResponse, +) { + constructor() : this( + "", + QuestionTypeResponse.SUBJECTIVE, + ) + fun toDomain() = SurveyQuestion( + questionTitle = questionTitle, + questionType = questionType.toDomain(), + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileRequest.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileRequest.kt new file mode 100644 index 000000000..59968fd6f --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileRequest.kt @@ -0,0 +1,15 @@ +package com.wap.wapp.core.network.model.user + +data class UserProfileRequest( + val userId: String, + val userName: String, + val studentId: String, + val registeredAt: String, +) { + constructor() : this( + "", + "", + "", + "", + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileResponse.kt b/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileResponse.kt new file mode 100644 index 000000000..deb0f61f1 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/model/user/UserProfileResponse.kt @@ -0,0 +1,24 @@ +package com.wap.wapp.core.network.model.user + +import com.wap.wapp.core.model.user.UserProfile + +data class UserProfileResponse( + val userId: String, + val userName: String, + val studentId: String, + val registeredAt: String, +) { + constructor() : this( + "", + "", + "", + "", + ) + + fun toDomain(): UserProfile = UserProfile( + userId = userId, + userName = userName, + studentId = studentId, + registeredAt = registeredAt, + ) +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSource.kt new file mode 100644 index 000000000..9e8c423ef --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSource.kt @@ -0,0 +1,15 @@ +package com.wap.wapp.core.network.source.attendance + +import com.wap.wapp.core.network.model.attendance.AttendanceResponse + +interface AttendanceDataSource { + suspend fun postAttendance( + eventId: String, + code: String, + deadline: String, + ): Result + + suspend fun getAttendance( + eventId: String, + ): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSourceImpl.kt new file mode 100644 index 000000000..a36c8b28f --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/attendance/AttendanceDataSourceImpl.kt @@ -0,0 +1,46 @@ +package com.wap.wapp.core.network.source.attendance + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.toObject +import com.wap.wapp.core.network.constant.ATTENDANCE_COLLECTION +import com.wap.wapp.core.network.model.attendance.AttendanceRequest +import com.wap.wapp.core.network.model.attendance.AttendanceResponse +import com.wap.wapp.core.network.utils.await +import com.wap.wapp.core.network.utils.toISOLocalDateTimeString +import java.time.LocalDateTime +import javax.inject.Inject + +class AttendanceDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : AttendanceDataSource { + override suspend fun postAttendance( + eventId: String, + code: String, + deadline: String, + ): Result = runCatching { + val attendanceRequest = AttendanceRequest( + eventId = eventId, + code = code, + deadline = deadline, + ) + + firebaseFirestore.collection(ATTENDANCE_COLLECTION) + .document(eventId) + .set(attendanceRequest) + .await() + } + + override suspend fun getAttendance(eventId: String): Result = runCatching { + val task = firebaseFirestore.collection(ATTENDANCE_COLLECTION) + .document(eventId) + .get() + .await() + + val attendanceResponse = task.toObject() + attendanceResponse ?: AttendanceResponse( + eventId = eventId, + "", + LocalDateTime.MIN.toISOLocalDateTimeString(), + ) + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSource.kt new file mode 100644 index 000000000..2cbc40e29 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSource.kt @@ -0,0 +1,12 @@ +package com.wap.wapp.core.network.source.attendancestatus + +import com.wap.wapp.core.network.model.attendancestatus.AttendanceStatusResponse + +interface AttendanceStatusDataSource { + suspend fun getAttendanceStatus( + eventId: String, + userId: String, + ): Result + + suspend fun postAttendanceStatus(eventId: String, userId: String): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSourceImpl.kt new file mode 100644 index 000000000..0c35bf89c --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/attendancestatus/AttendanceStatusDataSourceImpl.kt @@ -0,0 +1,45 @@ +package com.wap.wapp.core.network.source.attendancestatus + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.toObject +import com.wap.wapp.core.network.constant.ATTENDANCE_STATUS_COLLECTION +import com.wap.wapp.core.network.constant.EVENT_COLLECTION +import com.wap.wapp.core.network.model.attendancestatus.AttendanceStatusRequest +import com.wap.wapp.core.network.model.attendancestatus.AttendanceStatusResponse +import com.wap.wapp.core.network.utils.await +import com.wap.wapp.core.network.utils.generateNowDateTime +import com.wap.wapp.core.network.utils.toISOLocalDateTimeString +import javax.inject.Inject + +class AttendanceStatusDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : AttendanceStatusDataSource { + override suspend fun getAttendanceStatus( + eventId: String, + userId: String, + ): Result = runCatching { + val task = firebaseFirestore.collection(ATTENDANCE_STATUS_COLLECTION) + .document(userId) + .collection(EVENT_COLLECTION) + .document(eventId) + .get() + .await() + + // null์ผ ๊ฒฝ์šฐ ์ถœ์„์ด ๋˜์ง€ ์•Š์Œ (์ถœ์„ ์‹œ๊ฐ„์ด empty๋กœ ๋ฐ˜ํ™˜) + val attendanceStatusResponse = task.toObject() + attendanceStatusResponse ?: AttendanceStatusResponse("") + } + + override suspend fun postAttendanceStatus(eventId: String, userId: String): Result = + runCatching { + val attendanceStatusRequest = + AttendanceStatusRequest(generateNowDateTime().toISOLocalDateTimeString()) + + firebaseFirestore.collection(ATTENDANCE_STATUS_COLLECTION) + .document(userId) + .collection(EVENT_COLLECTION) + .document(eventId) + .set(attendanceStatusRequest) + .await() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSource.kt new file mode 100644 index 000000000..06bd34157 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSource.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.network.source.auth + +interface AuthDataSource { + suspend fun signOut(): Result + + suspend fun deleteUser(): Result + + suspend fun isUserSignIn(): Result + + suspend fun checkMemberCode(code: String): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSourceImpl.kt new file mode 100644 index 000000000..84def2902 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/AuthDataSourceImpl.kt @@ -0,0 +1,60 @@ +package com.wap.wapp.core.network.source.auth + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuth.AuthStateListener +import com.google.firebase.firestore.FirebaseFirestore +import com.wap.wapp.core.network.constant.CODES_COLLECTION +import com.wap.wapp.core.network.utils.await +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject + +class AuthDataSourceImpl @Inject constructor( + private val firebaseAuth: FirebaseAuth, + private val firebaseFirestore: FirebaseFirestore, +) : AuthDataSource { + override suspend fun signOut(): Result = runCatching { + firebaseAuth.signOut() + } + + override suspend fun deleteUser(): Result = runCatching { + val user = checkNotNull(firebaseAuth.currentUser) + + user.delete() + .await() + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun isUserSignIn(): Result = runCatching { + suspendCancellableCoroutine { cont -> + val authStateListener = object : AuthStateListener { + override fun onAuthStateChanged(remoteAuth: FirebaseAuth) { + val user = remoteAuth.currentUser + if (user != null) { + cont.resume(true, null) + } else { + cont.resume(false, null) + } + + // ํ•œ ๋ฒˆ ํ˜ธ์ถœ๋œ ํ›„์— Listener ์‚ญ์ œ + firebaseAuth.removeAuthStateListener(this) + } + } + + firebaseAuth.addAuthStateListener(authStateListener) + + cont.invokeOnCancellation { // Coroutine์ด ์ทจ์†Œ๋˜๋Š” ๊ฒฝ์šฐ ๋ฆฌ์Šค๋„ˆ ์‚ญ์ œ + firebaseAuth.removeAuthStateListener(authStateListener) + } + } + } + + override suspend fun checkMemberCode(code: String): Result = runCatching { + val result = firebaseFirestore.collection(CODES_COLLECTION) + .whereEqualTo("user", code) + .get() + .await() + + result.isEmpty.not() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSource.kt new file mode 100644 index 000000000..44191e0a4 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSource.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.core.network.source.auth + +interface SignInDataSource { + suspend fun signIn(email: String): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSourceImpl.kt new file mode 100644 index 000000000..e5ae18f79 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/auth/SignInDataSourceImpl.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.core.network.source.auth + +import android.app.Activity +import android.content.Context +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.OAuthProvider +import com.wap.wapp.core.network.utils.await +import dagger.hilt.android.qualifiers.ActivityContext +import javax.inject.Inject + +class SignInDataSourceImpl @Inject constructor( + private val firebaseAuth: FirebaseAuth, + @ActivityContext private val context: Context, +) : SignInDataSource { + override suspend fun signIn(email: String): Result = runCatching { + val provider = OAuthProvider.newBuilder("github.com") + provider.addCustomParameter("login", email) + + val activityContext = context as Activity + + val result = firebaseAuth.startActivityForSignInWithProvider( + activityContext, + provider.build(), + ).await() + + val user = checkNotNull(result.user) + user.uid + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSource.kt new file mode 100644 index 000000000..f0c4aabbc --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSource.kt @@ -0,0 +1,35 @@ +package com.wap.wapp.core.network.source.event + +import com.wap.wapp.core.network.model.event.EventResponse +import java.time.LocalDate + +interface EventDataSource { + suspend fun getMonthEventList(date: LocalDate): Result> + + suspend fun getDateEventList(date: LocalDate): Result> + + suspend fun getEventList(): Result> + + suspend fun getEventListFromDate(date: LocalDate): Result> + + suspend fun getEvent(eventId: String): Result + + suspend fun deleteEvent(eventId: String): Result + + suspend fun postEvent( + title: String, + content: String, + location: String, + startDateTime: String, + endDateTime: String, + ): Result + + suspend fun updateEvent( + eventId: String, + title: String, + content: String, + location: String, + startDateTime: String, + endDateTime: String, + ): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSourceImpl.kt new file mode 100644 index 000000000..62d4f6a45 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/event/EventDataSourceImpl.kt @@ -0,0 +1,168 @@ +package com.wap.wapp.core.network.source.event + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.toObject +import com.wap.wapp.core.network.constant.EVENT_COLLECTION +import com.wap.wapp.core.network.model.event.EventRequest +import com.wap.wapp.core.network.model.event.EventResponse +import com.wap.wapp.core.network.utils.await +import com.wap.wapp.core.network.utils.generateNowDateTime +import com.wap.wapp.core.network.utils.toISOLocalDateTimeString +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject + +class EventDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : EventDataSource { + override suspend fun getEventList(): Result> = runCatching { + val result = mutableListOf() + + val task = firebaseFirestore.collection(EVENT_COLLECTION) + .get() + .await() + + for (document in task.documents) { + val event = document.toObject() + checkNotNull(event) + result.add(event) + } + + result + } + + override suspend fun getEventListFromDate(date: LocalDate): Result> = + runCatching { + val result = mutableListOf() + + // ์„ ํƒ๋œ ๋‚ ์งœ 1์ผ 00์‹œ 00๋ถ„ 00์ดˆ + val startDateTime = date.atStartOfDay().toISOLocalDateTimeString() + val currentDateTime = generateNowDateTime().toISOLocalDateTimeString() + val task = firebaseFirestore.collection(EVENT_COLLECTION) + .whereGreaterThanOrEqualTo("startDateTime", startDateTime) + .whereLessThanOrEqualTo("startDateTime", currentDateTime) + .get() + .await() + + for (document in task.documents) { + val event = document.toObject() + checkNotNull(event) + result.add(event) + } + + result + } + + override suspend fun getMonthEventList(date: LocalDate): Result> = + runCatching { + val result = mutableListOf() + + // ์„ ํƒ๋œ ๋‚ ์งœ 1์ผ 00์‹œ 00๋ถ„ 00์ดˆ + val startDateTime = LocalDate.of(date.year, date.month, 1).atStartOfDay() + .toISOLocalDateTimeString() + + // ์„ ํƒ๋œ ๋‚ ์งœ์˜ ๋งˆ์ง€๋ง‰ ๋‚ (31์ผ) 23์‹œ 59๋ถ„ 59์ดˆ + val endDateTime = + LocalDate.of(date.year, date.month, date.lengthOfMonth()).atTime(LocalTime.MAX) + .toISOLocalDateTimeString() + + val task = firebaseFirestore.collection(EVENT_COLLECTION) + .whereGreaterThanOrEqualTo("startDateTime", startDateTime) + .whereLessThanOrEqualTo("startDateTime", endDateTime) + .get() + .await() + + for (document in task.documents) { + val event = document.toObject() + checkNotNull(event) + result.add(event) + } + + result + } + + override suspend fun getDateEventList(date: LocalDate): Result> = + runCatching { + val result = mutableListOf() + + val startDateTime = date.atStartOfDay().toISOLocalDateTimeString() + val endDateTime = date.atTime(LocalTime.MAX).toISOLocalDateTimeString() + + val task = firebaseFirestore.collection(EVENT_COLLECTION) + .whereGreaterThanOrEqualTo("startDateTime", startDateTime) + .whereLessThanOrEqualTo("startDateTime", endDateTime) + .get() + .await() + + for (document in task.documents) { + val event = document.toObject() + checkNotNull(event) + result.add(event) + } + + result + } + + override suspend fun getEvent(eventId: String): Result = runCatching { + val document = firebaseFirestore.collection(EVENT_COLLECTION) + .document(eventId) + .get() + .await() + + val eventResponse = document.toObject() + checkNotNull(eventResponse) + } + + override suspend fun deleteEvent(eventId: String): Result = runCatching { + firebaseFirestore.collection(EVENT_COLLECTION) + .document(eventId) + .delete() + .await() + } + + override suspend fun postEvent( + title: String, + content: String, + location: String, + startDateTime: String, + endDateTime: String, + ): Result = runCatching { + val documentId = firebaseFirestore.collection(EVENT_COLLECTION).document().id + + val eventRequest = EventRequest( + title = title, + content = content, + location = location, + startDateTime = startDateTime, + endDateTime = endDateTime, + eventId = documentId, + ) + + firebaseFirestore.collection(EVENT_COLLECTION) + .document(documentId) + .set(eventRequest) + .await() + } + + override suspend fun updateEvent( + eventId: String, + title: String, + content: String, + location: String, + startDateTime: String, + endDateTime: String, + ): Result = runCatching { + val updateData = mapOf( + "title" to title, + "content" to content, + "location" to location, + "startDateTime" to startDateTime, + "endDateTime" to endDateTime, + ) + + firebaseFirestore.collection(EVENT_COLLECTION) + .document(eventId) + .update(updateData) + .await() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSource.kt new file mode 100644 index 000000000..866f164b1 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSource.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.core.network.source.management + +interface ManagementDataSource { + suspend fun isManager(userId: String): Result + + suspend fun postManager(userId: String): Result + + suspend fun checkManagementCode(code: String): Result + + suspend fun deleteManager(userId: String): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSourceImpl.kt new file mode 100644 index 000000000..feb8599ef --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/management/ManagementDataSourceImpl.kt @@ -0,0 +1,47 @@ +package com.wap.wapp.core.network.source.management + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.SetOptions +import com.wap.wapp.core.network.constant.CODES_COLLECTION +import com.wap.wapp.core.network.constant.MANAGER_COLLECTION +import com.wap.wapp.core.network.utils.await +import javax.inject.Inject + +class ManagementDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : ManagementDataSource { + override suspend fun isManager(userId: String): Result = runCatching { + val result = firebaseFirestore.collection(MANAGER_COLLECTION) + .whereEqualTo("userId", userId) + .get() + .await() + + result.isEmpty.not() + } + + override suspend fun postManager(userId: String): Result = runCatching { + val userIdMap = mapOf("userId" to userId) + val setOption = SetOptions.merge() + + firebaseFirestore.collection(MANAGER_COLLECTION) + .document(userId) + .set(userIdMap, setOption) + .await() + } + + override suspend fun checkManagementCode(code: String): Result = runCatching { + val result = firebaseFirestore.collection(CODES_COLLECTION) + .whereEqualTo("management", code) + .get() + .await() + + result.isEmpty.not() + } + + override suspend fun deleteManager(userId: String): Result = runCatching { + firebaseFirestore.collection(MANAGER_COLLECTION) + .document(userId) + .delete() + .await() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSource.kt new file mode 100644 index 000000000..051c410c3 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSource.kt @@ -0,0 +1,33 @@ +package com.wap.wapp.core.network.source.survey + +import com.wap.wapp.core.model.survey.SurveyAnswer +import com.wap.wapp.core.network.model.survey.SurveyResponse + +interface SurveyDataSource { + suspend fun isSubmittedSurvey( + surveyFormId: String, + userId: String, + ): Result + + suspend fun getSurveyList(): Result> + + suspend fun getSurveyListByEventId(eventId: String): Result> + + suspend fun getSurveyListBySurveyFormId(surveyFormId: String): Result> + + suspend fun getUserRespondedSurveyList(userId: String): Result> + + suspend fun getSurvey(surveyId: String): Result + + suspend fun deleteSurvey(surveyId: String): Result + + suspend fun postSurvey( + surveyFormId: String, + eventId: String, + userId: String, + title: String, + content: String, + surveyAnswerList: List, + surveyedAt: String, + ): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSourceImpl.kt new file mode 100644 index 000000000..8cb01873f --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyDataSourceImpl.kt @@ -0,0 +1,148 @@ +package com.wap.wapp.core.network.source.survey + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.SetOptions +import com.wap.wapp.core.model.survey.SurveyAnswer +import com.wap.wapp.core.network.constant.SURVEY_COLLECTION +import com.wap.wapp.core.network.model.survey.SurveyRequest +import com.wap.wapp.core.network.model.survey.SurveyResponse +import com.wap.wapp.core.network.utils.await +import javax.inject.Inject + +class SurveyDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : SurveyDataSource { + override suspend fun getSurveyList(): Result> = runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_COLLECTION) + .get() + .await() + + for (document in task.documents) { + val surveyResponse = document.toObject(SurveyResponse::class.java) + checkNotNull(surveyResponse) + + result.add(surveyResponse) + } + + result + } + + override suspend fun getSurveyListByEventId(eventId: String): Result> = + runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_COLLECTION) + .whereEqualTo("eventId", eventId) + .get() + .await() + + for (document in task.documents) { + val surveyResponse = document.toObject(SurveyResponse::class.java) + checkNotNull(surveyResponse) + + result.add(surveyResponse) + } + + result + } + + override suspend fun getSurveyListBySurveyFormId( + surveyFormId: String, + ): Result> = runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_COLLECTION) + .whereEqualTo("surveyFormId", surveyFormId) + .get() + .await() + + for (document in task.documents) { + val surveyResponse = document.toObject(SurveyResponse::class.java) + checkNotNull(surveyResponse) + + result.add(surveyResponse) + } + + result + } + + override suspend fun getUserRespondedSurveyList(userId: String): Result> = + runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_COLLECTION) + .whereEqualTo("userId", userId) + .get() + .await() + + for (document in task.documents) { + val surveyResponse = document.toObject(SurveyResponse::class.java) + checkNotNull(surveyResponse) + + result.add(surveyResponse) + } + + result + } + + override suspend fun getSurvey(surveyId: String): Result = runCatching { + val result = firebaseFirestore.collection(SURVEY_COLLECTION) + .document(surveyId) + .get() + .await() + + val surveyResponse = result.toObject(SurveyResponse::class.java) + checkNotNull(surveyResponse) + } + + override suspend fun deleteSurvey(surveyId: String): Result = runCatching { + firebaseFirestore.collection(SURVEY_COLLECTION) + .document(surveyId) + .delete() + .await() + } + + override suspend fun postSurvey( + surveyFormId: String, + eventId: String, + userId: String, + title: String, + content: String, + surveyAnswerList: List, + surveyedAt: String, + ): Result = runCatching { + val documentId = firebaseFirestore.collection(SURVEY_COLLECTION).document().id + + val surveyRequest = SurveyRequest( + surveyId = documentId, + surveyFormId = surveyFormId, + eventId = eventId, + userId = userId, + title = title, + content = content, + surveyAnswerList = surveyAnswerList, + surveyedAt = surveyedAt, + ) + val setOption = SetOptions.merge() + + firebaseFirestore.collection(SURVEY_COLLECTION) + .document(documentId) + .set(surveyRequest, setOption) + .await() + } + + override suspend fun isSubmittedSurvey( + surveyFormId: String, + userId: String, + ): Result = runCatching { + val result = firebaseFirestore.collection(SURVEY_COLLECTION) + .whereEqualTo("surveyFormId", surveyFormId) + .whereEqualTo("userId", userId) + .get() + .await() + + result.isEmpty.not() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSource.kt new file mode 100644 index 000000000..63e366978 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSource.kt @@ -0,0 +1,25 @@ +package com.wap.wapp.core.network.source.survey + +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.core.network.model.survey.form.SurveyFormRequest +import com.wap.wapp.core.network.model.survey.form.SurveyFormResponse + +interface SurveyFormDataSource { + suspend fun postSurveyForm( + eventId: String, + title: String, + content: String, + surveyQuestionList: List, + deadline: String, + ): Result + + suspend fun getSurveyForm(surveyFormId: String): Result + + suspend fun getSurveyFormList(): Result> + + suspend fun getSurveyFormListByEventId(eventId: String): Result> + + suspend fun deleteSurveyForm(surveyFormId: String): Result + + suspend fun updateSurveyForm(surveyFormRequest: SurveyFormRequest): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSourceImpl.kt new file mode 100644 index 000000000..ca72a45b6 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/survey/SurveyFormDataSourceImpl.kt @@ -0,0 +1,110 @@ +package com.wap.wapp.core.network.source.survey + +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.SetOptions +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.core.network.constant.SURVEY_FORM_COLLECTION +import com.wap.wapp.core.network.model.survey.form.SurveyFormRequest +import com.wap.wapp.core.network.model.survey.form.SurveyFormResponse +import com.wap.wapp.core.network.utils.await +import javax.inject.Inject + +class SurveyFormDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, +) : SurveyFormDataSource { + override suspend fun getSurveyForm(surveyFormId: String): Result = + runCatching { + val result = firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .document(surveyFormId) + .get() + .await() + + val surveyFormResponse = result.toObject(SurveyFormResponse::class.java) + checkNotNull(surveyFormResponse) + } + + override suspend fun getSurveyFormList(): Result> = runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .get() + .await() + + for (document in task.documents) { + val surveyFormResponse = document.toObject(SurveyFormResponse::class.java) + checkNotNull(surveyFormResponse) + + result.add(surveyFormResponse) + } + + result + } + + override suspend fun getSurveyFormListByEventId( + eventId: String, + ): Result> = runCatching { + val result: MutableList = mutableListOf() + + val task = firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .whereEqualTo("eventId", eventId) + .get() + .await() + + for (document in task.documents) { + val surveyFormResponse = document.toObject(SurveyFormResponse::class.java) + checkNotNull(surveyFormResponse) + + result.add(surveyFormResponse) + } + + result + } + + override suspend fun deleteSurveyForm(surveyFormId: String): Result = runCatching { + firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .document(surveyFormId) + .delete() + .await() + } + + override suspend fun postSurveyForm( + eventId: String, + title: String, + content: String, + surveyQuestionList: List, + deadline: String, + ): Result = runCatching { + val documentId = firebaseFirestore.collection(SURVEY_FORM_COLLECTION).document().id + val surveyFormRequest = SurveyFormRequest( + surveyFormId = documentId, + eventId = eventId, + title = title, + content = content, + surveyQuestionList = surveyQuestionList, + deadline = deadline, + ) + + val setOption = SetOptions.merge() + firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .document(documentId) + .set(surveyFormRequest, setOption) + .await() + } + + override suspend fun updateSurveyForm(surveyFormRequest: SurveyFormRequest): Result = + runCatching { + val surveyFormMap = mapOf( + "surveyFormId" to surveyFormRequest.surveyFormId, + "eventId" to surveyFormRequest.eventId, + "title" to surveyFormRequest.title, + "content" to surveyFormRequest.content, + "surveyQuestionList" to surveyFormRequest.surveyQuestionList, + "deadline" to surveyFormRequest.deadline, + ) + + firebaseFirestore.collection(SURVEY_FORM_COLLECTION) + .document(surveyFormRequest.surveyFormId) + .update(surveyFormMap) + .await() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSource.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSource.kt new file mode 100644 index 000000000..93e48027c --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSource.kt @@ -0,0 +1,14 @@ +package com.wap.wapp.core.network.source.user + +import com.wap.wapp.core.network.model.user.UserProfileRequest +import com.wap.wapp.core.network.model.user.UserProfileResponse + +interface UserDataSource { + suspend fun postUserProfile(userProfileRequest: UserProfileRequest): Result + + suspend fun getUserProfile(userId: String): Result + + suspend fun getUserId(): Result + + suspend fun deleteUserProfile(userId: String): Result +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSourceImpl.kt b/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSourceImpl.kt new file mode 100644 index 000000000..5a334ba4b --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/source/user/UserDataSourceImpl.kt @@ -0,0 +1,54 @@ +package com.wap.wapp.core.network.source.user + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.SetOptions +import com.wap.wapp.core.network.constant.USER_COLLECTION +import com.wap.wapp.core.network.model.user.UserProfileRequest +import com.wap.wapp.core.network.model.user.UserProfileResponse +import com.wap.wapp.core.network.utils.await +import javax.inject.Inject + +class UserDataSourceImpl @Inject constructor( + private val firebaseFirestore: FirebaseFirestore, + private val firebaseAuth: FirebaseAuth, +) : UserDataSource { + override suspend fun postUserProfile( + userProfileRequest: UserProfileRequest, + ): Result = runCatching { + val userId = userProfileRequest.userId + val setOption = SetOptions.merge() + + firebaseFirestore.collection(USER_COLLECTION) + .document(userId) + .set( + userProfileRequest, + setOption, + ) + .await() + } + + override suspend fun getUserId(): Result = runCatching { + val userId = checkNotNull(firebaseAuth.uid) + userId + } + + override suspend fun getUserProfile( + userId: String, + ): Result = runCatching { + val result = firebaseFirestore.collection(USER_COLLECTION) + .document(userId) + .get() + .await() + + val userProfileResponse = result.toObject(UserProfileResponse::class.java) + checkNotNull(userProfileResponse) + } + + override suspend fun deleteUserProfile(userId: String): Result = runCatching { + firebaseFirestore.collection(USER_COLLECTION) + .document(userId) + .delete() + .await() + } +} diff --git a/core/network/src/main/java/com/wap/wapp/core/network/utils/DateUtil.kt b/core/network/src/main/java/com/wap/wapp/core/network/utils/DateUtil.kt new file mode 100644 index 000000000..3847fb853 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/utils/DateUtil.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.core.network.utils + +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +internal fun String.toISOLocalDateTime(): LocalDateTime = LocalDateTime.parse( + this, + DateTimeFormatter.ISO_LOCAL_DATE_TIME, +) + +internal fun LocalDateTime.toISOLocalDateTimeString(): String = + this.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + +internal fun generateNowDateTime(zoneId: ZoneId = ZoneId.of("Asia/Seoul")): LocalDateTime = + LocalDateTime.now(zoneId) diff --git a/core/network/src/main/java/com/wap/wapp/core/network/utils/SuspendCoroutine.kt b/core/network/src/main/java/com/wap/wapp/core/network/utils/SuspendCoroutine.kt new file mode 100644 index 000000000..d3de37a80 --- /dev/null +++ b/core/network/src/main/java/com/wap/wapp/core/network/utils/SuspendCoroutine.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.core.network.utils + +import com.google.android.gms.tasks.Task +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalCoroutinesApi::class) +suspend fun Task.await(): T { + return suspendCancellableCoroutine { cont -> + addOnCompleteListener { + if (it.exception != null) { + cont.resumeWithException(it.exception!!) + } else { + cont.resume(it.result, null) + } + } + } +} diff --git a/feature/attendance/.gitignore b/feature/attendance/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/attendance/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/attendance/build.gradle.kts b/feature/attendance/build.gradle.kts new file mode 100644 index 000000000..7c5edd1f3 --- /dev/null +++ b/feature/attendance/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.attendance" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:designsystem")) + implementation(project(":core:designresource")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/attendance/consumer-rules.pro b/feature/attendance/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/attendance/proguard-rules.pro b/feature/attendance/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/attendance/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/attendance/src/androidTest/java/com/wap/wapp/feature/attendance/ExampleInstrumentedTest.kt b/feature/attendance/src/androidTest/java/com/wap/wapp/feature/attendance/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..5d8dc66e7 --- /dev/null +++ b/feature/attendance/src/androidTest/java/com/wap/wapp/feature/attendance/ExampleInstrumentedTest.kt @@ -0,0 +1,23 @@ +package com.wap.wapp.feature.attendance + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.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("com.wap.wapp.feature.attendance.test", appContext.packageName) + } +} diff --git a/feature/attendance/src/main/AndroidManifest.xml b/feature/attendance/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/feature/attendance/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceContent.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceContent.kt new file mode 100644 index 000000000..a1b5506bc --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceContent.kt @@ -0,0 +1,36 @@ +package com.wap.wapp.feature.attendance + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.wap.wapp.feature.attendance.component.AttendanceItemCard +import com.wap.wapp.feature.attendance.model.EventAttendanceStatus + +@Composable +internal fun AttendanceContent( + eventsAttendanceStatus: List, + onSelectEventId: (String) -> Unit, + onSelectEventTitle: (String) -> Unit, + setAttendanceDialog: () -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + items(items = eventsAttendanceStatus, key = { it.eventId }) { attendanceStatus -> + AttendanceItemCard( + eventTitle = attendanceStatus.title, + eventContent = attendanceStatus.content, + remainAttendanceDateTime = + attendanceStatus.remainAttendanceDateTime, + isAttendance = attendanceStatus.isAttendance, + onSelectItemCard = { + onSelectEventId(attendanceStatus.eventId) + onSelectEventTitle(attendanceStatus.title) + setAttendanceDialog() + }, + ) + } + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceScreen.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceScreen.kt new file mode 100644 index 000000000..d5427ae9e --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceScreen.kt @@ -0,0 +1,227 @@ +package com.wap.wapp.feature.attendance + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getString +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.NothingToShow +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappLeftMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.model.user.UserRole +import com.wap.wapp.feature.attendance.AttendanceViewModel.AttendanceEvent.Failure +import com.wap.wapp.feature.attendance.AttendanceViewModel.AttendanceEvent.Success +import com.wap.wapp.feature.attendance.AttendanceViewModel.EventAttendanceStatusState +import com.wap.wapp.feature.attendance.AttendanceViewModel.UserRoleState +import com.wap.wapp.feature.attendance.component.AttendanceCheckButton +import com.wap.wapp.feature.attendance.component.AttendanceDialog +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +internal fun AttendanceRoute( + viewModel: AttendanceViewModel = hiltViewModel(), + navigateToSignIn: () -> Unit, + navigateToAttendanceManagement: () -> Unit, +) { + val snackBarHostState = remember { SnackbarHostState() } + val userRoleState by viewModel.userRole.collectAsStateWithLifecycle() + val eventsAttendanceStatusState + by viewModel.todayEventsAttendanceStatus.collectAsStateWithLifecycle() + val attendanceCode by viewModel.attendanceCode.collectAsStateWithLifecycle() + val selectedEventTitle by viewModel.selectedEventTitle.collectAsStateWithLifecycle() + val context = LocalContext.current + + LaunchedEffect(true) { + viewModel.apply { + launch { + errorFlow.collectLatest { throwable -> + snackBarHostState.showSnackbar(message = throwable.toSupportingText()) + } + } + + launch { + attendanceEvent.collect { event -> + when (event) { + is Success -> { + getTodayEventsAttendanceStatus() + snackBarHostState.showSnackbar( + getString(context, R.string.attendance_success), + ) + } + + is Failure -> snackBarHostState.showSnackbar(event.message) + } + } + } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + when (userRoleState) { + is UserRoleState.Loading -> CircleLoader(modifier = Modifier.fillMaxSize()) + is UserRoleState.Success -> { + when ((userRoleState as UserRoleState.Success).userRole) { + UserRole.GUEST -> AttendanceGuestScreen(onButtonClicked = navigateToSignIn) + UserRole.MANAGER, UserRole.MEMBER -> + AttendanceScreen( + userRole = (userRoleState as UserRoleState.Success).userRole, + snackBarHostState = snackBarHostState, + eventsAttendanceStatusState = eventsAttendanceStatusState, + attendanceCode = attendanceCode, + selectedEventTitle = selectedEventTitle, + clearAttendanceCode = viewModel::clearAttendanceCode, + onAttendanceCodeChanged = viewModel::setAttendanceCode, + onSelectEventId = viewModel::setSelectedEventId, + onSelectEventTitle = viewModel::setSelectedEventTitle, + verifyAttendanceCode = viewModel::verifyAttendanceCode, + navigateToAttendanceManagement = navigateToAttendanceManagement, + ) + } + } + } + } +} + +@Composable +internal fun AttendanceScreen( + userRole: UserRole, + snackBarHostState: SnackbarHostState, + eventsAttendanceStatusState: EventAttendanceStatusState, + attendanceCode: String, + selectedEventTitle: String, + clearAttendanceCode: () -> Unit, + onAttendanceCodeChanged: (String) -> Unit, + onSelectEventId: (String) -> Unit, + onSelectEventTitle: (String) -> Unit, + verifyAttendanceCode: () -> Unit, + navigateToAttendanceManagement: () -> Unit, +) { + var showAttendanceDialog by remember { mutableStateOf(false) } + + if (showAttendanceDialog) { + AttendanceDialog( + attendanceCode = attendanceCode, + eventTitle = selectedEventTitle, + onAttendanceCodeChanged = onAttendanceCodeChanged, + onDismissRequest = { showAttendanceDialog = false }, + onConfirmRequest = verifyAttendanceCode, + ) + } + + Scaffold( + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + WappLeftMainTopBar( + titleRes = R.string.attendance, + contentRes = R.string.attendance_content, + ) + when (eventsAttendanceStatusState) { + is EventAttendanceStatusState.Loading -> + CircleLoader(Modifier.fillMaxSize()) + + is EventAttendanceStatusState.Success -> { + Box { + if (eventsAttendanceStatusState.events.isEmpty()) { + NothingToShow(title = R.string.no_events_to_attendance) + } else { + AttendanceContent( + eventsAttendanceStatus = + eventsAttendanceStatusState.events, + onSelectEventId = onSelectEventId, + onSelectEventTitle = onSelectEventTitle, + setAttendanceDialog = { + clearAttendanceCode() + showAttendanceDialog = true + }, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 15.dp), + ) + } + if (userRole == UserRole.MANAGER) { + AttendanceCheckButton( + onAttendanceCheckButtonClicked = navigateToAttendanceManagement, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 16.dp, bottom = 16.dp), + ) + } + } + } + } + } + } +} + +@Composable +internal fun AttendanceGuestScreen( + onButtonClicked: () -> Unit, +) { + Surface( + color = WappTheme.colors.backgroundBlack, + modifier = Modifier.fillMaxSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(R.string.attendance_guset_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Text( + text = stringResource(R.string.attendance_guest_content), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + + WappButton( + textRes = R.string.go_to_signin, + onClick = onButtonClicked, + modifier = Modifier.padding(horizontal = 32.dp), + ) + } + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceViewModel.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceViewModel.kt new file mode 100644 index 000000000..bba29007f --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/AttendanceViewModel.kt @@ -0,0 +1,199 @@ +package com.wap.wapp.feature.attendance + +import androidx.core.text.isDigitsOnly +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.attendance.GetEventListAttendanceUseCase +import com.wap.wapp.core.domain.usecase.attendance.ValidationAttendanceCodeUseCase +import com.wap.wapp.core.domain.usecase.attendancestatus.GetEventListAttendanceStatusUseCase +import com.wap.wapp.core.domain.usecase.attendancestatus.PostAttendanceStatusUseCase +import com.wap.wapp.core.domain.usecase.event.GetDateEventListUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserProfileUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserRoleUseCase +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.user.UserProfile +import com.wap.wapp.core.model.user.UserRole +import com.wap.wapp.feature.attendance.model.EventAttendanceStatus +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +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 +import javax.inject.Inject + +@HiltViewModel +class AttendanceViewModel @Inject constructor( + private val getDateEventListUseCase: GetDateEventListUseCase, + private val getEventListAttendanceUseCase: GetEventListAttendanceUseCase, + private val getEventListAttendanceStatusUseCase: GetEventListAttendanceStatusUseCase, + private val getUserRoleUseCase: GetUserRoleUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, + private val postAttendanceStatusUseCase: PostAttendanceStatusUseCase, + private val validationAttendanceCodeUseCase: ValidationAttendanceCodeUseCase, +) : ViewModel() { + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow: SharedFlow = _errorFlow.asSharedFlow() + + private val _attendanceEvent: MutableSharedFlow = MutableSharedFlow() + val attendanceEvent = _attendanceEvent.asSharedFlow() + + private val _userProfile = MutableStateFlow(DEFAULT_USER_PROFILE) + val userProfile: StateFlow = _userProfile.asStateFlow() + + private val _userRole = MutableStateFlow(UserRoleState.Loading) + val userRole: StateFlow = _userRole.asStateFlow() + + private val _todayEventsAttendanceStatus = + MutableStateFlow(EventAttendanceStatusState.Loading) + val todayEventsAttendanceStatus: StateFlow = + _todayEventsAttendanceStatus.asStateFlow() + + private val _attendanceCode = MutableStateFlow("") + val attendanceCode: StateFlow = _attendanceCode.asStateFlow() + + private val _selectedEventId = MutableStateFlow("") + + private val _selectedEventTitle = MutableStateFlow("") + val selectedEventTitle: StateFlow = _selectedEventTitle.asStateFlow() + + init { + checkUserInformationAndGetEvents() + } + + private fun checkUserInformationAndGetEvents() = viewModelScope.launch { + getUserRoleUseCase().onSuccess { userRole -> + when (userRole) { + UserRole.GUEST -> _userRole.value = UserRoleState.Success(userRole) + + // ์ผ๋ฐ˜ ํšŒ์› ํ˜น์€ ์šด์˜์ง„ ์ผ ๊ฒฝ์šฐ, + // ์œ ์ € ํ”„๋กœํ•„์„ ๋ฐ›์•„์˜ค๊ณ , ํ•ด๋‹น ์œ ์ € ์•„์ด๋””๋ฅผ ์ด์šฉํ•ด์„œ ์ถœ์„ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ด + UserRole.MEMBER, UserRole.MANAGER -> { + async { getUserProfileUseCase() }.await().onSuccess { + _userRole.value = UserRoleState.Success(userRole) + _userProfile.value = it + launch { getTodayEventsAttendanceStatus() } + }.onFailure { exception -> _errorFlow.emit(exception) } + } + } + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + fun getTodayEventsAttendanceStatus() = viewModelScope.launch { + getDateEventListUseCase(DateUtil.generateNowDate()).onSuccess { eventList -> + getEventListAttendance(eventList = eventList, userId = _userProfile.value.userId) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + // ์˜ค๋Š˜ ์žˆ๋Š” ์ผ์ •์„ ๊ธฐ์ค€์œผ๋กœ, ์ถœ์„์ด ์‹œ์ž‘๋œ ์ผ์ •๋“ค์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + private suspend fun getEventListAttendance(eventList: List, userId: String) = + getEventListAttendanceUseCase(eventList.map { it.eventId }) + .onSuccess { attendanceList -> + val filteredEventList = eventList.filter { + it.eventId in attendanceList.map { it.eventId } + } + + val eventAttendanceList = filteredEventList.zip(attendanceList) + .map { (event, attendance) -> + EventAttendanceStatus( + eventId = event.eventId, + title = event.title, + content = event.content, + remainAttendanceDateTime = attendance.calculateDeadline(), + ) + } + + getEventListAttendanceStatus( + eventAttendanceList = eventAttendanceList, + userId = userId, + ) + }.onFailure { exception -> _errorFlow.emit(exception) } + + // ์ถœ์„์ด ์‹œ์ž‘๋œ ์ผ์ •๋“ค ์ค‘, ์œ ์ €๊ฐ€ ์ถœ์„์„ ํ–ˆ๋Š” ์ง€ ์•ˆํ–ˆ๋Š” ์ง€๋ฅผ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + private suspend fun getEventListAttendanceStatus( + eventAttendanceList: List, + userId: String, + ) = getEventListAttendanceStatusUseCase( + eventIdList = eventAttendanceList.map { it.eventId }, + userId = userId, + ).onSuccess { attendanceStatusList -> + val eventAttendanceStatusList = eventAttendanceList + .zip(attendanceStatusList) + .map { (eventAttendanceStatus, attendanceStatus) -> + EventAttendanceStatus( + eventId = eventAttendanceStatus.eventId, + title = eventAttendanceStatus.title, + content = eventAttendanceStatus.content, + remainAttendanceDateTime = eventAttendanceStatus.remainAttendanceDateTime, + isAttendance = attendanceStatus.isAttendance(), + ) + } + + _todayEventsAttendanceStatus.value = + EventAttendanceStatusState.Success(eventAttendanceStatusList) + }.onFailure { _errorFlow.emit(it) } + + fun verifyAttendanceCode() = viewModelScope.launch { + validationAttendanceCodeUseCase( + eventId = _selectedEventId.value, + attendanceCode = _attendanceCode.value, + ).onSuccess { result -> + // ์ถœ์„์— ์„ฑ๊ณตํ–ˆ์„ ๊ฒฝ์šฐ + if (result) { + postAttendanceStatusUseCase( + eventId = _selectedEventId.value, + userId = _userProfile.value.userId, + ) + .onSuccess { + _todayEventsAttendanceStatus.value = EventAttendanceStatusState.Loading + _attendanceEvent.emit(AttendanceEvent.Success) + } + .onFailure { exception -> _errorFlow.emit(exception) } + return@launch + } + // ์ถœ์„์— ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ + _attendanceEvent.emit(AttendanceEvent.Failure("์ถœ์„ ์ฝ”๋“œ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + fun setAttendanceCode(attendanceCode: String) { + if (attendanceCode.isDigitsOnly()) { + _attendanceCode.value = attendanceCode + } + } + + fun clearAttendanceCode() { + _attendanceCode.value = "" + } + + fun setSelectedEventId(eventId: String) { + _selectedEventId.value = eventId + } + + fun setSelectedEventTitle(eventTitle: String) { + _selectedEventTitle.value = eventTitle + } + + sealed class EventAttendanceStatusState { + data object Loading : EventAttendanceStatusState() + data class Success(val events: List) : EventAttendanceStatusState() + } + + sealed class UserRoleState { + data object Loading : UserRoleState() + data class Success(val userRole: UserRole) : UserRoleState() + } + + sealed class AttendanceEvent { + data object Success : AttendanceEvent() + data class Failure(val message: String) : AttendanceEvent() + } + + companion object { + val DEFAULT_USER_PROFILE = UserProfile("", "", "", "") + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceCheckButton.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceCheckButton.kt new file mode 100644 index 000000000..f93c2cabd --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceCheckButton.kt @@ -0,0 +1,40 @@ +package com.wap.wapp.feature.attendance.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +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 com.wap.designsystem.WappTheme +import com.wap.wapp.feature.attendance.R + +@Composable +internal fun AttendanceCheckButton( + onAttendanceCheckButtonClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + ElevatedButton( + modifier = modifier.height(48.dp), + onClick = { onAttendanceCheckButtonClicked() }, + colors = ButtonDefaults.buttonColors( + contentColor = WappTheme.colors.black, + containerColor = WappTheme.colors.yellow34, + disabledContentColor = WappTheme.colors.white, + disabledContainerColor = WappTheme.colors.grayA2, + ), + shape = RoundedCornerShape(8.dp), + content = { + Row { + Text( + text = stringResource(R.string.go_to_management_attendance_code), + style = WappTheme.typography.contentRegular, + ) + } + }, + ) +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceDialog.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceDialog.kt new file mode 100644 index 000000000..30bd39f33 --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceDialog.kt @@ -0,0 +1,166 @@ +package com.wap.wapp.feature.attendance.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.attendance.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun AttendanceDialog( + attendanceCode: String, + eventTitle: String, + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit, + onAttendanceCodeChanged: (String) -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(R.string.attendance), + style = WappTheme.typography.contentBold.copy(fontSize = 20.sp), + color = WappTheme.colors.yellow34, + modifier = Modifier.padding(top = 16.dp), + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Text( + text = generateDialogContentString(eventTitle), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + OutlinedTextField( + value = attendanceCode, + onValueChange = onAttendanceCodeChanged, + textStyle = WappTheme.typography.titleRegular.copy(textAlign = TextAlign.Center), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = WappTheme.colors.white, + focusedBorderColor = WappTheme.colors.yellow34, + unfocusedBorderColor = WappTheme.colors.yellow34, + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 16.dp), + ) { + Button( + onClick = { + onConfirmRequest() + onDismissRequest() + }, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.complete), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.black, + ) + } + + Button( + onClick = onDismissRequest, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.black25, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .weight(1f) + .border( + width = 1.dp, + color = WappTheme.colors.yellow34, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = stringResource(R.string.cancel), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.yellow34, + ) + } + } + } + } +} + +@Composable +private fun generateDialogContentString(eventTitle: String) = buildAnnotatedString { + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append(eventTitle) + } + append(" ์ผ์ •์— ์ถœ์„ํ•ฉ๋‹ˆ๋‹ค.") +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceItemCard.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceItemCard.kt new file mode 100644 index 000000000..dab927ad7 --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/component/AttendanceItemCard.kt @@ -0,0 +1,77 @@ +package com.wap.wapp.feature.attendance.component + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.attendance.R + +@Composable +internal fun AttendanceItemCard( + eventTitle: String, + eventContent: String, + remainAttendanceDateTime: String, + isAttendance: Boolean, + onSelectItemCard: () -> Unit = {}, +) { + val cardModifier = if (isAttendance) { + Modifier.fillMaxWidth() + } else { + Modifier + .fillMaxWidth() + .clickable { onSelectItemCard() } + } + Card( + colors = CardDefaults.cardColors(containerColor = WappTheme.colors.black25), + modifier = cardModifier, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = eventTitle, + color = WappTheme.colors.white, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = WappTheme.typography.titleBold, + ) + if (isAttendance) { + Text( + text = stringResource(id = R.string.attendance_complete), + color = WappTheme.colors.greenAB, + style = WappTheme.typography.captionMedium, + ) + } else { + Text( + text = remainAttendanceDateTime, + color = WappTheme.colors.yellow34, + style = WappTheme.typography.captionMedium, + ) + } + } + + Text( + text = eventContent, + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentMedium, + ) + } + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementScreen.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementScreen.kt new file mode 100644 index 000000000..ed741d19e --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementScreen.kt @@ -0,0 +1,157 @@ +package com.wap.wapp.feature.attendance.management + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.NothingToShow +import com.wap.designsystem.component.WappRightMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.feature.attendance.R +import com.wap.wapp.feature.attendance.management.AttendanceManagementViewModel.AttendanceManagementEvent +import com.wap.wapp.feature.attendance.management.AttendanceManagementViewModel.EventsState +import com.wap.wapp.feature.attendance.management.component.AttendanceItemCard +import com.wap.wapp.feature.attendance.management.component.AttendanceManagementDialog +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +internal fun AttendanceManagementRoute( + viewModel: AttendanceManagementViewModel = hiltViewModel(), + navigateToAttendance: () -> Unit, +) { + val snackBarHostState = remember { SnackbarHostState() } + val eventsState by viewModel.todayEvents.collectAsStateWithLifecycle() + val attendanceCode by viewModel.attendanceCode.collectAsStateWithLifecycle() + val selectedEventTitle by viewModel.selectedEventTitle.collectAsStateWithLifecycle() + + LaunchedEffect(true) { + launch { + viewModel.errorFlow.collectLatest { throwable -> + snackBarHostState.showSnackbar( + message = throwable.toSupportingText(), + ) + } + } + + launch { + viewModel.attendanceManagementEvent.collect { event -> + when (event) { + is AttendanceManagementEvent.Success -> navigateToAttendance() + + is AttendanceManagementEvent.Failure -> + snackBarHostState.showSnackbar(message = event.message) + } + } + } + } + + AttendanceManagementScreen( + snackBarHostState = snackBarHostState, + eventsState = eventsState, + attendanceCode = attendanceCode, + selectedEventTitle = selectedEventTitle, + postAttendance = viewModel::postAttendance, + clearAttendanceCode = viewModel::clearAttendanceCode, + onAttendanceCodeChanged = viewModel::setAttendanceCode, + onSelectEventId = viewModel::setSelectedEventId, + onSelectEventTitle = viewModel::setSelectedEventTitle, + navigateToAttendance = navigateToAttendance, + ) +} + +@Composable +internal fun AttendanceManagementScreen( + snackBarHostState: SnackbarHostState, + eventsState: EventsState, + attendanceCode: String, + selectedEventTitle: String, + postAttendance: () -> Unit, + clearAttendanceCode: () -> Unit, + onAttendanceCodeChanged: (String) -> Unit, + onSelectEventTitle: (String) -> Unit, + onSelectEventId: (String) -> Unit, + navigateToAttendance: () -> Unit, +) { + var showAttendanceManagementDialog by remember { mutableStateOf(false) } + + if (showAttendanceManagementDialog) { + AttendanceManagementDialog( + attendanceCode = attendanceCode, + selectedEventTitle = selectedEventTitle, + onConfirmRequest = postAttendance, + onDismissRequest = { showAttendanceManagementDialog = false }, + onAttendanceCodeChanged = onAttendanceCodeChanged, + ) + } + + Scaffold( + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + Column(modifier = Modifier.fillMaxSize()) { + WappRightMainTopBar( + titleRes = R.string.management_attendance, + contentRes = R.string.management_attendance_content, + showBackButton = true, + onClickBackButton = navigateToAttendance, + ) + when (eventsState) { + is EventsState.Loading -> CircleLoader( + modifier = Modifier.fillMaxSize(), + ) + + is EventsState.Success -> { + if (eventsState.events.isEmpty()) { + NothingToShow(title = R.string.no_events_to_manage_attendance) + return@Column + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp) + .weight(1f), + ) { + items( + items = eventsState.events, + key = { event -> event.eventId }, + ) { event -> + AttendanceItemCard( + event = event, + onSelectItemCard = { + clearAttendanceCode() + onSelectEventId(event.eventId) + onSelectEventTitle(event.title) + showAttendanceManagementDialog = true + }, + ) + } + } + } + } + } + } + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementViewModel.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementViewModel.kt new file mode 100644 index 000000000..30432a920 --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/AttendanceManagementViewModel.kt @@ -0,0 +1,93 @@ +package com.wap.wapp.feature.attendance.management + +import androidx.core.text.isDigitsOnly +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.commmon.util.DateUtil.generateNowDateTime +import com.wap.wapp.core.domain.usecase.attendance.PostAttendanceUseCase +import com.wap.wapp.core.domain.usecase.event.GetDateEventListUseCase +import com.wap.wapp.core.model.event.Event +import dagger.hilt.android.lifecycle.HiltViewModel +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 +import javax.inject.Inject + +@HiltViewModel +class AttendanceManagementViewModel @Inject constructor( + private val getDateEventListUseCase: GetDateEventListUseCase, + private val postAttendanceUseCase: PostAttendanceUseCase, +) : ViewModel() { + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow: SharedFlow = _errorFlow.asSharedFlow() + + private val _attendanceManagementEvent: MutableSharedFlow = + MutableSharedFlow() + val attendanceManagementEvent = _attendanceManagementEvent.asSharedFlow() + + private val _todayEventList = MutableStateFlow(EventsState.Loading) + val todayEvents: StateFlow = _todayEventList.asStateFlow() + + private val _attendanceCode = MutableStateFlow("") + val attendanceCode: StateFlow = _attendanceCode.asStateFlow() + + private val selectedEventId = MutableStateFlow("") + + private val _selectedEventTitle = MutableStateFlow("") + val selectedEventTitle: StateFlow = _selectedEventTitle.asStateFlow() + + init { + getTodayDateEvents() + } + + private fun getTodayDateEvents() = viewModelScope.launch { + getDateEventListUseCase(DateUtil.generateNowDate()).onSuccess { eventList -> + val unfinishedEventList = eventList.filter { it.isBeforeEndTime() } + _todayEventList.value = EventsState.Success(unfinishedEventList) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + fun postAttendance() = viewModelScope.launch { + if (_attendanceCode.value.isEmpty()) { + _attendanceManagementEvent.emit( + AttendanceManagementEvent.Failure("์ถœ์„ ์ฝ”๋“œ๋Š” ๊ณต๋ž€์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + ) + return@launch + } + + postAttendanceUseCase( + eventId = selectedEventId.value, + code = _attendanceCode.value, + deadline = generateNowDateTime().plusMinutes(10), + ).onSuccess { + _attendanceManagementEvent.emit(AttendanceManagementEvent.Success) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + fun setAttendanceCode(attendanceCode: String) { + if (attendanceCode.isDigitsOnly()) { + _attendanceCode.value = attendanceCode + } + } + + fun clearAttendanceCode() { _attendanceCode.value = "" } + + fun setSelectedEventId(eventId: String) { selectedEventId.value = eventId } + + fun setSelectedEventTitle(eventTitle: String) { _selectedEventTitle.value = eventTitle } + + sealed class EventsState { + data object Loading : EventsState() + data class Success(val events: List) : EventsState() + } + + sealed class AttendanceManagementEvent { + data object Success : AttendanceManagementEvent() + data class Failure(val message: String) : AttendanceManagementEvent() + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceItemCard.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceItemCard.kt new file mode 100644 index 000000000..8c5d5f5d8 --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceItemCard.kt @@ -0,0 +1,75 @@ +package com.wap.wapp.feature.attendance.management.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.designresource.R.drawable.ic_forward_yellow +import com.wap.wapp.core.model.event.Event + +@Composable +internal fun AttendanceItemCard( + event: Event, + onSelectItemCard: () -> Unit = {}, +) { + Card( + colors = CardDefaults.cardColors(containerColor = WappTheme.colors.black25), + modifier = Modifier + .fillMaxWidth() + .clickable { onSelectItemCard() }, + ) { + Box(modifier = Modifier.padding(16.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = event.title, + color = WappTheme.colors.white, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = WappTheme.typography.titleBold, + ) + + Text( + text = event.getCalculatedTime(), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.captionMedium, + ) + } + + Text( + text = event.content, + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentMedium, + ) + } + + Image( + painter = painterResource(id = ic_forward_yellow), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 12.dp), + ) + } + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceManagementDialog.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceManagementDialog.kt new file mode 100644 index 000000000..cddf754f6 --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/component/AttendanceManagementDialog.kt @@ -0,0 +1,195 @@ +package com.wap.wapp.feature.attendance.management.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.attendance.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun AttendanceManagementDialog( + attendanceCode: String, + selectedEventTitle: String, + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit, + onAttendanceCodeChanged: (String) -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(R.string.set_attendance_code), + style = WappTheme.typography.contentBold.copy(fontSize = 20.sp), + color = WappTheme.colors.yellow34, + modifier = Modifier.padding(top = 16.dp), + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Text( + text = generateDialogContentString(selectedEventTitle), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + OutlinedTextField( + value = attendanceCode, + onValueChange = onAttendanceCodeChanged, + textStyle = WappTheme.typography.titleRegular.copy(textAlign = TextAlign.Center), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = WappTheme.colors.white, + focusedBorderColor = WappTheme.colors.yellow34, + unfocusedBorderColor = WappTheme.colors.yellow34, + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Text( + text = generateDialogContentString2(), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(start = 12.dp, end = 12.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 16.dp), + ) { + Button( + onClick = { + onConfirmRequest() + onDismissRequest() + }, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.complete), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.black, + ) + } + + Button( + onClick = onDismissRequest, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.black25, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .weight(1f) + .border( + width = 1.dp, + color = WappTheme.colors.yellow34, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = stringResource(R.string.cancel), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.yellow34, + ) + } + } + } + } +} + +@Composable +private fun generateDialogContentString(eventTitle: String) = buildAnnotatedString { + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append(eventTitle) + } + append(" ์ผ์ •์— ์ถœ์„ ์ฝ”๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.") +} + +@Composable +private fun generateDialogContentString2() = buildAnnotatedString { + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append("์™„๋ฃŒ") + } + append("๋ฅผ ๋ˆ„๋ฅด์‹  ํ›„, ") + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append("10๋ถ„๊ฐ„") + } + append(" ์ถœ์„์ด ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.") +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/navigation/AttendanceManagementNavigation.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/navigation/AttendanceManagementNavigation.kt new file mode 100644 index 000000000..29a11a5fb --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/management/navigation/AttendanceManagementNavigation.kt @@ -0,0 +1,19 @@ +package com.wap.wapp.feature.attendance.management.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.attendance.management.AttendanceManagementRoute + +const val attendanceManagementNavigationRoute = "attendance/management" + +fun NavController.navigateToAttendanceManagement(navOptions: NavOptions? = navOptions {}) = + this.navigate(attendanceManagementNavigationRoute, navOptions) + +fun NavGraphBuilder.attendanceManagementScreen(navigateToAttendance: () -> Unit) { + composable(route = attendanceManagementNavigationRoute) { navBackStackEntry -> + AttendanceManagementRoute(navigateToAttendance = navigateToAttendance) + } +} diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/model/EventAttendanceStatus.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/model/EventAttendanceStatus.kt new file mode 100644 index 000000000..c2d6e40dd --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/model/EventAttendanceStatus.kt @@ -0,0 +1,9 @@ +package com.wap.wapp.feature.attendance.model + +data class EventAttendanceStatus( + val content: String, + val eventId: String, + val title: String, + val remainAttendanceDateTime: String, + val isAttendance: Boolean = false, +) diff --git a/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/navigation/AttendanceNavigation.kt b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/navigation/AttendanceNavigation.kt new file mode 100644 index 000000000..a367685ef --- /dev/null +++ b/feature/attendance/src/main/java/com/wap/wapp/feature/attendance/navigation/AttendanceNavigation.kt @@ -0,0 +1,25 @@ +package com.wap.wapp.feature.attendance.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.attendance.AttendanceRoute + +const val attendanceNavigationRoute = "attendance" + +fun NavController.navigateToAttendance(navOptions: NavOptions? = navOptions {}) = + this.navigate(attendanceNavigationRoute, navOptions) + +fun NavGraphBuilder.attendanceScreen( + navigateToSignIn: () -> Unit, + navigateToAttendanceManagement: () -> Unit, +) { + composable(route = attendanceNavigationRoute) { + AttendanceRoute( + navigateToSignIn = navigateToSignIn, + navigateToAttendanceManagement = navigateToAttendanceManagement, + ) + } +} diff --git a/feature/attendance/src/main/res/values/strings.xml b/feature/attendance/src/main/res/values/strings.xml new file mode 100644 index 000000000..e5e651208 --- /dev/null +++ b/feature/attendance/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + ์ถœ์„ + ์ถœ์„ ์™„๋ฃŒ + ์ผ์ •์— ์ถœ์„ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ถœ์„ํ•˜์„ธ์š”! + ์ง€๊ธˆ์€ ์ถœ์„ํ•  ์ผ์ •์ด ์—†์–ด์š”! + ์ถœ์„ ์ฝ”๋“œ ๊ด€๋ฆฌ + ์ถœ์„ ์ฝ”๋“œ ์„ค์ • + ์˜ค๋Š˜ ์žˆ์„ ์ผ์ •์— ๋Œ€ํ•œ ์ถœ์„์ฝ”๋“œ๋ฅผ ์„ค์ •ํ•ด์š”! + ์ง€๊ธˆ ์ถœ์„์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์ •์ด ์—†์–ด์š”! + ์ถœ์„ ์ฝ”๋“œ ๊ด€๋ฆฌ ํ•˜๋Ÿฌ๊ฐ€๊ธฐ + ์™„๋ฃŒ + ์ทจ์†Œ + ์ถœ์„์— ์„ฑ๊ณตํ•˜์…จ์Šต๋‹ˆ๋‹ค! + ์ถœ์„ ์ฝ”๋“œ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. + ์•— ํšŒ์›์ด ์•„๋‹ˆ์‹œ๋„ค์š” ! + ํšŒ์›๋งŒ ์ผ์ •์— ์ถœ์„ํ•  ์ˆ˜ ์žˆ์–ด์š”. + ๋กœ๊ทธ์ธ ํ•˜๋Ÿฌ ๊ฐ€๊ธฐ + diff --git a/feature/attendance/src/test/java/com/wap/wapp/feature/attendance/ExampleUnitTest.kt b/feature/attendance/src/test/java/com/wap/wapp/feature/attendance/ExampleUnitTest.kt new file mode 100644 index 000000000..4b542d0ce --- /dev/null +++ b/feature/attendance/src/test/java/com/wap/wapp/feature/attendance/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.attendance + +import org.junit.Assert.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/feature/auth/.gitignore b/feature/auth/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/auth/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts new file mode 100644 index 000000000..75ae84666 --- /dev/null +++ b/feature/auth/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.auth" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:designsystem")) + implementation(project(":core:designresource")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/auth/consumer-rules.pro b/feature/auth/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/auth/proguard-rules.pro b/feature/auth/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/auth/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/auth/src/androidTest/java/com/wap/wapp/feature/wapp/ExampleInstrumentedTest.kt b/feature/auth/src/androidTest/java/com/wap/wapp/feature/wapp/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..13d8e2c7e --- /dev/null +++ b/feature/auth/src/androidTest/java/com/wap/wapp/feature/wapp/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.wapp + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.feature.wapp.test", appContext.packageName) + } +} diff --git a/feature/auth/src/main/AndroidManifest.xml b/feature/auth/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/auth/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInContent.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInContent.kt new file mode 100644 index 000000000..1ed34624f --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInContent.kt @@ -0,0 +1,136 @@ +package com.wap.wapp.feature.auth.signin + +import androidx.compose.foundation.Image +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.fillMaxSize +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +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.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.designresource.R +import com.wap.wapp.feature.auth.R.string + +@Composable +internal fun SignInContent( + openSignInSheet: () -> Unit, + navigateToNotice: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll(scrollState), + ) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .weight(1f), + ) { + Image( + painter = painterResource(id = R.drawable.img_white_cat), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(width = 230.dp, height = 230.dp), + contentDescription = stringResource(string.wapp_icon_description), + ) + Row( + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Column { + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = stringResource(id = string.application_name), + style = WappTheme.typography.titleBold, + fontSize = 48.sp, + color = WappTheme.colors.white, + ) + } + Text( + text = stringResource(id = string.application_name), + fontSize = 48.sp, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.yellow34, + ) + } + } + ElevatedButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth(), + onClick = { + openSignInSheet() + }, + colors = ButtonDefaults.elevatedButtonColors( + containerColor = WappTheme.colors.white, + ), + shape = RoundedCornerShape(10.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_github), + contentDescription = stringResource( + id = string.sign_in_github_description, + ), + modifier = Modifier.size(40.dp), + tint = WappTheme.colors.black, + ) + Text( + text = stringResource(id = string.sign_in_github_content), + style = WappTheme.typography.contentMedium, + modifier = Modifier.padding(start = 16.dp), + ) + } + + ElevatedButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth() + .padding(top = 20.dp, bottom = 60.dp), + onClick = { + navigateToNotice() + }, + colors = ButtonDefaults.elevatedButtonColors( + containerColor = WappTheme.colors.yellow34, + contentColor = WappTheme.colors.white, + ), + shape = RoundedCornerShape(10.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_balloon), + contentDescription = stringResource( + id = string.sign_in_non_member_description, + ), + modifier = Modifier.size(40.dp), + ) + Text( + text = stringResource(id = string.sign_in_non_member_content), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + modifier = Modifier.padding(start = 16.dp), + ) + } + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInScreen.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInScreen.kt new file mode 100644 index 000000000..8e2ea2160 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/SignInScreen.kt @@ -0,0 +1,203 @@ +package com.wap.wapp.feature.auth.signin + +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.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberBottomSheetScaffoldState +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.domain.model.AuthState +import com.wap.wapp.core.domain.usecase.auth.SignInUseCase +import com.wap.wapp.feature.auth.R +import kotlinx.coroutines.launch + +@Composable +internal fun SignInRoute( + signInUseCase: SignInUseCase, + navigateToSignUp: () -> Unit, + navigateToNotice: () -> Unit, +) { + SignInScreen( + signInUseCase = signInUseCase, + navigateToSignUp = navigateToSignUp, + navigateToNotice = navigateToNotice, + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +internal fun SignInScreen( + signInUseCase: SignInUseCase, + navigateToNotice: () -> Unit, + navigateToSignUp: () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val scaffoldState = rememberBottomSheetScaffoldState() + val snackBarHostState = remember { SnackbarHostState() } + var email by rememberSaveable { mutableStateOf("") } + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + BottomSheetScaffold( + scaffoldState = scaffoldState, + snackbarHost = { SnackbarHost(snackBarHostState) }, + sheetContainerColor = WappTheme.colors.backgroundBlack, + containerColor = WappTheme.colors.backgroundBlack, + sheetPeekHeight = 0.dp, + modifier = Modifier.fillMaxSize(), + sheetContent = { + Column( + modifier = Modifier + .padding(16.dp) + .addFocusCleaner(focusManager), + ) { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.sign_in), + style = WappTheme.typography.contentBold, + color = WappTheme.colors.white, + fontSize = 18.sp, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.sign_in_email), + color = WappTheme.colors.white, + style = WappTheme.typography.labelRegular, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = email, + maxLines = 1, + onValueChange = { email = it }, + placeholder = { + Text( + text = stringResource(id = R.string.sign_in_email_hint), + ) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { keyboardController?.hide() }, + ), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = WappTheme.colors.white, + focusedBorderColor = WappTheme.colors.yellow34, + unfocusedTextColor = WappTheme.colors.white, + ), + ) + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + coroutineScope.launch { + signInUseCase(email = email) + .onSuccess { + when (it) { + AuthState.SIGN_IN -> { + navigateToNotice() + } + + AuthState.SIGN_UP -> { + navigateToSignUp() + } + } + } + .onFailure { throwable -> + snackBarHostState.showSnackbar( + throwable.toSupportingText(), + ) + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + enabled = email.isNotBlank(), + colors = ButtonDefaults.buttonColors( + contentColor = WappTheme.colors.white, + containerColor = WappTheme.colors.yellow34, + disabledContentColor = WappTheme.colors.white, + disabledContainerColor = WappTheme.colors.grayA2, + ), + shape = RoundedCornerShape(10.dp), + ) { + Text( + text = stringResource(id = R.string.done), + style = WappTheme.typography.contentMedium, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Divider( + color = WappTheme.colors.white, + thickness = 1.dp, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.sign_in_find_email), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.yellow34, + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + }, + ) { + SignInContent( + openSignInSheet = { + coroutineScope.launch { + scaffoldState.bottomSheetState.expand() + } + }, + navigateToNotice = { navigateToNotice() }, + modifier = Modifier.addFocusCleaner(focusManager), + ) + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/navigation/SignInNavigation.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/navigation/SignInNavigation.kt new file mode 100644 index 000000000..ee5139235 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signin/navigation/SignInNavigation.kt @@ -0,0 +1,29 @@ +package com.wap.wapp.feature.auth.signin.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.core.domain.usecase.auth.SignInUseCase +import com.wap.wapp.feature.auth.signin.SignInRoute + +const val signInNavigationRoute = "signIn_route" + +fun NavController.navigateToSignIn(navOptions: NavOptions? = navOptions {}) { + this.navigate(signInNavigationRoute, navOptions) +} + +fun NavGraphBuilder.signInScreen( + signInUseCase: SignInUseCase, + navigateToSignUp: () -> Unit, + navigateToNotice: () -> Unit, +) { + composable(route = signInNavigationRoute) { + SignInRoute( + signInUseCase = signInUseCase, + navigateToSignUp = navigateToSignUp, + navigateToNotice = navigateToNotice, + ) + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpChip.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpChip.kt new file mode 100644 index 000000000..8e5c76071 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpChip.kt @@ -0,0 +1,54 @@ +package com.wap.wapp.feature.auth.signup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.auth.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SignUpChip( + selectedItem: String, + onSelected: (String) -> Unit, +) { + val itemsList = listOf( + stringResource(id = R.string.first_semester), + stringResource(id = R.string.second_semester), + ) + LazyRow( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + items(itemsList) { item -> + FilterChip( + modifier = Modifier.padding(horizontal = 6.dp), + selected = (item == selectedItem), + onClick = { onSelected(item) }, + label = { + Text( + text = item, + color = WappTheme.colors.white, + style = WappTheme.typography.contentRegular, + textAlign = TextAlign.Center, + ) + }, + colors = FilterChipDefaults.filterChipColors( + containerColor = WappTheme.colors.black82, + selectedContainerColor = WappTheme.colors.yellow34, + ), + ) + } + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpScreen.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpScreen.kt new file mode 100644 index 000000000..4be640d33 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpScreen.kt @@ -0,0 +1,237 @@ +package com.wap.wapp.feature.auth.signup + +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.WindowInsets +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R +import com.wap.wapp.feature.auth.R.drawable.ic_card +import com.wap.wapp.feature.auth.R.drawable.ic_door +import com.wap.wapp.feature.auth.R.string +import com.wap.wapp.feature.auth.signup.SignUpViewModel.SignUpEvent +import com.wap.wapp.feature.auth.signup.validation.CodeValidationDialog +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun SignUpRoute( + viewModel: SignUpViewModel = hiltViewModel(), + navigateToSignIn: () -> Unit, + navigateToNotice: () -> Unit, +) { + SignUpScreen( + viewModel = viewModel, + navigateToNotice = navigateToNotice, + navigateToSignIn = navigateToSignIn, + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun SignUpScreen( + viewModel: SignUpViewModel, + navigateToNotice: () -> Unit, + navigateToSignIn: () -> Unit, +) { + val snackBarHostState = remember { SnackbarHostState() } + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + var showCodeValidationDialog by remember { mutableStateOf(false) } + + LaunchedEffect(true) { + viewModel.signUpEventFlow.collectLatest { + when (it) { + is SignUpEvent.SignUpSuccess -> navigateToNotice() + + is SignUpEvent.ValidateUserInformationSuccess -> { + showCodeValidationDialog = true + } + + is SignUpEvent.CheckMemberCodeSuccess -> viewModel.postUserProfile() + + is SignUpEvent.Failure -> + snackBarHostState.showSnackbar(message = it.throwable.toSupportingText()) + } + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets(0.dp), + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + ) { paddingValue -> + Column( + modifier = Modifier + .fillMaxSize() + .addFocusCleaner(focusManager) + .padding(paddingValue), + ) { + if (showCodeValidationDialog) { + CodeValidationDialog( + code = viewModel.memberCode.collectAsStateWithLifecycle().value, + setValidationCode = viewModel::setWapMemberCode, + onConfirmRequest = viewModel::checkMemberCode, + onDismissRequest = { showCodeValidationDialog = false }, + isError = viewModel.isError.collectAsStateWithLifecycle().value, + supportingText = + stringResource( + viewModel.errorSupportingText.collectAsStateWithLifecycle().value, + ), + ) + } + + WappSubTopBar( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + titleRes = string.sign_up, + showLeftButton = true, + onClickLeftButton = { navigateToSignIn() }, + ) + + Column(modifier = Modifier.padding(top = 40.dp, start = 20.dp, end = 20.dp)) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = string.sign_up_title), + style = WappTheme.typography.titleBold, + fontSize = 22.sp, + color = WappTheme.colors.white, + ) + + Text( + text = stringResource(id = string.sign_up_content), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.grayA2, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.padding(bottom = 20.dp), + ) { + SignUpTextField( + iconDescription = stringResource(id = string.sign_up_name_description), + text = viewModel.signUpName.collectAsState().value, + title = stringResource(id = string.sign_up_name_title), + onValueChanged = { name -> viewModel.setName(name) }, + hint = stringResource(id = string.sign_up_name_hint), + supportingText = stringResource(id = string.sign_up_name_caption), + icon = R.drawable.ic_profile, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + ) + + SignUpTextField( + iconDescription = + stringResource(id = string.sign_up_student_id_description), + text = viewModel.signUpStudentId.collectAsState().value, + title = stringResource(id = string.sign_up_student_id_title), + onValueChanged = { name -> viewModel.setStudentId(name) }, + hint = stringResource(id = string.sign_up_student_id_hint), + supportingText = stringResource(id = string.sign_up_student_id_caption), + icon = ic_card, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + SignUpTextField( + iconDescription = stringResource( + id = string.sign_up_registered_at_description, + ), + text = viewModel.signUpYear.collectAsState().value, + title = stringResource(id = string.sign_up_registered_at_title), + onValueChanged = { name -> viewModel.setYear(name) }, + hint = stringResource(id = string.sign_up_registered_at_hint), + supportingText = + stringResource(id = string.sign_up_registered_at_caption), + icon = ic_door, + modifier = Modifier.width(150.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + keyboardActions = + KeyboardActions(onDone = { keyboardController?.hide() }), + ) + + SignUpChip( + selectedItem = viewModel.signUpSemester.collectAsState().value, + onSelected = { semester -> viewModel.setSemester(semester) }, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = { viewModel.validateUserInformation() }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + shape = RoundedCornerShape(10.dp), + ) { + Text( + text = stringResource(id = string.done), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + ) + } + } + } + } + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpTextField.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpTextField.kt new file mode 100644 index 000000000..ff6245a16 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpTextField.kt @@ -0,0 +1,81 @@ +package com.wap.wapp.feature.auth.signup + +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +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.unit.dp +import com.wap.designsystem.WappTheme + +@Composable +internal fun SignUpTextField( + iconDescription: String, + title: String, + text: String, + onValueChanged: (String) -> Unit, + hint: String, + supportingText: String, + icon: Int, + keyboardOptions: KeyboardOptions, + keyboardActions: KeyboardActions, + modifier: Modifier, +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = icon), + contentDescription = iconDescription, + tint = WappTheme.colors.white, + modifier = Modifier.size(20.dp), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + color = WappTheme.colors.white, + style = WappTheme.typography.contentBold, + ) + } + + TextField( + value = text, + onValueChange = onValueChanged, + modifier = modifier, + colors = TextFieldDefaults.textFieldColors( + textColor = WappTheme.colors.white, + backgroundColor = WappTheme.colors.backgroundBlack, + focusedIndicatorColor = WappTheme.colors.white, + unfocusedIndicatorColor = WappTheme.colors.white, + ), + maxLines = 1, + placeholder = { + Text(text = hint, color = WappTheme.colors.grayA2) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = supportingText, + color = WappTheme.colors.yellow34, + style = WappTheme.typography.captionRegular, + ) + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpViewModel.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpViewModel.kt new file mode 100644 index 000000000..5f29039e7 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/SignUpViewModel.kt @@ -0,0 +1,111 @@ +package com.wap.wapp.feature.auth.signup + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.model.CodeValidation +import com.wap.wapp.core.domain.usecase.auth.CheckMemberCodeUseCase +import com.wap.wapp.core.domain.usecase.user.PostUserProfileUseCase +import com.wap.wapp.feature.auth.R +import dagger.hilt.android.lifecycle.HiltViewModel +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 +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val postUserProfileUseCase: PostUserProfileUseCase, + private val checkMemberCodeUseCase: CheckMemberCodeUseCase, +) : ViewModel() { + private val _signUpEventFlow = MutableSharedFlow() + val signUpEventFlow: SharedFlow = _signUpEventFlow.asSharedFlow() + + private val _signUpName: MutableStateFlow = MutableStateFlow("") + val signUpName: StateFlow = _signUpName.asStateFlow() + + private val _signUpStudentId: MutableStateFlow = MutableStateFlow("") + val signUpStudentId: StateFlow = _signUpStudentId.asStateFlow() + + private val _signUpYear: MutableStateFlow = MutableStateFlow("") + val signUpYear: StateFlow = _signUpYear.asStateFlow() + + private val _signUpSemester: MutableStateFlow = MutableStateFlow(FIRST_SEMESTER) + val signUpSemester: StateFlow = _signUpSemester.asStateFlow() + + private val _memberCode: MutableStateFlow = MutableStateFlow("") + val memberCode: StateFlow = _memberCode.asStateFlow() + + private val _isError: MutableStateFlow = MutableStateFlow(false) + val isError: StateFlow get() = _isError + + private val _errorSupportingText: MutableStateFlow = + MutableStateFlow(R.string.sign_up_dialog_hint) + val errorSupportingText: StateFlow = _errorSupportingText.asStateFlow() + + fun validateUserInformation() = viewModelScope.launch { + if (!isValidStudentId()) { + _signUpEventFlow.emit( + SignUpEvent.Failure(IllegalStateException("ํ•™๋ฒˆ์€ 9์ž๋ฆฌ๋กœ๋งŒ ์ž…๋ ฅํ•˜์‹ค ์ˆ˜ ์žˆ์–ด์š”!")), + ) + return@launch + } + + _signUpEventFlow.emit(SignUpEvent.ValidateUserInformationSuccess) + } + + fun checkMemberCode() = viewModelScope.launch { + checkMemberCodeUseCase(_memberCode.value).onSuccess { + when (it) { + CodeValidation.VALID -> + _signUpEventFlow.emit(SignUpEvent.CheckMemberCodeSuccess) + + CodeValidation.INVALID -> { + _isError.value = true + _errorSupportingText.value = R.string.sign_up_incorrect_code + } + } + }.onFailure { throwable -> + _isError.value = true + _signUpEventFlow.emit(SignUpEvent.Failure(throwable)) + } + } + + suspend fun postUserProfile() = postUserProfileUseCase( + userName = _signUpName.value, + studentId = _signUpStudentId.value, + registeredAt = "${_signUpYear.value} ${_signUpSemester.value}", + ).onSuccess { + _signUpEventFlow.emit(SignUpEvent.SignUpSuccess) + }.onFailure { throwable -> + _signUpEventFlow.emit(SignUpEvent.Failure(throwable)) + _isError.value = true + } + + fun isValidStudentId(): Boolean = (_signUpStudentId.value.length == STUDENT_ID_LENGTH) + + fun setName(name: String) { _signUpName.value = name } + + fun setStudentId(studentId: String) { _signUpStudentId.value = studentId } + + fun setYear(year: String) { _signUpYear.value = year } + + fun setSemester(semester: String) { _signUpSemester.value = semester } + + fun setWapMemberCode(code: String) { _memberCode.value = code } + + sealed class SignUpEvent { + data object ValidateUserInformationSuccess : SignUpEvent() + data object CheckMemberCodeSuccess : SignUpEvent() + data object SignUpSuccess : SignUpEvent() + data class Failure(val throwable: Throwable) : SignUpEvent() + } + + companion object { + const val FIRST_SEMESTER = "1ํ•™๊ธฐ" + const val STUDENT_ID_LENGTH = 9 + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/navigation/SignUpNavigation.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/navigation/SignUpNavigation.kt new file mode 100644 index 000000000..e6ede36f4 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/navigation/SignUpNavigation.kt @@ -0,0 +1,26 @@ +package com.wap.wapp.feature.auth.signup.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.auth.signup.SignUpRoute + +const val signUpNavigationRoute = "signUp_route" + +fun NavController.navigateToSignUp(navOptions: NavOptions? = navOptions {}) { + this.navigate(signUpNavigationRoute, navOptions) +} + +fun NavGraphBuilder.signUpScreen( + navigateToSignIn: () -> Unit, + navigateToNotice: () -> Unit, +) { + composable(route = signUpNavigationRoute) { + SignUpRoute( + navigateToSignIn = navigateToSignIn, + navigateToNotice = navigateToNotice, + ) + } +} diff --git a/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/validation/CodeValidationDialog.kt b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/validation/CodeValidationDialog.kt new file mode 100644 index 000000000..944503817 --- /dev/null +++ b/feature/auth/src/main/java/com/wap/wapp/feature/auth/signup/validation/CodeValidationDialog.kt @@ -0,0 +1,91 @@ +package com.wap.wapp.feature.auth.signup.validation + +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.padding +import androidx.compose.foundation.layout.wrapContentHeight +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.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappTextField +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.feature.auth.R + +@Composable +internal fun CodeValidationDialog( + code: String, + isError: Boolean, + supportingText: String, + setValidationCode: (String) -> Unit, + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit, +) { + val focusManager = LocalFocusManager.current + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 30.dp, vertical = 20.dp) + .addFocusCleaner(focusManager) + .clip(RoundedCornerShape(10.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(R.string.sign_up_dialog_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 20.dp), + ) + + Text( + text = stringResource(R.string.sign_up_dialog_content), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 10.dp), + ) + + Spacer(modifier = Modifier.padding(vertical = 10.dp)) + + WappTextField( + value = code, + onValueChanged = setValidationCode, + label = R.string.code, + isError = isError, + supportingText = supportingText, + ) + + Spacer(modifier = Modifier.padding(vertical = 10.dp)) + + WappButton( + onClick = onConfirmRequest, + isEnabled = code.isNotBlank(), + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + ) + } + } +} diff --git a/feature/auth/src/main/res/drawable/ic_card.xml b/feature/auth/src/main/res/drawable/ic_card.xml new file mode 100644 index 000000000..70f839a0f --- /dev/null +++ b/feature/auth/src/main/res/drawable/ic_card.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/auth/src/main/res/drawable/ic_door.xml b/feature/auth/src/main/res/drawable/ic_door.xml new file mode 100644 index 000000000..4221f2fbf --- /dev/null +++ b/feature/auth/src/main/res/drawable/ic_door.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml new file mode 100644 index 000000000..c696054e2 --- /dev/null +++ b/feature/auth/src/main/res/values/strings.xml @@ -0,0 +1,38 @@ + + + WAPP + WAPP Icon + Back Icon + ๋กœ๊ทธ์ธ + ์ด๋ฉ”์ผ + WAPP ICON + Github Email + ์ด๋ฉ”์ผ์„ ๊นŒ๋จน์œผ์…จ๋‚˜์š”? + Github ๋กœ๊ทธ์ธ + Github SignIn Icon + ๋น„ํšŒ์›์œผ๋กœ ๋‘˜๋Ÿฌ๋ณด๊ธฐ + Balloon Icon + ํšŒ์›๊ฐ€์ž… + ์ฒ˜์Œ ๊ฐ€์ž…ํ•˜์‹œ๋„ค์š”! + ํšŒ์›๋‹˜์˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์ด๋ฆ„ + ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ํšŒ์› ํ™•์ธ์„ ์œ„ํ•ด, ์‹ค๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + Profile Icon + ํ•™๋ฒˆ + ํ•™๋ฒˆ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ๋™๋ช…์ด์ธ์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด, ํ•„์š”ํ•ด์š”! + Card Icon + ์ž…๋ถ€์‹œ๊ธฐ + ์ž…๋ถ€๋…„๋„ ์ž…๋ ฅ + ํšŒ์›๋‹˜์˜ ๊ธฐ์ˆ˜ ์ •๋ณด๋ฅผ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! + Door Icon + ํšŒ์› ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + WAP ํšŒ์›๋งŒ ํ•ด๋‹น ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š” + Hint : WAP + ์ž˜๋ชป๋œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. + code + 1ํ•™๊ธฐ + 2ํ•™๊ธฐ + ์™„๋ฃŒ + diff --git a/feature/auth/src/test/java/com/wap/wapp/feature/wapp/ExampleUnitTest.kt b/feature/auth/src/test/java/com/wap/wapp/feature/wapp/ExampleUnitTest.kt new file mode 100644 index 000000000..e5c773ca9 --- /dev/null +++ b/feature/auth/src/test/java/com/wap/wapp/feature/wapp/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.wapp + +import org.junit.Assert.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/feature/build/intermediates/ktLint/reporterProviders.bin b/feature/build/intermediates/ktLint/reporterProviders.bin new file mode 100644 index 000000000..11e8bd367 Binary files /dev/null and b/feature/build/intermediates/ktLint/reporterProviders.bin differ diff --git a/feature/build/intermediates/ktLint/reporters.bin b/feature/build/intermediates/ktLint/reporters.bin new file mode 100644 index 000000000..d5a0d6164 Binary files /dev/null and b/feature/build/intermediates/ktLint/reporters.bin differ diff --git a/feature/management-event/.gitignore b/feature/management-event/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/management-event/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/management-event/build.gradle.kts b/feature/management-event/build.gradle.kts new file mode 100644 index 000000000..4813c8cdc --- /dev/null +++ b/feature/management-event/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.management.event" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:designsystem")) + implementation(project(":core:designresource")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/management-event/consumer-rules.pro b/feature/management-event/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/management-event/proguard-rules.pro b/feature/management-event/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/management-event/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/management-event/src/androidTest/java/com/wap/wapp/feature/management/event/ExampleInstrumentedTest.kt b/feature/management-event/src/androidTest/java/com/wap/wapp/feature/management/event/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..87deb4c37 --- /dev/null +++ b/feature/management-event/src/androidTest/java/com/wap/wapp/feature/management/event/ExampleInstrumentedTest.kt @@ -0,0 +1,23 @@ +package com.wap.wapp.feature.management.event + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.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("com.wap.wapp.feature.management.event.test", appContext.packageName) + } +} diff --git a/feature/management-event/src/main/AndroidManifest.xml b/feature/management-event/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/feature/management-event/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/DeadlineCard.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/DeadlineCard.kt new file mode 100644 index 000000000..9f88bec84 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/DeadlineCard.kt @@ -0,0 +1,57 @@ +package com.wap.wapp.feature.management.event.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme + +@Composable +internal fun DeadlineCard( + title: String, + hint: String, + onCardClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Text( + text = title, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + modifier = Modifier.weight(2f), + ) + + Card( + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .weight(3f) + .clickable { onCardClicked() }, + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Text( + text = hint, + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(), + ) + } + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTextField.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTextField.kt new file mode 100644 index 000000000..b3b8bffa3 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTextField.kt @@ -0,0 +1,69 @@ +package com.wap.wapp.feature.management.event.component + +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.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +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.unit.dp +import com.wap.designsystem.WappTheme + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun RegistrationTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + textAlign: TextAlign? = null, + align: Alignment = Alignment.TopStart, + placeholder: String, + singleline: Boolean = true, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = WappTheme.typography.contentMedium.copy( + textAlign = textAlign, + color = WappTheme.colors.white, + ), + cursorBrush = SolidColor(WappTheme.colors.yellow34), + visualTransformation = VisualTransformation.None, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + singleLine = singleline, + modifier = modifier.background( + color = WappTheme.colors.black25, + shape = RoundedCornerShape(10.dp), + ), + ) { innerTextField -> + Box(modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)) { + if (value.isEmpty()) { + Text( + text = placeholder, + color = WappTheme.colors.gray82, + textAlign = TextAlign.Center, + style = WappTheme.typography.contentMedium, + modifier = Modifier.align(align), + ) + } + + Box(modifier = Modifier.align(align)) { + innerTextField() + } + } + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTitle.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTitle.kt new file mode 100644 index 000000000..e352197ce --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/RegistrationTitle.kt @@ -0,0 +1,38 @@ +package com.wap.wapp.feature.management.event.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme + +@Composable +internal fun RegistrationTitle( + title: String, + content: String, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = title, + style = WappTheme.typography.titleBold, + fontSize = 22.sp, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + Text( + text = content, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappDatePickerDialog.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappDatePickerDialog.kt new file mode 100644 index 000000000..5f51ed4bf --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappDatePickerDialog.kt @@ -0,0 +1,303 @@ +package com.wap.wapp.feature.management.event.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.feature.management.event.R +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale + +@Composable +internal fun WappDatePickerDialog( + date: LocalDate, + onDateChanged: (LocalDate) -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Card(shape = RoundedCornerShape(10.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = WappTheme.colors.black25) + .padding(10.dp), + ) { + var selectedDate by remember { mutableStateOf(date) } + +// CalendarHeader๋Š” 2024-01๊ณผ ๊ฐ™์ด ๋…„ - ๋‹ฌ์„ ํ‘œ์‹œํ•ด์ฃผ๊ณ , +// ๋‹ฌ์„ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค. + CalendarHeader( + selectedDate = selectedDate, + onDateSelected = { date -> selectedDate = date }, + ) + +// CalendarBody๋Š” ์›”,ํ™”,์ˆ˜,๋ชฉ,๊ธˆ,ํ† ,์ผ์˜ ์ƒ๋‹จ ์š”์ผ๋“ค์„ ํฌํ•จํ•œ ์บ˜๋ฆฐ๋”๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. + CalendarBody( + selectedDate = selectedDate, + onDateSelected = { date -> selectedDate = date }, + ) + + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 30.dp) + .clickable { onDismissRequest() }, + ) + + Text( + stringResource(R.string.select), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 20.dp) + .clickable { + onDateChanged(selectedDate) + onDismissRequest() + }, + ) + } + } + } + } +} + +@Composable +private fun CalendarHeader( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) = Box(modifier = Modifier.fillMaxWidth()) { + val date = selectedDate.format(DateUtil.yyyyMMddFormatter) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.Center), + ) { + Image( + painter = painterResource(id = com.wap.wapp.core.designresource.R.drawable.ic_back), + contentDescription = stringResource(id = R.string.backMonthArrowContentDescription), + modifier = Modifier + .padding(end = 20.dp) + .clickable { onDateSelected(selectedDate.minusMonths(1)) }, + ) + + Text( + text = date.substring( + DateUtil.YEAR_MONTH_START_INDEX, + DateUtil.YEAR_MONTH_END_INDEX, + ), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + Image( + painter = painterResource(id = com.wap.wapp.core.designresource.R.drawable.ic_forward), + contentDescription = stringResource(id = R.string.forwardMonthArrowContentDescription), + modifier = Modifier + .padding(start = 20.dp) + .clickable { onDateSelected(selectedDate.plusMonths(1)) }, + ) + } +} + +@Composable +private fun CalendarBody( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) { +// CalendarWeekDays๋Š” ์ผ,์›”,ํ™”,์ˆ˜,๋ชฉ,๊ธˆ,ํ† ์ผ๊ณผ ๊ฐ™์€ ์š”์ผ์„ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” Composable์ž…๋‹ˆ๋‹ค. + CalendarWeekDays() + +// ์‹ค์งˆ์ ์ธ ๋™์ ์ธ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. + CalendarMonthItem( + selectedDate = selectedDate, + onDateSelected = onDateSelected, + ) +} + +@Composable +private fun CalendarWeekDays(modifier: Modifier = Modifier) { + Row(modifier = modifier) { + DateUtil.DaysOfWeek.values().forEach { dayOfWeek -> + val textColor = when (dayOfWeek) { + DateUtil.DaysOfWeek.SATURDAY -> WappTheme.colors.blueA3 + DateUtil.DaysOfWeek.SUNDAY -> WappTheme.colors.red + else -> WappTheme.colors.white + } + + Text( + text = dayOfWeek.displayName, + textAlign = TextAlign.Center, + color = textColor, + modifier = Modifier + .weight(1f) + .padding(vertical = 10.dp), + ) + } + } +} + +@Composable +private fun CalendarMonthItem( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(DateUtil.DAYS_IN_WEEK), + modifier = Modifier.fillMaxWidth(), + ) { +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์— ์•ฝ๊ฐ„ ๋ณด์—ฌ์ง€๋Š” ์ง€๋‚œ ๋‹ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val visibleDaysFromLastMonth = calculateVisibleDaysFromLastMonth(selectedDate) + val beforeMonthDaysToShow = generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth, + selectedDate, + ) + itemsIndexed(beforeMonthDaysToShow) { index, day -> + CalendarDayText( + text = day.toString(), + color = getDayColor(index + 1).copy(alpha = ALPHA_DIM), + ) + } + +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val thisMonthLastDate = selectedDate.lengthOfMonth() + val thisMonthFirstDayOfWeek = selectedDate.withDayOfMonth(1).dayOfWeek + val thisMonthDaysToShow: List = (1..thisMonthLastDate).toList() + items(thisMonthDaysToShow) { day -> + val date = selectedDate.withDayOfMonth(day) + val currentLocalDate = LocalDate.of( + selectedDate.year, + selectedDate.month, + day, + ) + + val isSelected = (day == selectedDate.dayOfMonth) + CalendarDayText( + text = DateUtil.ddFormatter.format(date), + color = getDayColor(day + thisMonthFirstDayOfWeek.value), + isSelected = isSelected, + modifier = Modifier.clickable { onDateSelected(currentLocalDate) }, + ) + } + +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์— ์•ฝ๊ฐ„ ๋ณด์—ฌ์ง€๋Š” ๋‹ค์Œ ๋‹ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val remainingDays = + DateUtil.DAYS_IN_WEEK - (visibleDaysFromLastMonth + thisMonthDaysToShow.size) % + DateUtil.DAYS_IN_WEEK + val nextMonthDaysToShow = IntRange(1, remainingDays).toList() + items(nextMonthDaysToShow) { day -> + CalendarDayText( + text = day.toString(), + color = + getDayColor(visibleDaysFromLastMonth + thisMonthDaysToShow.size + day) + .copy(alpha = ALPHA_DIM), + ) + } + } +} + +@Composable +private fun CalendarDayText( + text: String, + color: Color, + isSelected: Boolean = false, + modifier: Modifier = Modifier, +) { + var columnModifier = modifier.padding(5.dp) + + if (isSelected) { + columnModifier = columnModifier.background( + color = WappTheme.colors.gray82.copy(alpha = 0.4F), + shape = RoundedCornerShape(5.dp), + ) + } + + columnModifier = columnModifier.padding(5.dp) + + Column( + modifier = columnModifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + textAlign = TextAlign.Center, + color = color, + ) + } + } +} + +@Composable +private fun getDayColor(day: Int): Color = when (day % DateUtil.DAYS_IN_WEEK) { + SUNDAY -> WappTheme.colors.red + SATURDAY -> WappTheme.colors.blueA3 + else -> WappTheme.colors.white +} + +private fun generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth: Int, + currentDate: LocalDate, +): List { + val beforeMonth = currentDate.minusMonths(1) + val beforeMonthLastDay = beforeMonth.lengthOfMonth() + return IntRange(beforeMonthLastDay - visibleDaysFromLastMonth + 1, beforeMonthLastDay).toList() +} + +private fun calculateVisibleDaysFromLastMonth(currentDate: LocalDate): Int { + val firstDayOfWeek: DayOfWeek = currentDate.withDayOfMonth(1).dayOfWeek + + var count = 0 + for (day in DateUtil.DaysOfWeek.values()) { + if (day.name == firstDayOfWeek.getDisplayName(TextStyle.FULL, Locale.US).uppercase()) { + break + } + count += 1 + } + + return count +} + +private const val ALPHA_DIM = 0.3F +private const val SUNDAY = 1 +private const val SATURDAY = 0 diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappTimePickerDialog.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappTimePickerDialog.kt new file mode 100644 index 000000000..5d478cb67 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/component/WappTimePickerDialog.kt @@ -0,0 +1,91 @@ +package com.wap.wapp.feature.management.event.component + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerDefaults +import androidx.compose.material3.TimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.management.event.R +import java.time.LocalTime + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WappTimePickerDialog( + state: TimePickerState, + onDismissRequest: () -> Unit, + onConfirmButtonClicked: (LocalTime) -> Unit, + onDismissButtonClicked: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Card(shape = RoundedCornerShape(10.dp)) { + Column(modifier = Modifier.background(WappTheme.colors.black25)) { + TimePicker( + state = state, + colors = TimePickerDefaults.colors( + selectorColor = WappTheme.colors.yellow34, + clockDialSelectedContentColor = WappTheme.colors.black25, + clockDialUnselectedContentColor = WappTheme.colors.gray95, + clockDialColor = WappTheme.colors.black25, + periodSelectorBorderColor = Color.Transparent, + timeSelectorSelectedContainerColor = WappTheme.colors.yellow34, + timeSelectorUnselectedContainerColor = WappTheme.colors.black25, + timeSelectorSelectedContentColor = WappTheme.colors.black25, + timeSelectorUnselectedContentColor = WappTheme.colors.gray95, + periodSelectorSelectedContentColor = WappTheme.colors.black25, + periodSelectorUnselectedContentColor = WappTheme.colors.gray95, + periodSelectorSelectedContainerColor = WappTheme.colors.yellow34, + periodSelectorUnselectedContainerColor = WappTheme.colors.black25, + ), + modifier = Modifier.padding(16.dp), + ) + + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 30.dp) + .clickable { onDismissButtonClicked() }, + ) + + Text( + stringResource(R.string.select), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 20.dp) + .clickable { + onConfirmButtonClicked( + LocalTime.of(state.hour, state.minute), + ) + }, + ) + } + } + } + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditScreen.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditScreen.kt new file mode 100644 index 000000000..b3be18447 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditScreen.kt @@ -0,0 +1,402 @@ +package com.wap.wapp.feature.management.event.edit + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +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.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberTimePickerState +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.draw.clip +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.feature.management.event.R +import com.wap.wapp.feature.management.event.edit.EventEditViewModel.EventEditEvent +import com.wap.wapp.feature.management.event.registration.EventRegistrationContent +import com.wap.wapp.feature.management.event.registration.EventRegistrationState +import kotlinx.coroutines.flow.collectLatest +import java.time.LocalDate +import java.time.LocalTime + +@Composable +internal fun EventEditRoute( + eventId: String, + navigateToManagement: () -> Unit, + viewModel: EventEditViewModel = hiltViewModel(), +) { + val snackBarHostState = remember { SnackbarHostState() } + val currentRegistrationState + by viewModel.currentEditState.collectAsStateWithLifecycle() + val title by viewModel.eventTitle.collectAsStateWithLifecycle() + val content by viewModel.eventContent.collectAsStateWithLifecycle() + val location by viewModel.eventLocation.collectAsStateWithLifecycle() + val startDate by viewModel.eventStartDate.collectAsStateWithLifecycle() + val startTime by viewModel.eventStartTime.collectAsStateWithLifecycle() + val endDate by viewModel.eventEndDate.collectAsStateWithLifecycle() + val endTime by viewModel.eventEndTime.collectAsStateWithLifecycle() + val onTitleChanged = viewModel::setEventTitle + val onContentChanged = viewModel::setEventContent + val onLocationChanged = viewModel::setEventLocation + val onStartDateChanged = viewModel::setEventStartDate + val onStartTimeChanged = viewModel::setEventStartTime + val onEndDateChanged = viewModel::setEventEndDate + val onEndTimeChanged = viewModel::setEventEndTime + + LaunchedEffect(true) { + viewModel.getEvent(eventId = eventId) + + viewModel.eventEditEvent.collectLatest { + when (it) { + is EventEditEvent.Failure -> + snackBarHostState.showSnackbar(it.error.toSupportingText()) + + is EventEditEvent.ValidationError -> + snackBarHostState.showSnackbar(it.message) + + is EventEditEvent.EditSuccess -> + navigateToManagement() + + is EventEditEvent.DeleteSuccess -> + navigateToManagement() + } + } + } + + EventEditScreen( + currentEditState = currentRegistrationState, + title = title, + content = content, + location = location, + startDate = startDate, + startTime = startTime, + endDate = endDate, + endTime = endTime, + snackBarHostState = snackBarHostState, + onTitleChanged = onTitleChanged, + onContentChanged = onContentChanged, + onLocationChanged = onLocationChanged, + onStartDateChanged = onStartDateChanged, + onStartTimeChanged = onStartTimeChanged, + onEndDateChanged = onEndDateChanged, + onEndTimeChanged = onEndTimeChanged, + onNextButtonClicked = { currentState, nextState -> + if (viewModel.validateEvent(currentState)) { + viewModel.setEventRegistrationState(nextState) + } + }, + onCloseButtonClicked = navigateToManagement, + onPreviousButtonClicked = viewModel::setEventRegistrationState, + deleteEvent = viewModel::deleteEvent, + onEditButtonClicked = { lastState -> + if (viewModel.validateEvent(lastState)) { + viewModel.updateEvent() + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun EventEditScreen( + currentEditState: EventRegistrationState, + title: String, + content: String, + location: String, + startDate: LocalDate, + startTime: LocalTime, + endDate: LocalDate, + endTime: LocalTime, + snackBarHostState: SnackbarHostState, + onTitleChanged: (String) -> Unit, + onContentChanged: (String) -> Unit, + onLocationChanged: (String) -> Unit, + onStartDateChanged: (LocalDate) -> Unit, + onStartTimeChanged: (LocalTime) -> Unit, + onEndDateChanged: (LocalDate) -> Unit, + onEndTimeChanged: (LocalTime) -> Unit, + onNextButtonClicked: (EventRegistrationState, EventRegistrationState) -> Unit, + onEditButtonClicked: (EventRegistrationState) -> Unit, + onCloseButtonClicked: () -> Unit, + onPreviousButtonClicked: (EventRegistrationState) -> Unit, + deleteEvent: () -> Unit, +) { + var showStartDatePicker by remember { mutableStateOf(false) } + var showStartTimePicker by remember { mutableStateOf(false) } + var showEndDatePicker by remember { mutableStateOf(false) } + var showEndTimePicker by remember { mutableStateOf(false) } + var showDeleteEventDialog by remember { mutableStateOf(false) } + val startTimePickerState = rememberTimePickerState() + val endTimePickerState = rememberTimePickerState() + val scrollState = rememberScrollState() + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + if (showDeleteEventDialog) { + DeleteEventDialog( + deleteEvent = deleteEvent, + onDismissRequest = { showDeleteEventDialog = false }, + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .height(IntrinsicSize.Max) + .padding(paddingValues) + .padding(top = 16.dp), + ) { + WappSubTopBar( + titleRes = R.string.event_edit, + showLeftButton = true, + showRightButton = true, + leftButtonDrawableRes = drawable.ic_close, + onClickLeftButton = onCloseButtonClicked, + onClickRightButton = { showDeleteEventDialog = true }, + ) + + EventEditStateIndicator( + eventRegistrationState = currentEditState, + modifier = Modifier.padding(top = 16.dp, start = 20.dp, end = 20.dp), + ) + + EventRegistrationContent( + eventRegistrationState = currentEditState, + modifier = Modifier.padding(horizontal = 20.dp), + eventTitle = title, + eventContent = content, + location = location, + startDate = startDate, + startTime = startTime, + endDate = endDate, + endTime = endTime, + scrollState = scrollState, + showStartDatePicker = showStartDatePicker, + showStartTimePicker = showStartTimePicker, + showEndDatePicker = showEndDatePicker, + showEndTimePicker = showEndTimePicker, + onTitleChanged = onTitleChanged, + onContentChanged = onContentChanged, + onLocationChanged = onLocationChanged, + startTimePickerState = startTimePickerState, + endTimePickerState = endTimePickerState, + onStartDateChanged = onStartDateChanged, + onStartTimeChanged = onStartTimeChanged, + onEndDateChanged = onEndDateChanged, + onEndTimeChanged = onEndTimeChanged, + onStartDatePickerStateChanged = { state -> showStartDatePicker = state }, + onStartTimePickerStateChanged = { state -> showStartTimePicker = state }, + onEndDatePickerStateChanged = { state -> showEndDatePicker = state }, + onEndTimePickerStateChanged = { state -> showEndTimePicker = state }, + onNextButtonClicked = onNextButtonClicked, + onRegisterButtonClicked = onEditButtonClicked, + onPreviousButtonClicked = onPreviousButtonClicked, + ) + } + } +} + +@Composable +private fun EventEditStateIndicator( + eventRegistrationState: EventRegistrationState, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + EventEditStateProgressBar(eventRegistrationState.progress) + EventEditStateText(eventRegistrationState.page) + } +} + +@Composable +private fun EventEditStateText( + currentRegistrationPage: String, +) { + Row { + Text( + text = currentRegistrationPage, + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.yellow34, + ) + Text( + text = stringResource(R.string.event_registration_total_page), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + ) + } +} + +@Composable +private fun EventEditStateProgressBar( + currentRegistrationProgress: Float, +) { + val progress by animateFloatAsState( + targetValue = currentRegistrationProgress, + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioMediumBouncy, + ), + ) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = WappTheme.colors.yellow34, + trackColor = WappTheme.colors.white, + progress = progress, + strokeCap = StrokeCap.Round, + ) +} + +@Composable +private fun DeleteEventDialog( + deleteEvent: () -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(id = R.string.delete_event), + style = WappTheme.typography.contentBold.copy(fontSize = 20.sp), + color = WappTheme.colors.yellow34, + modifier = Modifier.padding(top = 16.dp), + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Text( + text = generateDialogContentString(), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp), + ) { + Button( + onClick = { + deleteEvent() + onDismissRequest() + }, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.complete), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.black, + ) + } + + Button( + onClick = onDismissRequest, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.black25, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .weight(1f) + .border( + width = 1.dp, + color = WappTheme.colors.yellow34, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = stringResource(R.string.cancel), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.yellow34, + ) + } + } + } + } +} + +@Composable +private fun generateDialogContentString() = buildAnnotatedString { + append("์ •๋ง๋กœ ํ•ด๋‹น ์ผ์ •์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n\n") + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append("ํ•ด๋‹น ์ผ์ •๊ณผ ๊ด€๋ จ๋œ ์„ค๋ฌธ๊ณผ ๋‹ต๋ณ€๋“ค์ด ๋ชจ๋‘ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.") + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditViewModel.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditViewModel.kt new file mode 100644 index 000000000..9426ec163 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/edit/EventEditViewModel.kt @@ -0,0 +1,187 @@ +package com.wap.wapp.feature.management.event.edit + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.event.DeleteEventAndRelatedSurveysUseCase +import com.wap.wapp.core.domain.usecase.event.GetEventUseCase +import com.wap.wapp.core.domain.usecase.event.UpdateEventUseCase +import com.wap.wapp.feature.management.event.registration.EventRegistrationState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject + +@HiltViewModel +class EventEditViewModel @Inject constructor( + private val getEventUseCase: GetEventUseCase, + private val updateEventUseCase: UpdateEventUseCase, + private val deleteEventAndRelatedSurveysUseCase: DeleteEventAndRelatedSurveysUseCase, +) : ViewModel() { + private val _currentEditState: MutableStateFlow = + MutableStateFlow(EventRegistrationState.EVENT_DETAILS) + val currentEditState = _currentEditState.asStateFlow() + + private val _eventEditEvent: MutableSharedFlow = + MutableSharedFlow() + val eventEditEvent = _eventEditEvent.asSharedFlow() + + private val _eventTitle: MutableStateFlow = MutableStateFlow("") + val eventTitle = _eventTitle.asStateFlow() + + private val _eventContent: MutableStateFlow = MutableStateFlow("") + val eventContent = _eventContent.asStateFlow() + + private val _eventLocation: MutableStateFlow = MutableStateFlow("") + val eventLocation = _eventLocation.asStateFlow() + + private val _eventStartDate: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowDate()) + val eventStartDate = _eventStartDate.asStateFlow() + + private val _eventStartTime: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowTime()) + val eventStartTime = _eventStartTime.asStateFlow() + + private val _eventEndDate: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowDate()) + val eventEndDate = _eventEndDate.asStateFlow() + + private val _eventEndTime: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowTime().plusHours(1)) + val eventEndTime = _eventEndTime.asStateFlow() + + private val _eventId: MutableStateFlow = MutableStateFlow("") + + fun setEventTitle(eventTitle: String) { _eventTitle.value = eventTitle } + + fun setEventContent(eventContent: String) { _eventContent.value = eventContent } + + fun setEventLocation(eventLocation: String) { _eventLocation.value = eventLocation } + + fun setEventStartDate(eventDate: LocalDate) { _eventStartDate.value = eventDate } + + fun setEventStartTime(eventTime: LocalTime) { _eventStartTime.value = eventTime } + + fun setEventEndDate(eventDate: LocalDate) { + if (!isValidEndDate(eventDate)) { + emitValidationErrorMessage("์ข…๋ฃŒ ๋‚ ์งœ๋Š” ์‹œ์ž‘ ๋‚ ์งœ์™€ ๊ฐ™๊ฑฐ๋‚˜ ๋” ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return + } + _eventEndDate.value = eventDate + } + + fun setEventEndTime(eventTime: LocalTime) { + if (!isValidEndTime(eventTime)) { + emitValidationErrorMessage("์ข…๋ฃŒ ๋‚ ์งœ๋Š” ์‹œ์ž‘ ๋‚ ์งœ์™€ ๊ฐ™๊ฑฐ๋‚˜ ๋” ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return + } + _eventEndTime.value = eventTime + } + + fun validateEvent(eventRegistrationState: EventRegistrationState): Boolean { + when (eventRegistrationState) { + EventRegistrationState.EVENT_DETAILS -> { + if (!isValidTitle()) { + emitValidationErrorMessage("ํ–‰์‚ฌ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + + if (!isValidContent()) { + emitValidationErrorMessage("ํ–‰์‚ฌ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + } + + EventRegistrationState.EVENT_SCHEDULE -> { + if (!isValidLocation()) { + emitValidationErrorMessage("์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + + if (!isValidEndTime(_eventEndTime.value)) { + emitValidationErrorMessage("์ผ์ • ์ข…๋ฃŒ๋Š” ์‹œ์ž‘๋ณด๋‹ค ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return false + } + } + } + return true + } + + fun setEventRegistrationState(eventRegistrationState: EventRegistrationState) { + _currentEditState.value = eventRegistrationState + } + + fun updateEvent() = viewModelScope.launch { + updateEventUseCase( + eventTitle = _eventTitle.value, + eventContent = _eventContent.value, + eventLocation = _eventLocation.value, + eventStartDate = _eventStartDate.value, + eventStartTime = _eventStartTime.value, + eventEndDate = _eventEndDate.value, + eventEndTime = _eventEndTime.value, + eventId = _eventId.value, + ).onSuccess { + _eventEditEvent.emit(EventEditEvent.EditSuccess) + }.onFailure { throwable -> + _eventEditEvent.emit(EventEditEvent.Failure(throwable)) + } + } + + fun deleteEvent() = viewModelScope.launch { + deleteEventAndRelatedSurveysUseCase(_eventId.value).onSuccess { + _eventEditEvent.emit(EventEditEvent.DeleteSuccess) + }.onFailure { throwable -> + _eventEditEvent.emit(EventEditEvent.Failure(throwable)) + } + } + + private fun isValidEndTime(eventTime: LocalTime): Boolean { + val startDate = _eventStartDate.value + val endDate = _eventEndDate.value + + if (startDate == endDate) { + val startTime = _eventStartTime.value + return eventTime > startTime + } + return startDate < endDate + } + + private fun isValidEndDate(eventDate: LocalDate): Boolean = eventDate >= _eventStartDate.value + + private fun isValidContent(): Boolean = _eventContent.value.isNotEmpty() + + private fun isValidTitle(): Boolean = _eventTitle.value.isNotEmpty() + + private fun isValidLocation(): Boolean = _eventLocation.value.isNotEmpty() + + fun getEvent(eventId: String) = viewModelScope.launch { + getEventUseCase(eventId).onSuccess { + _eventContent.value = it.content + _eventTitle.value = it.title + _eventStartDate.value = it.startDateTime.toLocalDate() + _eventStartTime.value = it.startDateTime.toLocalTime() + _eventEndDate.value = it.endDateTime.toLocalDate() + _eventEndTime.value = it.endDateTime.toLocalTime() + _eventLocation.value = it.location + _eventId.value = it.eventId + } + .onFailure { emitValidationErrorMessage("์ด๋ฒคํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.") } + } + + private fun emitValidationErrorMessage(message: String) = + viewModelScope.launch { _eventEditEvent.emit(EventEditEvent.ValidationError(message)) } + + sealed class EventEditEvent { + data class ValidationError(val message: String) : EventEditEvent() + data class Failure(val error: Throwable) : EventEditEvent() + data object EditSuccess : EventEditEvent() + data object DeleteSuccess : EventEditEvent() + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/navigation/ManagementEventNavigation.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/navigation/ManagementEventNavigation.kt new file mode 100644 index 000000000..9e3418b58 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/navigation/ManagementEventNavigation.kt @@ -0,0 +1,46 @@ +package com.wap.wapp.feature.management.event.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 androidx.navigation.navOptions +import com.wap.wapp.feature.management.event.edit.EventEditRoute +import com.wap.wapp.feature.management.event.registration.EventRegistrationRoute + +const val eventRegistrationNavigationRoute = "event_registration_route" +const val eventEditNavigationRoute = "event/edit/{eventId}" + +fun NavController.navigateToEventRegistration(navOptions: NavOptions? = navOptions {}) { + this.navigate(eventRegistrationNavigationRoute, navOptions) +} + +fun NavController.navigateToEventEdit( + eventId: String, + navOptions: NavOptions? = navOptions {}, +) { + this.navigate("event/edit/$eventId", navOptions) +} + +fun NavGraphBuilder.managementEventNavGraph( + navigateToManagement: () -> Unit, +) { + composable(route = eventRegistrationNavigationRoute) { + EventRegistrationRoute( + navigateToManagement = navigateToManagement, + ) + } + + composable( + route = eventEditNavigationRoute, + arguments = listOf(navArgument("eventId") { type = NavType.StringType }), + ) { navBackStackEntry -> + val eventId = navBackStackEntry.arguments?.getString("eventId") ?: "" + EventEditRoute( + eventId = eventId, + navigateToManagement = navigateToManagement, + ) + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationContent.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationContent.kt new file mode 100644 index 000000000..afa329844 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationContent.kt @@ -0,0 +1,360 @@ +package com.wap.wapp.feature.management.event.registration + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.ScrollState +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.feature.management.event.R +import com.wap.wapp.feature.management.event.component.DeadlineCard +import com.wap.wapp.feature.management.event.component.RegistrationTextField +import com.wap.wapp.feature.management.event.component.RegistrationTitle +import com.wap.wapp.feature.management.event.component.WappDatePickerDialog +import com.wap.wapp.feature.management.event.component.WappTimePickerDialog +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.LocalTime + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun EventRegistrationContent( + eventRegistrationState: EventRegistrationState, + modifier: Modifier = Modifier, + eventTitle: String, + eventContent: String, + location: String, + startDate: LocalDate, + startTime: LocalTime, + endDate: LocalDate, + endTime: LocalTime, + showStartDatePicker: Boolean, + showStartTimePicker: Boolean, + showEndDatePicker: Boolean, + showEndTimePicker: Boolean, + scrollState: ScrollState, + startTimePickerState: TimePickerState, + endTimePickerState: TimePickerState, + onTitleChanged: (String) -> Unit, + onContentChanged: (String) -> Unit, + onLocationChanged: (String) -> Unit, + onEndDateChanged: (LocalDate) -> Unit, + onEndTimeChanged: (LocalTime) -> Unit, + onStartDateChanged: (LocalDate) -> Unit, + onStartTimeChanged: (LocalTime) -> Unit, + onStartDatePickerStateChanged: (Boolean) -> Unit, + onStartTimePickerStateChanged: (Boolean) -> Unit, + onEndDatePickerStateChanged: (Boolean) -> Unit, + onEndTimePickerStateChanged: (Boolean) -> Unit, + onNextButtonClicked: (EventRegistrationState, EventRegistrationState) -> Unit, + onRegisterButtonClicked: (EventRegistrationState) -> Unit, + onPreviousButtonClicked: (EventRegistrationState) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + + Column( + modifier = modifier + .fillMaxSize() + .addFocusCleaner(focusManager) + .padding(top = 40.dp, bottom = 16.dp), + ) { + AnimatedContent( + targetState = eventRegistrationState, + transitionSpec = { + if (targetState.ordinal > initialState.ordinal) { + slideInHorizontally(initialOffsetX = { it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() + } else { + slideInHorizontally(initialOffsetX = { -it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { it }) + fadeOut() + } + }, + label = stringResource(R.string.event_registration_content_navigation_animated_label), + ) { eventState -> + when (eventState) { + EventRegistrationState.EVENT_DETAILS -> EventDetailsContent( + eventTitle = eventTitle, + eventContent = eventContent, + onTitleChanged = onTitleChanged, + onContentChanged = onContentChanged, + onNextButtonClicked = { + coroutineScope.launch { + scrollState.scrollTo(0) + } + onNextButtonClicked( + EventRegistrationState.EVENT_DETAILS, // current State + EventRegistrationState.EVENT_SCHEDULE, // next Stae + ) + }, + ) + + EventRegistrationState.EVENT_SCHEDULE -> EventScheduleContent( + location = location, + startDate = startDate, + startTime = startTime, + endDate = endDate, + endTime = endTime, + startTimePickerState = startTimePickerState, + endTimePickerState = endTimePickerState, + onLocationChanged = onLocationChanged, + onEndDateChanged = onEndDateChanged, + onEndTimeChanged = onEndTimeChanged, + onStartDateChanged = onStartDateChanged, + onStartTimeChanged = onStartTimeChanged, + showStartDatePicker = showStartDatePicker, + showStartTimePicker = showStartTimePicker, + showEndDatePicker = showEndDatePicker, + showEndTimePicker = showEndTimePicker, + onStartDatePickerStateChanged = onStartDatePickerStateChanged, + onStartTimePickerStateChanged = onStartTimePickerStateChanged, + onEndDatePickerStateChanged = onEndDatePickerStateChanged, + onEndTimePickerStateChanged = onEndTimePickerStateChanged, + onRegisterButtonClicked = { + onRegisterButtonClicked(EventRegistrationState.EVENT_SCHEDULE) + }, + onPreviousButtonClicked = { + onPreviousButtonClicked(EventRegistrationState.EVENT_DETAILS) + }, + ) + } + } + } +} + +@Composable +private fun EventDetailsContent( + eventTitle: String, + eventContent: String, + onTitleChanged: (String) -> Unit, + onContentChanged: (String) -> Unit, + onNextButtonClicked: () -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.fillMaxSize(), + ) { + RegistrationTitle( + title = stringResource(id = R.string.event_details_title), + content = stringResource(id = R.string.event_details_content), + ) + + Text( + text = stringResource(R.string.event_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 40.dp), + ) + + RegistrationTextField( + value = eventTitle, + onValueChange = onTitleChanged, + placeholder = stringResource(R.string.event_title_hint), + modifier = Modifier.fillMaxWidth(), + ) + + Text( + text = stringResource(R.string.event_schedule_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 30.dp), + ) + + RegistrationTextField( + value = eventContent, + onValueChange = onContentChanged, + placeholder = stringResource(R.string.event_content_hint), + singleline = false, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) + + WappButton( + onClick = onNextButtonClicked, + textRes = R.string.next, + modifier = Modifier.padding(top = 20.dp), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EventScheduleContent( + location: String, + startDate: LocalDate, + startTime: LocalTime, + endDate: LocalDate, + endTime: LocalTime, + startTimePickerState: TimePickerState, + endTimePickerState: TimePickerState, + showStartDatePicker: Boolean, + showStartTimePicker: Boolean, + showEndDatePicker: Boolean, + showEndTimePicker: Boolean, + onStartDatePickerStateChanged: (Boolean) -> Unit, + onStartTimePickerStateChanged: (Boolean) -> Unit, + onEndDatePickerStateChanged: (Boolean) -> Unit, + onEndTimePickerStateChanged: (Boolean) -> Unit, + onLocationChanged: (String) -> Unit, + onStartDateChanged: (LocalDate) -> Unit, + onStartTimeChanged: (LocalTime) -> Unit, + onEndDateChanged: (LocalDate) -> Unit, + onEndTimeChanged: (LocalTime) -> Unit, + onRegisterButtonClicked: () -> Unit, + onPreviousButtonClicked: () -> Unit, +) { + if (showEndDatePicker) { + WappDatePickerDialog( + date = endDate, + onDismissRequest = { onEndDatePickerStateChanged(false) }, + onDateChanged = onEndDateChanged, + ) + } + + if (showEndTimePicker) { + WappTimePickerDialog( + state = endTimePickerState, + onDismissRequest = { onEndTimePickerStateChanged(false) }, + onConfirmButtonClicked = { localTime -> + onEndTimeChanged(localTime) + onEndTimePickerStateChanged(false) + }, + onDismissButtonClicked = { + onEndTimePickerStateChanged(false) + }, + ) + } + + if (showStartDatePicker) { + WappDatePickerDialog( + date = startDate, + onDismissRequest = { onStartDatePickerStateChanged(false) }, + onDateChanged = onStartDateChanged, + ) + } + + if (showStartTimePicker) { + WappTimePickerDialog( + state = startTimePickerState, + onDismissRequest = { onStartTimePickerStateChanged(false) }, + onConfirmButtonClicked = { localTime -> + onStartTimeChanged(localTime) + onStartTimePickerStateChanged(false) + }, + onDismissButtonClicked = { + onStartTimePickerStateChanged(false) + }, + ) + } + Column { + RegistrationTitle( + title = stringResource(id = R.string.event_schedule_title), + content = stringResource(id = R.string.event_schedule_content), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(top = 40.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.event_location), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + modifier = Modifier.weight(2f), + ) + + RegistrationTextField( + value = location, + onValueChange = onLocationChanged, + placeholder = stringResource(R.string.event_location_hint), + textAlign = TextAlign.Center, + modifier = Modifier.weight(3f), + align = Alignment.Center, + ) + } + + DeadlineCard( + title = stringResource(R.string.start_date), + hint = startDate.format(DateUtil.yyyyMMddFormatter), + onCardClicked = { + onStartDatePickerStateChanged(true) + }, + modifier = Modifier.padding(top = 20.dp), + ) + + DeadlineCard( + title = stringResource(R.string.start_time), + hint = startTime.format(DateUtil.HHmmFormatter), + onCardClicked = { + onStartTimePickerStateChanged(true) + }, + modifier = Modifier.padding(top = 20.dp), + ) + + DeadlineCard( + title = stringResource(R.string.end_date), + hint = endDate.format(DateUtil.yyyyMMddFormatter), + onCardClicked = { + onEndDatePickerStateChanged(true) + }, + modifier = Modifier.padding(top = 20.dp), + ) + + DeadlineCard( + title = stringResource(R.string.end_time), + hint = endTime.format(DateUtil.HHmmFormatter), + onCardClicked = { + onEndTimePickerStateChanged(true) + }, + modifier = Modifier.padding(top = 20.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + WappButton( + textRes = R.string.previous, + onClick = onPreviousButtonClicked, + modifier = Modifier.weight(1f), + ) + + WappButton( + textRes = R.string.register_event, + onClick = onRegisterButtonClicked, + modifier = Modifier.weight(1f), + ) + } + } + } +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationEvent.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationEvent.kt new file mode 100644 index 000000000..2c625d643 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationEvent.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.feature.management.event.registration + +sealed class EventRegistrationEvent { + data class ValidationError(val message: String) : EventRegistrationEvent() + data class Failure(val error: Throwable) : EventRegistrationEvent() + data object Success : EventRegistrationEvent() +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationScreen.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationScreen.kt new file mode 100644 index 000000000..ebeb1bed0 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationScreen.kt @@ -0,0 +1,265 @@ +package com.wap.wapp.feature.management.event.registration + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +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.WindowInsets +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberTimePickerState +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.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.feature.management.event.R +import kotlinx.coroutines.flow.collectLatest +import java.time.LocalDate +import java.time.LocalTime + +@Composable +internal fun EventRegistrationRoute( + viewModel: EventRegistrationViewModel = hiltViewModel(), + navigateToManagement: () -> Unit, +) { + val snackBarHostState = remember { SnackbarHostState() } + val currentRegistrationState + by viewModel.currentRegistrationState.collectAsStateWithLifecycle() + val title by viewModel.eventTitle.collectAsStateWithLifecycle() + val content by viewModel.eventContent.collectAsStateWithLifecycle() + val location by viewModel.eventLocation.collectAsStateWithLifecycle() + val startDate by viewModel.eventStartDate.collectAsStateWithLifecycle() + val startTime by viewModel.eventStartTime.collectAsStateWithLifecycle() + val endDate by viewModel.eventEndDate.collectAsStateWithLifecycle() + val endTime by viewModel.eventEndTime.collectAsStateWithLifecycle() + val onTitleChanged = viewModel::setEventTitle + val onContentChanged = viewModel::setEventContent + val onLocationChanged = viewModel::setEventLocation + val onStartDateChanged = viewModel::setEventStartDate + val onStartTimeChanged = viewModel::setEventStartTime + val onEndDateChanged = viewModel::setEventEndDate + val onEndTimeChanged = viewModel::setEventEndTime + + LaunchedEffect(true) { + viewModel.eventRegistrationEvent.collectLatest { + when (it) { + is EventRegistrationEvent.Failure -> + snackBarHostState.showSnackbar(it.error.toSupportingText()) + + is EventRegistrationEvent.ValidationError -> + snackBarHostState.showSnackbar(it.message) + + is EventRegistrationEvent.Success -> navigateToManagement() + } + } + } + + EventRegistrationScreen( + currentRegistrationState = currentRegistrationState, + title = title, + content = content, + location = location, + startDate = startDate, + startTime = startTime, + endDate = endDate, + endTime = endTime, + snackBarHostState = snackBarHostState, + onTitleChanged = onTitleChanged, + onContentChanged = onContentChanged, + onLocationChanged = onLocationChanged, + onStartDateChanged = onStartDateChanged, + onStartTimeChanged = onStartTimeChanged, + onEndDateChanged = onEndDateChanged, + onEndTimeChanged = onEndTimeChanged, + onNextButtonClicked = { currentState, nextState -> + if (viewModel.validateEvent(currentState)) { + viewModel.setEventRegistrationState(nextState) + } + }, + onCloseButtonClicked = navigateToManagement, + onPreviousButtonClicked = viewModel::setEventRegistrationState, + onRegisterButtonClicked = { lastState -> + if (viewModel.validateEvent(lastState)) { // ๋งˆ์ง€๋ง‰ ์ƒํƒœ ๋Œ€์ž… + viewModel.registerEvent() + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun EventRegistrationScreen( + currentRegistrationState: EventRegistrationState, + title: String, + content: String, + location: String, + startDate: LocalDate, + startTime: LocalTime, + endDate: LocalDate, + endTime: LocalTime, + snackBarHostState: SnackbarHostState, + onTitleChanged: (String) -> Unit, + onContentChanged: (String) -> Unit, + onLocationChanged: (String) -> Unit, + onStartDateChanged: (LocalDate) -> Unit, + onStartTimeChanged: (LocalTime) -> Unit, + onEndDateChanged: (LocalDate) -> Unit, + onEndTimeChanged: (LocalTime) -> Unit, + onNextButtonClicked: (EventRegistrationState, EventRegistrationState) -> Unit, + onPreviousButtonClicked: (EventRegistrationState) -> Unit, + onRegisterButtonClicked: (EventRegistrationState) -> Unit, + onCloseButtonClicked: () -> Unit, +) { + var showStartDatePicker by remember { mutableStateOf(false) } + var showStartTimePicker by remember { mutableStateOf(false) } + var showEndDatePicker by remember { mutableStateOf(false) } + var showEndTimePicker by remember { mutableStateOf(false) } + val startTimePickerState = rememberTimePickerState() + val endTimePickerState = rememberTimePickerState() + val scrollState = rememberScrollState() + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + contentWindowInsets = WindowInsets(0.dp), + modifier = Modifier + .fillMaxSize(), + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .height(IntrinsicSize.Max) + .padding(paddingValues) // paddingValue padding + .padding(top = 20.dp), // dp value padding + ) { + WappSubTopBar( + titleRes = R.string.event_registration, + showLeftButton = true, + onClickLeftButton = onCloseButtonClicked, + leftButtonDrawableRes = drawable.ic_close, + ) + + EventRegistrationStateIndicator( + eventRegistrationState = currentRegistrationState, + modifier = Modifier.padding(top = 16.dp, start = 20.dp, end = 20.dp), + ) + + EventRegistrationContent( + eventRegistrationState = currentRegistrationState, + modifier = Modifier.padding(horizontal = 20.dp), + eventTitle = title, + eventContent = content, + location = location, + startDate = startDate, + startTime = startTime, + endDate = endDate, + endTime = endTime, + scrollState = scrollState, + showStartDatePicker = showStartDatePicker, + showStartTimePicker = showStartTimePicker, + showEndDatePicker = showEndDatePicker, + showEndTimePicker = showEndTimePicker, + onTitleChanged = onTitleChanged, + onContentChanged = onContentChanged, + onLocationChanged = onLocationChanged, + startTimePickerState = startTimePickerState, + endTimePickerState = endTimePickerState, + onStartDateChanged = onStartDateChanged, + onStartTimeChanged = onStartTimeChanged, + onEndDateChanged = onEndDateChanged, + onEndTimeChanged = onEndTimeChanged, + onStartDatePickerStateChanged = { state -> showStartDatePicker = state }, + onStartTimePickerStateChanged = { state -> showStartTimePicker = state }, + onEndDatePickerStateChanged = { state -> showEndDatePicker = state }, + onEndTimePickerStateChanged = { state -> showEndTimePicker = state }, + onNextButtonClicked = onNextButtonClicked, + onPreviousButtonClicked = onPreviousButtonClicked, + onRegisterButtonClicked = onRegisterButtonClicked, + ) + } + } +} + +@Composable +private fun EventRegistrationStateIndicator( + eventRegistrationState: EventRegistrationState, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + EventRegistrationStateProgressBar(eventRegistrationState.progress) + EventRegistrationStateText(eventRegistrationState.page) + } +} + +@Composable +private fun EventRegistrationStateText( + currentRegistrationPage: String, +) { + Row { + Text( + text = currentRegistrationPage, + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.yellow34, + ) + Text( + text = stringResource(R.string.event_registration_total_page), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + ) + } +} + +@Composable +private fun EventRegistrationStateProgressBar( + currentRegistrationProgress: Float, +) { + val progress by animateFloatAsState( + targetValue = currentRegistrationProgress, + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioMediumBouncy, + ), + label = stringResource(R.string.event_registration_state_progress_animation), + ) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = WappTheme.colors.yellow34, + trackColor = WappTheme.colors.white, + progress = progress, + strokeCap = StrokeCap.Round, + ) +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationState.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationState.kt new file mode 100644 index 000000000..61fdf6768 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationState.kt @@ -0,0 +1,9 @@ +package com.wap.wapp.feature.management.event.registration + +enum class EventRegistrationState( + val page: String, + val progress: Float, +) { + EVENT_DETAILS("1", 0.5f), + EVENT_SCHEDULE("2", 1.0f), +} diff --git a/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationViewModel.kt b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationViewModel.kt new file mode 100644 index 000000000..978a70439 --- /dev/null +++ b/feature/management-event/src/main/java/com/wap/wapp/feature/management/event/registration/EventRegistrationViewModel.kt @@ -0,0 +1,165 @@ +package com.wap.wapp.feature.management.event.registration + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil.generateNowDate +import com.wap.wapp.core.commmon.util.DateUtil.generateNowTime +import com.wap.wapp.core.domain.usecase.event.PostEventUseCase +import com.wap.wapp.feature.management.event.registration.EventRegistrationState.EVENT_DETAILS +import com.wap.wapp.feature.management.event.registration.EventRegistrationState.EVENT_SCHEDULE +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject + +@HiltViewModel +class EventRegistrationViewModel @Inject constructor( + private val postEventUseCase: PostEventUseCase, +) : ViewModel() { + private val _currentRegistrationState: MutableStateFlow = + MutableStateFlow(EVENT_DETAILS) + val currentRegistrationState = _currentRegistrationState.asStateFlow() + + private val _eventRegistrationEvent: MutableSharedFlow = + MutableSharedFlow() + val eventRegistrationEvent = _eventRegistrationEvent.asSharedFlow() + + private val _eventTitle: MutableStateFlow = MutableStateFlow("") + val eventTitle = _eventTitle.asStateFlow() + + private val _eventContent: MutableStateFlow = MutableStateFlow("") + val eventContent = _eventContent.asStateFlow() + + private val _eventLocation: MutableStateFlow = MutableStateFlow("") + val eventLocation = _eventLocation.asStateFlow() + + private val _eventStartDate: MutableStateFlow = + MutableStateFlow(generateNowDate()) + val eventStartDate = _eventStartDate.asStateFlow() + + private val _eventStartTime: MutableStateFlow = + MutableStateFlow(generateNowTime()) + val eventStartTime = _eventStartTime.asStateFlow() + + private val _eventEndDate: MutableStateFlow = + MutableStateFlow(generateNowDate()) + val eventEndDate = _eventEndDate.asStateFlow() + + private val _eventEndTime: MutableStateFlow = + MutableStateFlow(generateNowTime().plusHours(1)) + val eventEndTime = _eventEndTime.asStateFlow() + + fun setEventTitle(eventTitle: String) { _eventTitle.value = eventTitle } + + fun setEventContent(eventContent: String) { _eventContent.value = eventContent } + + fun setEventLocation(eventLocation: String) { _eventLocation.value = eventLocation } + + fun setEventStartDate(eventDate: LocalDate) { + if (!isValidStartDate(eventDate)) { + emitValidationErrorMessage("์ตœ์†Œ ํ•˜๋ฃจ ์ด์ƒ ์ผ์ • ๋‚ ์งœ๋ฅผ ์ง€์ •ํ•˜์„ธ์š”.") + return + } + _eventStartDate.value = eventDate + } + + fun setEventStartTime(eventTime: LocalTime) { _eventStartTime.value = eventTime } + + fun setEventEndDate(eventDate: LocalDate) { + if (!isValidEndDate(eventDate)) { + emitValidationErrorMessage("์ข…๋ฃŒ ๋‚ ์งœ๋Š” ์‹œ์ž‘ ๋‚ ์งœ์™€ ๊ฐ™๊ฑฐ๋‚˜ ๋” ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return + } + _eventEndDate.value = eventDate + } + + fun setEventEndTime(eventTime: LocalTime) { + if (!isValidEndTime(eventTime)) { + emitValidationErrorMessage("์ข…๋ฃŒ ๋‚ ์งœ๋Š” ์‹œ์ž‘ ๋‚ ์งœ์™€ ๊ฐ™๊ฑฐ๋‚˜ ๋” ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return + } + _eventEndTime.value = eventTime + } + + fun validateEvent(eventRegistrationState: EventRegistrationState): Boolean { + when (eventRegistrationState) { + EVENT_DETAILS -> { + if (!isValidTitle()) { + emitValidationErrorMessage("ํ–‰์‚ฌ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + + if (!isValidContent()) { + emitValidationErrorMessage("ํ–‰์‚ฌ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + } + + EVENT_SCHEDULE -> { + if (!isValidLocation()) { + emitValidationErrorMessage("์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.") + return false + } + if (!isValidEndTime(_eventEndTime.value)) { + emitValidationErrorMessage("์ผ์ • ์ข…๋ฃŒ๋Š” ์‹œ์ž‘๋ณด๋‹ค ๋Šฆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + return false + } + } + } + return true + } + + fun setEventRegistrationState(eventRegistrationState: EventRegistrationState) { + _currentRegistrationState.value = eventRegistrationState + } + + fun registerEvent() = viewModelScope.launch { + postEventUseCase( + eventTitle = _eventTitle.value, + eventContent = _eventContent.value, + eventLocation = _eventLocation.value, + eventStartDate = _eventStartDate.value, + eventStartTime = _eventStartTime.value, + eventEndDate = _eventEndDate.value, + eventEndTime = _eventEndTime.value, + ).onSuccess { + _eventRegistrationEvent.emit(EventRegistrationEvent.Success) + }.onFailure { throwable -> + _eventRegistrationEvent.emit(EventRegistrationEvent.Failure(throwable)) + } + } + + private fun isValidEndTime(eventTime: LocalTime): Boolean { + val startDate = _eventStartDate.value + val endDate = _eventEndDate.value + + if (startDate == endDate) { + val startTime = _eventStartTime.value + return eventTime > startTime + } + return startDate < endDate + } + + private fun isValidEndDate(eventDate: LocalDate): Boolean = eventDate >= _eventStartDate.value + + private fun isValidStartDate(eventDate: LocalDate): Boolean = eventDate > generateNowDate() + + private fun isValidContent(): Boolean = _eventContent.value.isNotEmpty() + + private fun isValidTitle(): Boolean = _eventTitle.value.isNotEmpty() + + private fun isValidLocation(): Boolean = _eventLocation.value.isNotEmpty() + + private fun emitValidationErrorMessage(message: String) { + viewModelScope.launch { + _eventRegistrationEvent.emit( + EventRegistrationEvent.ValidationError(message), + ) + } + } +} diff --git a/feature/management-event/src/main/res/values/strings.xml b/feature/management-event/src/main/res/values/strings.xml new file mode 100644 index 000000000..5e70a1b93 --- /dev/null +++ b/feature/management-event/src/main/res/values/strings.xml @@ -0,0 +1,35 @@ + + + ์ผ์ • ์ถ”๊ฐ€ + ์ผ์ • ์ˆ˜์ • + / 2 + ํ–‰์‚ฌ ๋‚ด์šฉ ์ž‘์„ฑ + ๋ถ€์›๋“ค์ด ์•Œ์•„๋ณด๊ธฐ ์‰ฝ๋„๋ก ์ž์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. + "ํ–‰์‚ฌ ์ผ์ • ์ž‘์„ฑ" + ์ •ํ™•ํ•œ ๋‚ ์งœ์™€ ์‹œ๊ฐ„ ๊ทธ๋ฆฌ๊ณ  ์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ํ–‰์‚ฌ ์ด๋ฆ„ + ํ–‰์‚ฌ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”. + ํ–‰์‚ฌ ์†Œ๊ฐœ + ํ–‰์‚ฌ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ์†Œ๊ฐœํ•ด์ฃผ์„ธ์š”. + ์žฅ์†Œ + ๋‚ ์งœ + ์‹œ๊ฐ„ + ์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. + ์‹œ์ž‘ ๋‚ ์งœ + ์‹œ์ž‘ ์‹œ๊ฐ„ + ์ข…๋ฃŒ ๋‚ ์งœ + ์ข…๋ฃŒ ์‹œ๊ฐ„ + ๋‚ ์งœ + ์‹œ๊ฐ„ + ๋‹ค์Œ + ์ผ์ • ๋“ฑ๋ก + ์ผ์ • ์‚ญ์ œ + ์ทจ์†Œ + ์™„๋ฃŒ + ์„ ํƒ + "์ด์ „ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + "๋‹ค์Œ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + ์ด์ „ + Event Registration Content Navigation Animated Label + Event Registration State Progress Animation + diff --git a/feature/management-event/src/test/java/com/wap/wapp/feature/management/event/ExampleUnitTest.kt b/feature/management-event/src/test/java/com/wap/wapp/feature/management/event/ExampleUnitTest.kt new file mode 100644 index 000000000..303c8ee2d --- /dev/null +++ b/feature/management-event/src/test/java/com/wap/wapp/feature/management/event/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.management.event + +import org.junit.Assert.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/feature/management-survey/.gitignore b/feature/management-survey/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/management-survey/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/management-survey/build.gradle.kts b/feature/management-survey/build.gradle.kts new file mode 100644 index 000000000..77f011751 --- /dev/null +++ b/feature/management-survey/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.management.survey" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:designsystem")) + implementation(project(":core:designresource")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/management-survey/consumer-rules.pro b/feature/management-survey/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/management-survey/proguard-rules.pro b/feature/management-survey/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/management-survey/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/management-survey/src/main/AndroidManifest.xml b/feature/management-survey/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/feature/management-survey/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/EventsState.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/EventsState.kt new file mode 100644 index 000000000..7a5cd3ba0 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/EventsState.kt @@ -0,0 +1,9 @@ +package com.wap.wapp.feature.management.survey + +import com.wap.wapp.core.model.event.Event + +sealed class EventsState { + data object Loading : EventsState() + data class Success(val events: List) : EventsState() + data class Failure(val throwable: Throwable) : EventsState() +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyDeadlineContent.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyDeadlineContent.kt new file mode 100644 index 000000000..08c99bd40 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyDeadlineContent.kt @@ -0,0 +1,160 @@ +package com.wap.wapp.feature.management.survey + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappTitle +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.feature.management.survey.component.WappDatePickerDialog +import com.wap.wapp.feature.management.survey.component.WappTimePickerDialog +import java.time.LocalDate +import java.time.LocalTime + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SurveyDeadlineContent( + dateDeadline: LocalDate, + timeDeadline: LocalTime, + timePickerState: TimePickerState, + showDatePicker: Boolean, + showTimePicker: Boolean, + onDatePickerStateChanged: (Boolean) -> Unit, + onTimePickerStateChanged: (Boolean) -> Unit, + onDateChanged: (LocalDate) -> Unit, + onTimeChanged: (LocalTime) -> Unit, + onPreviousButtonClicked: () -> Unit, + onRegisterButtonClicked: () -> Unit, +) { + if (showDatePicker) { + WappDatePickerDialog( + date = dateDeadline, + onDismissRequest = { onDatePickerStateChanged(false) }, + onDateChanged = onDateChanged, + ) + } + + if (showTimePicker) { + WappTimePickerDialog( + state = timePickerState, + onDismissRequest = { onTimePickerStateChanged(false) }, + onConfirmButtonClicked = { localTime -> + onTimeChanged(localTime) + onTimePickerStateChanged(false) + }, + onDismissButtonClicked = { + onTimePickerStateChanged(false) + }, + ) + } + + val scrollState = rememberScrollState() + + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + WappTitle( + title = stringResource(R.string.survey_deadline_title), + content = stringResource(R.string.survey_deadline_content), + modifier = Modifier.padding(bottom = 20.dp), + ) + + DeadlineCard( + title = stringResource(R.string.date), + hint = dateDeadline.format(DateUtil.yyyyMMddFormatter), + onCardClicked = { + onDatePickerStateChanged(true) + }, + ) + + DeadlineCard( + title = stringResource(R.string.time), + hint = timeDeadline.format(DateUtil.HHmmFormatter), + onCardClicked = { + onTimePickerStateChanged(true) + }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + WappButton( + textRes = R.string.previous, + onClick = onPreviousButtonClicked, + modifier = Modifier.weight(1f), + ) + + WappButton( + textRes = R.string.submit_survey, + onClick = onRegisterButtonClicked, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun DeadlineCard( + title: String, + hint: String, + onCardClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Text( + text = title, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + modifier = Modifier.weight(2f), + ) + + Card( + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .weight(3f) + .clickable { onCardClicked() }, + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Text( + text = hint, + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(), + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyEventContent.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyEventContent.kt new file mode 100644 index 000000000..0b8b4323e --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyEventContent.kt @@ -0,0 +1,113 @@ +package com.wap.wapp.feature.management.survey + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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 com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappTitle +import com.wap.wapp.core.model.event.Event + +@Composable +internal fun SurveyEventContent( + eventsState: EventsState, + selectedEvent: Event, + onEventSelected: (Event) -> Unit, + onNextButtonClicked: () -> Unit, +) { + Column(modifier = Modifier.fillMaxSize()) { + WappTitle( + title = stringResource(R.string.event_selection_title), + content = stringResource(R.string.event_selection_content), + modifier = Modifier.padding(bottom = 40.dp), + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + when (eventsState) { + is EventsState.Loading -> item { + CircleLoader( + modifier = Modifier.fillMaxSize(), + ) + } + + is EventsState.Success -> { + items(eventsState.events) { event -> + EventCard( + event = event, + selectedEvent = selectedEvent, + onEventSelected = onEventSelected, + ) + } + } + + is EventsState.Failure -> {} + } + } + + WappButton( + textRes = R.string.next, + onClick = onNextButtonClicked, + modifier = Modifier.padding(top = 16.dp), + ) + } +} + +@Composable +private fun EventCard( + event: Event, + selectedEvent: Event, + onEventSelected: (Event) -> Unit, +) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + modifier = Modifier + .fillMaxWidth() + .clickable { onEventSelected(event) }, + border = BorderStroke( + color = if (event.eventId == selectedEvent.eventId) { + WappTheme.colors.yellow34 + } else { + WappTheme.colors.black25 + }, + width = 1.dp, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp), + ) { + Text( + text = event.title, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + Text( + text = event.content, + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.yellow34, + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormContent.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormContent.kt new file mode 100644 index 000000000..8b23a31d2 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormContent.kt @@ -0,0 +1,143 @@ +package com.wap.wapp.feature.management.survey + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.QuestionType +import java.time.LocalDate +import java.time.LocalTime + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SurveyFormContent( + surveyRegistrationState: SurveyFormState, + eventsState: EventsState, + eventSelection: Event, + title: String, + content: String, + questionTitle: String, + questionType: QuestionType, + timeDeadline: LocalTime, + dateDeadline: LocalDate, + currentQuestionNumber: Int, + totalQuestionNumber: Int, + timePickerState: TimePickerState, + showDatePicker: Boolean, + showTimePicker: Boolean, + onDateChanged: (LocalDate) -> Unit, + onDatePickerStateChanged: (Boolean) -> Unit, + onTimePickerStateChanged: (Boolean) -> Unit, + onEventContentInitialized: () -> Unit, + onEventSelected: (Event) -> Unit, + onTitleChanged: (String) -> Unit, + onContentChanged: (String) -> Unit, + onQuestionTitleChanged: (String) -> Unit, + onQuestionTypeChanged: (QuestionType) -> Unit, + onPreviousQuestionButtonClicked: () -> Unit, + onNextQuestionButtonClicked: () -> Unit, + onTimeChanged: (LocalTime) -> Unit, + onPreviousButtonClicked: (SurveyFormState) -> Unit, // previousState + onNextButtonClicked: (SurveyFormState, SurveyFormState) -> Unit, // (currentState, nextState) + onAddQuestionButtonClicked: () -> Unit, + onDeleteQuestionButtonClicked: () -> Unit, + onRegisterButtonClicked: () -> Unit, +) { + AnimatedContent( + targetState = surveyRegistrationState, + transitionSpec = { + if (targetState.ordinal > initialState.ordinal) { + slideInHorizontally(initialOffsetX = { it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() + } else { + slideInHorizontally(initialOffsetX = { -it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { it }) + fadeOut() + } + }, + label = stringResource(R.string.survey_form_registration_content_animated_content), + ) { registrationState -> + when (registrationState) { + SurveyFormState.EVENT_SELECTION -> { + onEventContentInitialized() + SurveyEventContent( + eventsState = eventsState, + selectedEvent = eventSelection, + onEventSelected = onEventSelected, + onNextButtonClicked = { + onNextButtonClicked( + SurveyFormState.EVENT_SELECTION, + SurveyFormState.INFORMATION, + ) + }, + ) + } + + SurveyFormState.INFORMATION -> { + SurveyInformationContent( + title = title, + onTitleChanged = onTitleChanged, + content = content, + onContentChanged = onContentChanged, + onPreviousButtonClicked = { + onPreviousButtonClicked(SurveyFormState.EVENT_SELECTION) + }, + onNextButtonClicked = { + onNextButtonClicked( + SurveyFormState.INFORMATION, + SurveyFormState.QUESTION, + ) + }, + ) + } + + SurveyFormState.QUESTION -> { + SurveyQuestionContent( + questionTitle = questionTitle, + questionType = questionType, + onQuestionTypeChanged = { defaultQuestionType -> + onQuestionTypeChanged(defaultQuestionType) + }, + onQuestionChanged = onQuestionTitleChanged, + onAddQuestionButtonClicked = onAddQuestionButtonClicked, + onDeleteQuestionButtonClicked = onDeleteQuestionButtonClicked, + currentQuestionNumber = currentQuestionNumber, + totalQuestionNumber = totalQuestionNumber, + onPreviousButtonClicked = { + onPreviousButtonClicked(SurveyFormState.INFORMATION) + }, + onNextButtonClicked = { + onNextButtonClicked( + SurveyFormState.QUESTION, + SurveyFormState.DEADLINE, + ) + }, + onPreviousQuestionButtonClicked = onPreviousQuestionButtonClicked, + onNextQuestionButtonClicked = onNextQuestionButtonClicked, + ) + } + + SurveyFormState.DEADLINE -> { + SurveyDeadlineContent( + timeDeadline = timeDeadline, + dateDeadline = dateDeadline, + timePickerState = timePickerState, + showDatePicker = showDatePicker, + showTimePicker = showTimePicker, + onDateChanged = onDateChanged, + onTimePickerStateChanged = onTimePickerStateChanged, + onTimeChanged = onTimeChanged, + onRegisterButtonClicked = onRegisterButtonClicked, + onDatePickerStateChanged = onDatePickerStateChanged, + onPreviousButtonClicked = { onPreviousButtonClicked(SurveyFormState.QUESTION) }, + ) + } + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormState.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormState.kt new file mode 100644 index 000000000..42314cce6 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormState.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.feature.management.survey + +enum class SurveyFormState( + val page: String, + val progress: Float, +) { + EVENT_SELECTION("1", 0.25f), + INFORMATION("2", 0.50f), + QUESTION("3", 0.75f), + DEADLINE("4", 1f), +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormStateIndicator.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormStateIndicator.kt new file mode 100644 index 000000000..1ebd8d6d0 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyFormStateIndicator.kt @@ -0,0 +1,76 @@ +package com.wap.wapp.feature.management.survey + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme + +@Composable +internal fun SurveyFormStateIndicator( + surveyRegistrationState: SurveyFormState, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + SurveyRegistrationStateProgressBar(surveyRegistrationState.progress) + SurveyRegistrationStateText(surveyRegistrationState.page) + } +} + +@Composable +private fun SurveyRegistrationStateText( + currentRegistrationPage: String, +) { + Row { + Text( + text = currentRegistrationPage, + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.yellow34, + ) + Text( + text = stringResource(R.string.survey_registration_total_page), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + ) + } +} + +@Composable +private fun SurveyRegistrationStateProgressBar( + currentRegistrationProgress: Float, +) { + val progress by animateFloatAsState( + targetValue = currentRegistrationProgress, + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioMediumBouncy, + ), + ) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + color = WappTheme.colors.yellow34, + trackColor = WappTheme.colors.white, + progress = progress, + strokeCap = StrokeCap.Round, + ) +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyInformationContent.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyInformationContent.kt new file mode 100644 index 000000000..83be22aa4 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyInformationContent.kt @@ -0,0 +1,90 @@ +package com.wap.wapp.feature.management.survey + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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 com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappRoundedTextField +import com.wap.designsystem.component.WappTitle + +@Composable +internal fun SurveyInformationContent( + title: String, + onTitleChanged: (String) -> Unit, + content: String, + onContentChanged: (String) -> Unit, + onPreviousButtonClicked: () -> Unit, + onNextButtonClicked: () -> Unit, +) { + val scrollState = rememberScrollState() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + WappTitle( + title = stringResource(R.string.survey_information_title), + content = stringResource(R.string.survey_information_content), + modifier = Modifier.padding(bottom = 24.dp), + ) + + Text( + text = stringResource(R.string.survey_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + WappRoundedTextField( + value = title, + onValueChange = onTitleChanged, + placeholder = R.string.survey_title_hint, + modifier = Modifier.fillMaxWidth(), + ) + + Text( + text = stringResource(R.string.survey_introduce), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 14.dp), + ) + + WappRoundedTextField( + value = content, + onValueChange = onContentChanged, + placeholder = R.string.survey_introduce_hint, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(top = 4.dp), + ) { + WappButton( + textRes = R.string.previous, + onClick = onPreviousButtonClicked, + modifier = Modifier.weight(1f), + ) + + WappButton( + textRes = R.string.next, + onClick = onNextButtonClicked, + modifier = Modifier.weight(1f), + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyQuestionContent.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyQuestionContent.kt new file mode 100644 index 000000000..9ffd7a688 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/SurveyQuestionContent.kt @@ -0,0 +1,312 @@ +package com.wap.wapp.feature.management.survey + +import androidx.compose.foundation.Image +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.fillMaxSize +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +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.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappRoundedTextField +import com.wap.designsystem.component.WappTitle +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.core.model.survey.QuestionType + +@Composable +internal fun SurveyQuestionContent( + questionTitle: String, + questionType: QuestionType, + currentQuestionNumber: Int, + totalQuestionNumber: Int, + onQuestionChanged: (String) -> Unit, + onQuestionTypeChanged: (QuestionType) -> Unit, + onPreviousQuestionButtonClicked: () -> Unit, + onNextQuestionButtonClicked: () -> Unit, + onPreviousButtonClicked: () -> Unit, + onNextButtonClicked: () -> Unit, + onAddQuestionButtonClicked: () -> Unit, + onDeleteQuestionButtonClicked: () -> Unit, +) { + val scrollState = rememberScrollState() + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + WappTitle( + title = stringResource(R.string.survey_question_title), + content = stringResource(R.string.survey_question_content), + modifier = Modifier.padding(bottom = 30.dp), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.survey_question), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + SurveyQuestionNumberText( + questionNumber = currentQuestionNumber + 1, + totalQuestionNumber = totalQuestionNumber + 1, + onPreviousQuestionButtonClicked = onPreviousQuestionButtonClicked, + onNextQuestionButtonClicked = onNextQuestionButtonClicked, + ) + } + + WappRoundedTextField( + value = questionTitle, + onValueChange = onQuestionChanged, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + placeholder = R.string.suvey_question_hint, + ) + + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.delete_survey_question), + color = WappTheme.colors.white, + style = WappTheme.typography.labelRegular, + modifier = Modifier + .padding(end = 8.dp) + .clickable { onDeleteQuestionButtonClicked() }, + ) + + Text( + text = stringResource(id = R.string.add_survey_question), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.labelRegular, + modifier = Modifier.clickable { onAddQuestionButtonClicked() }, + ) + } + + Text( + text = stringResource(R.string.survey_question_type), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 22.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + SurveyQuestionTypeChip( + selected = (questionType == QuestionType.SUBJECTIVE), + onSelected = { onQuestionTypeChanged(QuestionType.SUBJECTIVE) }, + label = stringResource(R.string.essay), + ) + + SurveyQuestionTypeChip( + selected = (questionType == QuestionType.OBJECTIVE), + onSelected = { onQuestionTypeChanged(QuestionType.OBJECTIVE) }, + label = stringResource(R.string.multie_choice), + ) + } + + SurveyQuestionTypeDescription(type = questionType) + + Spacer(modifier = Modifier.weight(1f)) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + WappButton( + textRes = R.string.previous, + onClick = onPreviousButtonClicked, + modifier = Modifier.weight(1f), + ) + + WappButton( + textRes = R.string.next, + onClick = onNextButtonClicked, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun SurveyQuestionNumberText( + questionNumber: Int, + totalQuestionNumber: Int, + onPreviousQuestionButtonClicked: () -> Unit, + onNextQuestionButtonClicked: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + // ์ด์ „ ์งˆ๋ฌธ ๋ฒ„ํŠผ + if (questionNumber > 1) { + Image( + painter = painterResource(id = drawable.ic_back), + contentDescription = stringResource(R.string.previous_question), + modifier = Modifier + .size(14.dp) + .padding(end = 4.dp) + .clickable { onPreviousQuestionButtonClicked() }, + ) + } + + Text( + color = WappTheme.colors.white, + style = WappTheme.typography.contentRegular, + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + WappTheme.colors.yellow34, + ), + ) { append(questionNumber.toString()) } + append(" / $totalQuestionNumber") + }, + ) + + // ๋‹ค์Œ ์งˆ๋ฌธ ๋ฒ„ํŠผ + if (questionNumber < totalQuestionNumber) { + Image( + painter = painterResource(id = drawable.ic_forward), + contentDescription = stringResource(R.string.next_question), + modifier = Modifier + .size(14.dp) + .padding(start = 4.dp) + .clickable { onNextQuestionButtonClicked() }, + ) + } + } +} + +@Composable +private fun SurveyQuestionTypeDescription(type: QuestionType) { + when (type) { + QuestionType.SUBJECTIVE -> { + Text( + text = stringResource(R.string.essay_hint), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.labelRegular, + modifier = Modifier.padding(top = 10.dp), + ) + } + + QuestionType.OBJECTIVE -> { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .padding(top = 10.dp) + .fillMaxWidth(), + ) { + SurveyQuestionTypeCard( + title = stringResource(R.string.good), + content = stringResource(R.string.good_description), + ) + + SurveyQuestionTypeCard( + title = stringResource(R.string.soso), + content = stringResource(R.string.soso_description), + ) + + SurveyQuestionTypeCard( + title = stringResource(R.string.bad), + content = stringResource(R.string.bad_description), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SurveyQuestionTypeChip( + selected: Boolean, + onSelected: () -> Unit, + label: String, +) { + FilterChip( + selected = selected, + onClick = { onSelected() }, + label = { + Text(text = label, modifier = Modifier.padding(vertical = 8.dp)) + }, + colors = FilterChipDefaults.filterChipColors( + containerColor = WappTheme.colors.backgroundBlack, + selectedContainerColor = WappTheme.colors.backgroundBlack, + labelColor = WappTheme.colors.white, + selectedLabelColor = WappTheme.colors.yellow34, + ), + border = FilterChipDefaults.filterChipBorder( + borderWidth = 1.dp, + selectedBorderWidth = 1.dp, + borderColor = WappTheme.colors.white, + selectedBorderColor = WappTheme.colors.yellow34, + ), + ) +} + +@Composable +private fun SurveyQuestionTypeCard( + title: String, + content: String, +) { + Card( + shape = RoundedCornerShape(10.dp), + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + ) { + Text( + modifier = Modifier.weight(1f), + text = title, + color = WappTheme.colors.white, + style = WappTheme.typography.contentBold, + textAlign = TextAlign.Center, + ) + + Text( + text = content, + color = WappTheme.colors.white, + style = WappTheme.typography.labelRegular, + modifier = Modifier.weight(4f), + textAlign = TextAlign.End, + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/DatePickerDialog.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/DatePickerDialog.kt new file mode 100644 index 000000000..adbb72adf --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/DatePickerDialog.kt @@ -0,0 +1,303 @@ +package com.wap.wapp.feature.management.survey.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.feature.management.survey.R +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale + +@Composable +internal fun WappDatePickerDialog( + date: LocalDate, + onDateChanged: (LocalDate) -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Card(shape = RoundedCornerShape(10.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = WappTheme.colors.black25) + .padding(10.dp), + ) { + var selectedDate by remember { mutableStateOf(date) } + +// CalendarHeader๋Š” 2024-01๊ณผ ๊ฐ™์ด ๋…„ - ๋‹ฌ์„ ํ‘œ์‹œํ•ด์ฃผ๊ณ , +// ๋‹ฌ์„ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค. + CalendarHeader( + selectedDate = selectedDate, + onDateSelected = { date -> selectedDate = date }, + ) + +// CalendarBody๋Š” ์›”,ํ™”,์ˆ˜,๋ชฉ,๊ธˆ,ํ† ,์ผ์˜ ์ƒ๋‹จ ์š”์ผ๋“ค์„ ํฌํ•จํ•œ ์บ˜๋ฆฐ๋”๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. + CalendarBody( + selectedDate = selectedDate, + onDateSelected = { date -> selectedDate = date }, + ) + + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 30.dp) + .clickable { onDismissRequest() }, + ) + + Text( + stringResource(R.string.select), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 20.dp) + .clickable { + onDateChanged(selectedDate) + onDismissRequest() + }, + ) + } + } + } + } +} + +@Composable +private fun CalendarHeader( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) = Box(modifier = Modifier.fillMaxWidth()) { + val date = selectedDate.format(DateUtil.yyyyMMddFormatter) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.Center), + ) { + Image( + painter = painterResource(id = com.wap.wapp.core.designresource.R.drawable.ic_back), + contentDescription = stringResource(id = R.string.backMonthArrowContentDescription), + modifier = Modifier + .padding(end = 20.dp) + .clickable { onDateSelected(selectedDate.minusMonths(1)) }, + ) + + Text( + text = date.substring( + DateUtil.YEAR_MONTH_START_INDEX, + DateUtil.YEAR_MONTH_END_INDEX, + ), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + Image( + painter = painterResource(id = com.wap.wapp.core.designresource.R.drawable.ic_forward), + contentDescription = stringResource(id = R.string.forwardMonthArrowContentDescription), + modifier = Modifier + .padding(start = 20.dp) + .clickable { onDateSelected(selectedDate.plusMonths(1)) }, + ) + } +} + +@Composable +private fun CalendarBody( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) { +// CalendarWeekDays๋Š” ์ผ,์›”,ํ™”,์ˆ˜,๋ชฉ,๊ธˆ,ํ† ์ผ๊ณผ ๊ฐ™์€ ์š”์ผ์„ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” Composable์ž…๋‹ˆ๋‹ค. + CalendarWeekDays() + +// ์‹ค์งˆ์ ์ธ ๋™์ ์ธ ๋‹ฌ๋ ฅ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. + CalendarMonthItem( + selectedDate = selectedDate, + onDateSelected = onDateSelected, + ) +} + +@Composable +private fun CalendarWeekDays(modifier: Modifier = Modifier) { + Row(modifier = modifier) { + DateUtil.DaysOfWeek.values().forEach { dayOfWeek -> + val textColor = when (dayOfWeek) { + DateUtil.DaysOfWeek.SATURDAY -> WappTheme.colors.blueA3 + DateUtil.DaysOfWeek.SUNDAY -> WappTheme.colors.red + else -> WappTheme.colors.white + } + + Text( + text = dayOfWeek.displayName, + textAlign = TextAlign.Center, + color = textColor, + modifier = Modifier + .weight(1f) + .padding(vertical = 10.dp), + ) + } + } +} + +@Composable +private fun CalendarMonthItem( + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(DateUtil.DAYS_IN_WEEK), + modifier = Modifier.fillMaxWidth(), + ) { +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์— ์•ฝ๊ฐ„ ๋ณด์—ฌ์ง€๋Š” ์ง€๋‚œ ๋‹ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val visibleDaysFromLastMonth = calculateVisibleDaysFromLastMonth(selectedDate) + val beforeMonthDaysToShow = generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth, + selectedDate, + ) + itemsIndexed(beforeMonthDaysToShow) { index, day -> + CalendarDayText( + text = day.toString(), + color = getDayColor(index + 1).copy(alpha = ALPHA_DIM), + ) + } + +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val thisMonthLastDate = selectedDate.lengthOfMonth() + val thisMonthFirstDayOfWeek = selectedDate.withDayOfMonth(1).dayOfWeek + val thisMonthDaysToShow: List = (1..thisMonthLastDate).toList() + items(thisMonthDaysToShow) { day -> + val date = selectedDate.withDayOfMonth(day) + val currentLocalDate = LocalDate.of( + selectedDate.year, + selectedDate.month, + day, + ) + + val isSelected = (day == selectedDate.dayOfMonth) + CalendarDayText( + text = DateUtil.ddFormatter.format(date), + color = getDayColor(day + thisMonthFirstDayOfWeek.value), + isSelected = isSelected, + modifier = Modifier.clickable { onDateSelected(currentLocalDate) }, + ) + } + +// ์ด๋ฒˆ ๋‹ฌ ๋‹ฌ๋ ฅ์— ์•ฝ๊ฐ„ ๋ณด์—ฌ์ง€๋Š” ๋‹ค์Œ ๋‹ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + val remainingDays = + DateUtil.DAYS_IN_WEEK - (visibleDaysFromLastMonth + thisMonthDaysToShow.size) % + DateUtil.DAYS_IN_WEEK + val nextMonthDaysToShow = IntRange(1, remainingDays).toList() + items(nextMonthDaysToShow) { day -> + CalendarDayText( + text = day.toString(), + color = + getDayColor(visibleDaysFromLastMonth + thisMonthDaysToShow.size + day) + .copy(alpha = ALPHA_DIM), + ) + } + } +} + +@Composable +private fun CalendarDayText( + text: String, + color: Color, + isSelected: Boolean = false, + modifier: Modifier = Modifier, +) { + var columnModifier = modifier.padding(5.dp) + + if (isSelected) { + columnModifier = columnModifier.background( + color = WappTheme.colors.gray82.copy(alpha = 0.4F), + shape = RoundedCornerShape(5.dp), + ) + } + + columnModifier = columnModifier.padding(5.dp) + + Column( + modifier = columnModifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + textAlign = TextAlign.Center, + color = color, + ) + } + } +} + +@Composable +private fun getDayColor(day: Int): Color = when (day % DateUtil.DAYS_IN_WEEK) { + SUNDAY -> WappTheme.colors.red + SATURDAY -> WappTheme.colors.blueA3 + else -> WappTheme.colors.white +} + +private fun generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth: Int, + currentDate: LocalDate, +): List { + val beforeMonth = currentDate.minusMonths(1) + val beforeMonthLastDay = beforeMonth.lengthOfMonth() + return IntRange(beforeMonthLastDay - visibleDaysFromLastMonth + 1, beforeMonthLastDay).toList() +} + +private fun calculateVisibleDaysFromLastMonth(currentDate: LocalDate): Int { + val firstDayOfWeek: DayOfWeek = currentDate.withDayOfMonth(1).dayOfWeek + + var count = 0 + for (day in DateUtil.DaysOfWeek.values()) { + if (day.name == firstDayOfWeek.getDisplayName(TextStyle.FULL, Locale.US).uppercase()) { + break + } + count += 1 + } + + return count +} + +private const val ALPHA_DIM = 0.3F +private const val SUNDAY = 1 +private const val SATURDAY = 0 diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/TimePickerDialog.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/TimePickerDialog.kt new file mode 100644 index 000000000..4f96b4601 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/component/TimePickerDialog.kt @@ -0,0 +1,91 @@ +package com.wap.wapp.feature.management.survey.component + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerDefaults +import androidx.compose.material3.TimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.management.survey.R +import java.time.LocalTime + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WappTimePickerDialog( + state: TimePickerState, + onDismissRequest: () -> Unit, + onConfirmButtonClicked: (LocalTime) -> Unit, + onDismissButtonClicked: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Card(shape = RoundedCornerShape(10.dp)) { + Column(modifier = Modifier.background(WappTheme.colors.black25)) { + TimePicker( + state = state, + colors = TimePickerDefaults.colors( + selectorColor = WappTheme.colors.yellow34, + clockDialSelectedContentColor = WappTheme.colors.black25, + clockDialUnselectedContentColor = WappTheme.colors.gray95, + clockDialColor = WappTheme.colors.black25, + periodSelectorBorderColor = Color.Transparent, + timeSelectorSelectedContainerColor = WappTheme.colors.yellow34, + timeSelectorUnselectedContainerColor = WappTheme.colors.black25, + timeSelectorSelectedContentColor = WappTheme.colors.black25, + timeSelectorUnselectedContentColor = WappTheme.colors.gray95, + periodSelectorSelectedContentColor = WappTheme.colors.black25, + periodSelectorUnselectedContentColor = WappTheme.colors.gray95, + periodSelectorSelectedContainerColor = WappTheme.colors.yellow34, + periodSelectorUnselectedContainerColor = WappTheme.colors.black25, + ), + modifier = Modifier.padding(16.dp), + ) + + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 30.dp) + .clickable { onDismissButtonClicked() }, + ) + + Text( + stringResource(R.string.select), + color = WappTheme.colors.grayBD, + style = WappTheme.typography.contentBold, + modifier = Modifier + .padding(end = 20.dp) + .clickable { + onConfirmButtonClicked( + LocalTime.of(state.hour, state.minute), + ) + }, + ) + } + } + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyDeleteDialog.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyDeleteDialog.kt new file mode 100644 index 000000000..fff64f6ff --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyDeleteDialog.kt @@ -0,0 +1,131 @@ +package com.wap.wapp.feature.management.survey.edit + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +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.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.management.survey.R + +@Composable +internal fun DeleteSurveyDialog( + deleteSurvey: () -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + .padding(horizontal = 30.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(id = R.string.delete_survey), + style = WappTheme.typography.contentBold.copy(fontSize = 20.sp), + color = WappTheme.colors.yellow34, + modifier = Modifier.padding(top = 16.dp), + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Text( + text = generateDialogContentString(), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp), + ) { + Button( + onClick = { + deleteSurvey() + onDismissRequest() + }, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.complete), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.black, + ) + } + + Button( + onClick = onDismissRequest, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.black25, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .weight(1f) + .border( + width = 1.dp, + color = WappTheme.colors.yellow34, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = stringResource(R.string.cancel), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.yellow34, + ) + } + } + } + } +} + +@Composable +private fun generateDialogContentString() = buildAnnotatedString { + append("์ •๋ง๋กœ ํ•ด๋‹น ์„ค๋ฌธ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n") + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append("ํ•ด๋‹น ์„ค๋ฌธ๊ณผ ๊ด€๋ จ๋œ ๋‹ต๋ณ€๋“ค์ด ๋ชจ๋‘ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.") + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditScreen.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditScreen.kt new file mode 100644 index 000000000..87778ea97 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditScreen.kt @@ -0,0 +1,216 @@ +package com.wap.wapp.feature.management.survey.edit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.rememberTimePickerState +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.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.feature.management.survey.R +import com.wap.wapp.feature.management.survey.SurveyFormContent +import com.wap.wapp.feature.management.survey.SurveyFormState +import com.wap.wapp.feature.management.survey.SurveyFormStateIndicator +import kotlinx.coroutines.flow.collectLatest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SurveyFormEditScreen( + viewModel: SurveyFormEditViewModel = hiltViewModel(), + surveyFormId: String, + navigateToManagement: () -> Unit, +) { + val currentRegistrationState = + viewModel.currentSurveyFormState.collectAsStateWithLifecycle().value + val eventList = viewModel.eventList.collectAsStateWithLifecycle().value + val eventSelection = viewModel.eventSelection.collectAsStateWithLifecycle().value + val title = viewModel.surveyTitle.collectAsStateWithLifecycle().value + val content = viewModel.surveyContent.collectAsStateWithLifecycle().value + val questionTitle = viewModel.questionTitle.collectAsStateWithLifecycle().value + val questionType = viewModel.questionType.collectAsStateWithLifecycle().value + val currentQuestionNumber = viewModel.currentQuestionNumber.collectAsStateWithLifecycle().value + val totalQuestionNumber = + viewModel.totalQuestionNumber.collectAsStateWithLifecycle().value + val questionList = viewModel.questionList.collectAsStateWithLifecycle().value + val timeDeadline = viewModel.timeDeadline.collectAsStateWithLifecycle().value + val dateDeadline = viewModel.dateDeadline.collectAsStateWithLifecycle().value + val snackBarHostState = remember { SnackbarHostState() } + val timePickerState = rememberTimePickerState() + var showDatePicker by remember { mutableStateOf(false) } + var showTimePicker by remember { mutableStateOf(false) } + var showDeleteSurveyDialog by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + + LaunchedEffect(true) { + viewModel.getSurveyForm(surveyFormId) + + viewModel.surveyFormEditUiEvent.collectLatest { surveyFormEditEvent -> + when (surveyFormEditEvent) { + is SurveyFormEditViewModel.SurveyFormEditUiEvent.EditSuccess -> + navigateToManagement() + + is SurveyFormEditViewModel.SurveyFormEditUiEvent.DeleteSuccess -> + navigateToManagement() + + is SurveyFormEditViewModel.SurveyFormEditUiEvent.Failure -> + snackBarHostState.showSnackbar(surveyFormEditEvent.throwable.toSupportingText()) + + is SurveyFormEditViewModel.SurveyFormEditUiEvent.ValidationError -> + snackBarHostState.showSnackbar(surveyFormEditEvent.message) + } + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + topBar = { + WappSubTopBar( + titleRes = R.string.survey_edit, + showLeftButton = true, + showRightButton = true, + leftButtonDrawableRes = drawable.ic_close, + modifier = Modifier.padding(top = 16.dp), + onClickLeftButton = navigateToManagement, + onClickRightButton = { showDeleteSurveyDialog = true }, + ) + }, + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .addFocusCleaner(focusManager) + .padding(paddingValues) // paddingValue padding + .padding(vertical = 16.dp, horizontal = 20.dp), // dp value padding + verticalArrangement = Arrangement.spacedBy(40.dp), + ) { + if (showDeleteSurveyDialog) { + DeleteSurveyDialog( + deleteSurvey = viewModel::deleteSurveyForm, + onDismissRequest = { showDeleteSurveyDialog = false }, + ) + } + + SurveyFormStateIndicator(surveyRegistrationState = currentRegistrationState) + + SurveyFormContent( + surveyRegistrationState = currentRegistrationState, + eventsState = eventList, + eventSelection = eventSelection, + title = title, + content = content, + questionTitle = questionTitle, + questionType = questionType, + dateDeadline = dateDeadline, + timeDeadline = timeDeadline, + timePickerState = timePickerState, + showDatePicker = showDatePicker, + showTimePicker = showTimePicker, + currentQuestionNumber = currentQuestionNumber, + totalQuestionNumber = totalQuestionNumber, + onDatePickerStateChanged = { state -> showDatePicker = state }, + onTimePickerStateChanged = { state -> showTimePicker = state }, + onEventContentInitialized = { viewModel.getEventList() }, + onEventSelected = { event -> viewModel.setSurveyEventSelection(event) }, + onTitleChanged = { title -> viewModel.setSurveyTitle(title) }, + onContentChanged = { content -> viewModel.setSurveyContent(content) }, + onQuestionTitleChanged = { question -> viewModel.setSurveyQuestionTitle(question) }, + onQuestionTypeChanged = { questionType -> + viewModel.setSurveyQuestionType(questionType) + }, + onNextQuestionButtonClicked = { // '>' ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + viewModel.editSurveyQuestion() // ๋‹ต๋ณ€ ์ˆ˜์ • + + viewModel.setNextQuestionNumber() // ๋‹ค์Œ ์งˆ๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + viewModel.setQuestion() + }, + onPreviousQuestionButtonClicked = { // '<' ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + // ๋‹ค๋ฅธ ์งˆ๋ฌธ์œผ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „, ์งˆ๋ฌธ ์ถ”๊ฐ€ ํ˜น์€ ์ €์žฅ + if (currentQuestionNumber == questionList.size) { + viewModel.addSurveyQuestion() + } else { + viewModel.editSurveyQuestion() + } + + viewModel.setPreviousQuestionNumber() // ์ด์ „ ์งˆ๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + viewModel.setQuestion() + }, + onAddQuestionButtonClicked = { // ๋ฌธํ•ญ ์ถ”๊ฐ€ ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + if (currentQuestionNumber == questionList.size) { + viewModel.addSurveyQuestion() // ์งˆ๋ฌธ ์ถ”๊ฐ€ + } else { + viewModel.editSurveyQuestion() + } + + viewModel.setLastQuestion() + }, + onDeleteQuestionButtonClicked = { + viewModel.deleteSurveyQuestion() + viewModel.setQuestion() + }, + onDateChanged = viewModel::setSurveyDateDeadline, + onTimeChanged = { localTime -> viewModel.setSurveyTimeDeadline(localTime) }, + onPreviousButtonClicked = { previousState -> // ์ด์ „ ๋ฒ„ํŠผ + if (previousState == SurveyFormState.QUESTION) { + viewModel.setSurveyQuestionFromQuestionList() + } + + viewModel.setSurveyFormState(previousState) + }, + onNextButtonClicked = { currentState, nextState -> // ๋‹ค์Œ ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(currentState).not()) { + return@SurveyFormContent + } + + if (currentState == SurveyFormState.QUESTION) { + if (currentQuestionNumber == questionList.size) { // ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์ธ ๊ฒฝ์šฐ + viewModel.addSurveyQuestion() + } else { + viewModel.editSurveyQuestion() + } + } + + viewModel.setSurveyFormState(nextState) + }, + onRegisterButtonClicked = { + if (viewModel.validateSurveyForm(SurveyFormState.DEADLINE)) { + viewModel.updateSurveyForm() + } + }, + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditViewModel.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditViewModel.kt new file mode 100644 index 000000000..f56638c58 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/edit/SurveyFormEditViewModel.kt @@ -0,0 +1,304 @@ +package com.wap.wapp.feature.management.survey.edit + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.event.GetEventListUseCase +import com.wap.wapp.core.domain.usecase.survey.DeleteSurveyFormAndRelatedSurveysUseCase +import com.wap.wapp.core.domain.usecase.survey.GetSurveyFormUseCase +import com.wap.wapp.core.domain.usecase.survey.UpdateSurveyFormUseCase +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.feature.management.survey.EventsState +import com.wap.wapp.feature.management.survey.SurveyFormState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +@HiltViewModel +class SurveyFormEditViewModel @Inject constructor( + private val getSurveyFormUseCase: GetSurveyFormUseCase, + private val getEventListUseCase: GetEventListUseCase, + private val updateSurveyFormUseCase: UpdateSurveyFormUseCase, + private val deleteSurveyFormAndRelatedSurveysUseCase: DeleteSurveyFormAndRelatedSurveysUseCase, +) : ViewModel() { + private val _surveyFormEditUiEvent: MutableSharedFlow = + MutableSharedFlow() + val surveyFormEditUiEvent = _surveyFormEditUiEvent.asSharedFlow() + + private val _currentSurveyFormState: MutableStateFlow = + MutableStateFlow(SurveyFormState.EVENT_SELECTION) + val currentSurveyFormState = _currentSurveyFormState.asStateFlow() + + // ๋ถˆ๋Ÿฌ์˜ฌ ์„ค๋ฌธ ํ˜•์‹ Id + private val surveyFormId: MutableStateFlow = MutableStateFlow("") + + private val _eventList: MutableStateFlow = MutableStateFlow(EventsState.Loading) + val eventList: StateFlow = _eventList.asStateFlow() + + private val _eventSelection: MutableStateFlow = + MutableStateFlow(EVENT_SELECTION_INIT) + val eventSelection = _eventSelection.asStateFlow() + + private val _surveyTitle: MutableStateFlow = MutableStateFlow("") + val surveyTitle = _surveyTitle.asStateFlow() + + private val _surveyContent: MutableStateFlow = MutableStateFlow("") + val surveyContent = _surveyContent.asStateFlow() + + private val _questionTitle: MutableStateFlow = MutableStateFlow("") + val questionTitle = _questionTitle.asStateFlow() + + private val _questionType: MutableStateFlow = + MutableStateFlow(QuestionType.SUBJECTIVE) + val questionType = _questionType.asStateFlow() + + private val _currentQuestionNumber: MutableStateFlow = MutableStateFlow(0) + val currentQuestionNumber = _currentQuestionNumber.asStateFlow() // ํ˜„์žฌ ์งˆ๋ฌธ์˜ ๋ฒˆํ˜ธ UI State + + private val _totalQuestionNumber: MutableStateFlow = MutableStateFlow(0) + val totalQuestionNumber = _totalQuestionNumber.asStateFlow() // ์ „์ฒด ์งˆ๋ฌธ์˜ ๋ฒˆํ˜ธ UI State + + private val _questionList: MutableStateFlow> = + MutableStateFlow(mutableListOf()) + val questionList = _questionList.asStateFlow() // ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์งˆ๋ฌธ ๋ชฉ๋ก + + private val _timeDeadline: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowTime()) + val timeDeadline = _timeDeadline.asStateFlow() + + private val _dateDeadline: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowDate()) + val dateDeadline = _dateDeadline.asStateFlow() + + fun getSurveyForm(surveyFormId: String) { // ์„ค๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + viewModelScope.launch { + getSurveyFormUseCase(surveyFormId = surveyFormId) + .onSuccess { surveyForm -> + setSurveyForm(surveyForm) + } + .onFailure { throwable -> + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.Failure(throwable)) + } + } + } + + private fun setSurveyForm(surveyForm: SurveyForm) { // ์ด์ „ ์„ค๋ฌธ ๋ฐ์ดํ„ฐ, ์ƒํƒœ ๋Œ€์ž… + setSurveyFormId(surveyForm.surveyFormId) + setSurveyEventSelection(EVENT_SELECTION_INIT.copy(eventId = surveyForm.eventId)) + setSurveyTitle(surveyForm.title) + setSurveyQuestionList(surveyForm.surveyQuestionList) + setSurveyQuestionFromQuestionList() + setSurveyContent(surveyForm.content) + setSurveyTimeDeadline(surveyForm.deadline.toLocalTime()) + setSurveyDateDeadline(surveyForm.deadline.toLocalDate()) + _currentQuestionNumber.value = _questionList.value.lastIndex + _totalQuestionNumber.value = _questionList.value.lastIndex + } + + fun updateSurveyForm() = viewModelScope.launch { + val surveyForm = SurveyForm( + surveyFormId = surveyFormId.value, + eventId = _eventSelection.value.eventId, + title = _surveyTitle.value, + content = _surveyContent.value, + surveyQuestionList = _questionList.value, + deadline = LocalDateTime.of(_dateDeadline.value, _timeDeadline.value), + ) + + updateSurveyFormUseCase(surveyForm = surveyForm) + .onSuccess { + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.EditSuccess) + }.onFailure { throwable -> + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.Failure(throwable)) + } + } + + fun deleteSurveyForm() = viewModelScope.launch { + deleteSurveyFormAndRelatedSurveysUseCase(surveyFormId.value) + .onSuccess { + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.EditSuccess) + }.onFailure { throwable -> + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.Failure(throwable)) + } + } + + fun getEventList() = viewModelScope.launch { + getEventListUseCase().onSuccess { eventList -> + _eventList.value = EventsState.Success(eventList) + }.onFailure { throwable -> + _surveyFormEditUiEvent.emit(SurveyFormEditUiEvent.Failure(throwable)) + } + } + + fun validateSurveyForm(currentState: SurveyFormState): Boolean { + when (currentState) { + SurveyFormState.EVENT_SELECTION -> { + if (isNotValidEventSelection()) { + emitValidationErrorMessage("ํ–‰์‚ฌ๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.INFORMATION -> { + if (isNotValidInformation()) { + emitValidationErrorMessage("์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.QUESTION -> { + if (isNotValidSurveyQuestion()) { + emitValidationErrorMessage("์งˆ๋ฌธ ๋‚ด์šฉ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.DEADLINE -> { + if (isNotValidDeadline()) { + emitValidationErrorMessage("์ตœ์†Œ ํ•˜๋ฃจ ์ด์ƒ ์„ค๋ฌธ ๋‚ ์งœ๋ฅผ ์ง€์ •ํ•˜์„ธ์š”.") + return false + } + } + } + + return true + } + + fun setSurveyFormState(nextState: SurveyFormState) { _currentSurveyFormState.value = nextState } + + private fun setSurveyFormId(surveyFormId: String) { this.surveyFormId.value = surveyFormId } + + fun setSurveyEventSelection(event: Event) { _eventSelection.value = event } + + fun setSurveyTitle(title: String) { _surveyTitle.value = title } + + fun setSurveyContent(content: String) { _surveyContent.value = content } + + fun setSurveyQuestionTitle(question: String) { _questionTitle.value = question } + + fun setSurveyQuestionType(questionType: QuestionType) { _questionType.value = questionType } + + fun setNextQuestionNumber() { _currentQuestionNumber.value += 1 } + + fun setPreviousQuestionNumber() { _currentQuestionNumber.value -= 1 } + + fun setLastQuestion() { + val lastIndex = _questionList.value.size + _currentQuestionNumber.value = lastIndex + _totalQuestionNumber.value = lastIndex + clearSurveyQuestionTitle() + setDefaultSurveyQuestionType() + } + + // ํ˜„์žฌ ์งˆ๋ฌธ ๋ฒˆํ˜ธ์— ๋งž๋Š” ์งˆ๋ฌธ + fun setQuestion() { + val currentNumber = _currentQuestionNumber.value + val totalSize = _questionList.value.size + + if (currentNumber < totalSize) { + val surveyQuestion = _questionList.value[_currentQuestionNumber.value] + setSurveyQuestionTitle(surveyQuestion.questionTitle) + setSurveyQuestionType(surveyQuestion.questionType) + return + } + } + + // ์ด์ „ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ์ง„์ž…์ ์ด ์งˆ๋ฌธ ํŽ˜์ด์ง€์ธ ๊ฒฝ์šฐ, ์‘๋‹ต ๋ชฉ๋ก์—์„œ ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ ๋…ธ์ถœ + private fun setSurveyQuestionList(surveyQuestionList: List) { + _questionList.value.addAll(surveyQuestionList) + } + + fun addSurveyQuestion() { + _questionList.value.add( + SurveyQuestion( + questionTitle = _questionTitle.value, + questionType = _questionType.value, + ), + ) + clearSurveyQuestionTitle() + } + + fun deleteSurveyQuestion() { + val totalQuestionNumber = _totalQuestionNumber.value + if (totalQuestionNumber < 1) { + emitValidationErrorMessage("์‚ญ์ œํ•  ๋ฌธํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค.") + return + } + + val currentQuestionNumber = _currentQuestionNumber.value + val lastQuestionNumber = _questionList.value.lastIndex // ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ ๋ฒˆํ˜ธ + if (currentQuestionNumber <= lastQuestionNumber) { + val questionList = _questionList.value + questionList.removeAt(currentQuestionNumber) + } // ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์งˆ๋ฌธ์„ ์‚ญ์ œํ•˜๋Š” ๊ฒฝ์šฐ๋Š” skip + + if (currentQuestionNumber > 0) { // 5/5 -> 4/4, 1/4 -> 1/3, 2/3 -> 1/2 + setPreviousQuestionNumber() + } + _totalQuestionNumber.value -= 1 + } + + fun editSurveyQuestion() { + val questionNumber = _currentQuestionNumber.value + _questionList.value[questionNumber] = SurveyQuestion( + questionTitle = _questionTitle.value, + questionType = _questionType.value, + ) + clearSurveyQuestionTitle() + } + + fun setSurveyQuestionFromQuestionList() { + // ์ž‘์„ฑ๋œ ์งˆ๋ฌธ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ, ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์€ UI์— ๋…ธ์ถœ + val lastSurveyQuestion = _questionList.value.last() + setSurveyQuestionTitle(lastSurveyQuestion.questionTitle) + setSurveyQuestionType(lastSurveyQuestion.questionType) + } + + fun setSurveyTimeDeadline(time: LocalTime) { _timeDeadline.value = time } + + fun setSurveyDateDeadline(date: LocalDate) { _dateDeadline.value = date } + + private fun setDefaultSurveyQuestionType() { _questionType.value = QuestionType.SUBJECTIVE } + + private fun clearSurveyQuestionTitle() { _questionTitle.value = "" } + + private fun isNotValidSurveyQuestion() = _questionTitle.value.isBlank() + + private fun isNotValidEventSelection() = + _eventSelection.value.eventId == EVENT_SELECTION_INIT.eventId + + private fun isNotValidInformation() = + _surveyTitle.value.isBlank() || _surveyContent.value.isBlank() + + private fun isNotValidDeadline() = _dateDeadline.value <= DateUtil.generateNowDate() + + private fun emitValidationErrorMessage(message: String) { + viewModelScope.launch { + _surveyFormEditUiEvent.emit( + SurveyFormEditUiEvent.ValidationError(message), + ) + } + } + + sealed class SurveyFormEditUiEvent { + data object EditSuccess : SurveyFormEditUiEvent() + data object DeleteSuccess : SurveyFormEditUiEvent() + data class Failure(val throwable: Throwable) : SurveyFormEditUiEvent() + data class ValidationError(val message: String) : SurveyFormEditUiEvent() + } + + companion object { + val EVENT_SELECTION_INIT = Event("", "", "", "", LocalDateTime.MIN, LocalDateTime.MAX) + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/navigation/ManagementSurveyNavigation.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/navigation/ManagementSurveyNavigation.kt new file mode 100644 index 000000000..a599e6f43 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/navigation/ManagementSurveyNavigation.kt @@ -0,0 +1,53 @@ +package com.wap.wapp.feature.management.survey.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 androidx.navigation.navOptions +import com.wap.wapp.feature.management.survey.edit.SurveyFormEditScreen +import com.wap.wapp.feature.management.survey.registration.SurveyRegistrationScreen + +fun NavController.navigateToSurveyFormRegistration(navOptions: NavOptions? = navOptions {}) { + this.navigate(ManagementSurveyRoute.surveyFormRegistrationRoute, navOptions) +} + +fun NavController.navigateToSurveyFormEdit( + surveyFormId: String, + navOptions: NavOptions? = navOptions {}, +) { + this.navigate(ManagementSurveyRoute.surveyFormEditRoute(surveyFormId), navOptions) +} + +fun NavGraphBuilder.managementSurveyNavGraph( + navigateToManagement: () -> Unit, +) { + composable(route = ManagementSurveyRoute.surveyFormRegistrationRoute) { + SurveyRegistrationScreen( + navigateToManagement = navigateToManagement, + ) + } + + composable( + route = ManagementSurveyRoute.surveyFormEditRoute("{id}"), + arguments = listOf( + navArgument("id") { + type = NavType.StringType + }, + ), + ) { navBackStackEntry -> + val surveyFormId = navBackStackEntry.arguments?.getString("id") ?: "" + SurveyFormEditScreen( + surveyFormId = surveyFormId, + navigateToManagement = navigateToManagement, + ) + } +} + +object ManagementSurveyRoute { + const val surveyFormRegistrationRoute = "surveyForm/registration" + + fun surveyFormEditRoute(surveyFormId: String) = "surveyForm/edit/$surveyFormId" +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationScreen.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationScreen.kt new file mode 100644 index 000000000..0b528b893 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationScreen.kt @@ -0,0 +1,199 @@ +package com.wap.wapp.feature.management.survey.registration + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.rememberTimePickerState +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.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.feature.management.survey.R +import com.wap.wapp.feature.management.survey.SurveyFormContent +import com.wap.wapp.feature.management.survey.SurveyFormState +import com.wap.wapp.feature.management.survey.SurveyFormStateIndicator +import kotlinx.coroutines.flow.collectLatest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SurveyRegistrationScreen( + viewModel: SurveyFormRegistrationViewModel = hiltViewModel(), + navigateToManagement: () -> Unit, +) { + val currentRegistrationState = + viewModel.currentSurveyFormState.collectAsStateWithLifecycle().value + val eventList = viewModel.eventList.collectAsStateWithLifecycle().value + val eventSelection = viewModel.eventSelection.collectAsStateWithLifecycle().value + val title = viewModel.surveyTitle.collectAsStateWithLifecycle().value + val content = viewModel.surveyContent.collectAsStateWithLifecycle().value + val questionTitle = viewModel.questionTitle.collectAsStateWithLifecycle().value + val questionType = viewModel.questionType.collectAsStateWithLifecycle().value + val currentQuestionNumber = viewModel.currentQuestionNumber.collectAsStateWithLifecycle().value + val totalQuestionNumber = + viewModel.totalQuestionNumber.collectAsStateWithLifecycle().value + val questionList = viewModel.questionList.collectAsStateWithLifecycle().value + val timeDeadline = viewModel.timeDeadline.collectAsStateWithLifecycle().value + val dateDeadline = viewModel.dateDeadline.collectAsStateWithLifecycle().value + val snackBarHostState = remember { SnackbarHostState() } + val timePickerState = rememberTimePickerState() + var showDatePicker by remember { mutableStateOf(false) } + var showTimePicker by remember { mutableStateOf(false) } + + LaunchedEffect(true) { + viewModel.surveyRegistrationEvent.collectLatest { + when (it) { + is SurveyFormRegistrationViewModel.SurveyRegistrationEvent.Failure -> { + snackBarHostState.showSnackbar(it.error.toSupportingText()) + } + + is SurveyFormRegistrationViewModel.SurveyRegistrationEvent.ValidationError -> { + snackBarHostState.showSnackbar(it.message) + } + + is SurveyFormRegistrationViewModel.SurveyRegistrationEvent.Success -> { + navigateToManagement() + } + } + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets(0.dp), + topBar = { + WappSubTopBar( + titleRes = R.string.survey_registeration, + showLeftButton = true, + onClickLeftButton = navigateToManagement, + leftButtonDrawableRes = drawable.ic_close, + modifier = Modifier.padding(top = 16.dp), + ) + }, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 16.dp, horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(40.dp), + ) { + SurveyFormStateIndicator(surveyRegistrationState = currentRegistrationState) + + SurveyFormContent( + surveyRegistrationState = currentRegistrationState, + eventsState = eventList, + eventSelection = eventSelection, + title = title, + content = content, + questionTitle = questionTitle, + questionType = questionType, + dateDeadline = dateDeadline, + timeDeadline = timeDeadline, + timePickerState = timePickerState, + showDatePicker = showDatePicker, + showTimePicker = showTimePicker, + currentQuestionNumber = currentQuestionNumber, + totalQuestionNumber = totalQuestionNumber, + onDatePickerStateChanged = { state -> showDatePicker = state }, + onTimePickerStateChanged = { state -> showTimePicker = state }, + onEventContentInitialized = { viewModel.getEventList() }, + onEventSelected = { event -> viewModel.setSurveyEventSelection(event) }, + onTitleChanged = { title -> viewModel.setSurveyTitle(title) }, + onContentChanged = { content -> viewModel.setSurveyContent(content) }, + onQuestionTitleChanged = { question -> viewModel.setSurveyQuestionTitle(question) }, + onQuestionTypeChanged = { questionType -> + viewModel.setSurveyQuestionType(questionType) + }, + onNextQuestionButtonClicked = { // '>' ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + viewModel.editSurveyQuestion() // ๋‹ต๋ณ€ ์ˆ˜์ • + + viewModel.setNextQuestionNumber() // ๋‹ค์Œ ์งˆ๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + viewModel.setQuestion() + }, + onPreviousQuestionButtonClicked = { // '<' ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + // ๋‹ค๋ฅธ ์งˆ๋ฌธ์œผ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „, ์งˆ๋ฌธ ์ถ”๊ฐ€ ํ˜น์€ ์ €์žฅ + if (currentQuestionNumber == questionList.size) { + viewModel.addSurveyQuestion() + } else { + viewModel.editSurveyQuestion() + } + + viewModel.setPreviousQuestionNumber() // ์ด์ „ ์งˆ๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + viewModel.setQuestion() + }, + onAddQuestionButtonClicked = { // ๋ฌธํ•ญ ์ถ”๊ฐ€ ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(SurveyFormState.QUESTION).not()) { + return@SurveyFormContent + } + + if (currentQuestionNumber == questionList.size) { + viewModel.addSurveyQuestion() // ์งˆ๋ฌธ ์ถ”๊ฐ€ + } else { + viewModel.editSurveyQuestion() + } + + viewModel.setLastQuestion() + }, + onDeleteQuestionButtonClicked = { + viewModel.deleteSurveyQuestion() + viewModel.setQuestion() + }, + onDateChanged = viewModel::setSurveyDateDeadline, + onTimeChanged = { localTime -> viewModel.setSurveyTimeDeadline(localTime) }, + onPreviousButtonClicked = { previousState -> // ์ด์ „ ๋ฒ„ํŠผ + if (previousState == SurveyFormState.QUESTION) { + viewModel.setSurveyQuestionFromQuestionList() + } + + viewModel.setSurveyFormState(previousState) + }, + onNextButtonClicked = { currentState, nextState -> // ๋‹ค์Œ ๋ฒ„ํŠผ + if (viewModel.validateSurveyForm(currentState).not()) { + return@SurveyFormContent + } + + if (currentState == SurveyFormState.QUESTION) { + if (currentQuestionNumber == questionList.size) { // ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์ธ ๊ฒฝ์šฐ + viewModel.addSurveyQuestion() + } else { + viewModel.editSurveyQuestion() + } + } + + viewModel.setSurveyFormState(nextState) + }, + onRegisterButtonClicked = { + if (viewModel.validateSurveyForm(SurveyFormState.DEADLINE)) { + viewModel.registerSurvey() + } + }, + ) + } + } +} diff --git a/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationViewModel.kt b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationViewModel.kt new file mode 100644 index 000000000..694276966 --- /dev/null +++ b/feature/management-survey/src/main/java/com/wap/wapp/feature/management/survey/registration/SurveyFormRegistrationViewModel.kt @@ -0,0 +1,249 @@ +package com.wap.wapp.feature.management.survey.registration + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.event.GetEventListUseCase +import com.wap.wapp.core.domain.usecase.survey.PostSurveyFormUseCase +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.core.model.survey.SurveyQuestion +import com.wap.wapp.feature.management.survey.EventsState +import com.wap.wapp.feature.management.survey.SurveyFormState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +@HiltViewModel +class SurveyFormRegistrationViewModel @Inject constructor( + private val registerSurveyUseCase: PostSurveyFormUseCase, + private val getEventListUseCase: GetEventListUseCase, +) : ViewModel() { + private val _surveyRegistrationEvent: MutableSharedFlow = + MutableSharedFlow() + val surveyRegistrationEvent = _surveyRegistrationEvent.asSharedFlow() + + private val _currentSurveyFormState: MutableStateFlow = + MutableStateFlow(SurveyFormState.EVENT_SELECTION) + val currentSurveyFormState = _currentSurveyFormState.asStateFlow() + + private val _eventList: MutableStateFlow = MutableStateFlow(EventsState.Loading) + val eventList: StateFlow = _eventList.asStateFlow() + + private val _eventSelection: MutableStateFlow = + MutableStateFlow(EVENT_SELECTION_INIT) + val eventSelection = _eventSelection.asStateFlow() + + private val _surveyTitle: MutableStateFlow = MutableStateFlow("") + val surveyTitle = _surveyTitle.asStateFlow() + + private val _surveyContent: MutableStateFlow = MutableStateFlow("") + val surveyContent = _surveyContent.asStateFlow() + + private val _questionTitle: MutableStateFlow = MutableStateFlow("") + val questionTitle = _questionTitle.asStateFlow() + + private val _questionType: MutableStateFlow = + MutableStateFlow(QuestionType.SUBJECTIVE) + val questionType = _questionType.asStateFlow() + + private val _currentQuestionNumber: MutableStateFlow = MutableStateFlow(0) + val currentQuestionNumber = _currentQuestionNumber.asStateFlow() // ํ˜„์žฌ ์งˆ๋ฌธ์˜ ๋ฒˆํ˜ธ UI State + + private val _totalQuestionNumber: MutableStateFlow = MutableStateFlow(0) + val totalQuestionNumber = _totalQuestionNumber.asStateFlow() // ์ „์ฒด ์งˆ๋ฌธ์˜ ๋ฒˆํ˜ธ UI State + + private val _questionList: MutableStateFlow> = + MutableStateFlow(mutableListOf()) + val questionList = _questionList.asStateFlow() // ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์งˆ๋ฌธ ๋ชฉ๋ก + + private val _timeDeadline: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowTime()) + val timeDeadline = _timeDeadline.asStateFlow() + + private val _dateDeadline: MutableStateFlow = + MutableStateFlow(DateUtil.generateNowDate()) + val dateDeadline = _dateDeadline.asStateFlow() + + fun getEventList() = viewModelScope.launch { + getEventListUseCase().onSuccess { eventList -> + _eventList.value = EventsState.Success(eventList) + }.onFailure { throwable -> + _surveyRegistrationEvent.emit(SurveyRegistrationEvent.Failure(throwable)) + } + } + + fun registerSurvey() = viewModelScope.launch { + registerSurveyUseCase( + event = _eventSelection.value, + title = _surveyTitle.value, + content = _surveyContent.value, + surveyQuestionList = _questionList.value, + deadlineDate = _dateDeadline.value, + deadlineTime = _timeDeadline.value, + ).onSuccess { + _surveyRegistrationEvent.emit(SurveyRegistrationEvent.Success) + }.onFailure { throwable -> + _surveyRegistrationEvent.emit(SurveyRegistrationEvent.Failure(throwable)) + } + } + + fun validateSurveyForm(currentState: SurveyFormState): Boolean { + when (currentState) { + SurveyFormState.EVENT_SELECTION -> { + if (isNotValidEventSelection()) { + emitValidationErrorMessage("ํ–‰์‚ฌ๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.INFORMATION -> { + if (isNotValidInformation()) { + emitValidationErrorMessage("์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.QUESTION -> { + if (isNotValidSurveyQuestion()) { + emitValidationErrorMessage("์งˆ๋ฌธ ๋‚ด์šฉ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.") + return false + } + } + + SurveyFormState.DEADLINE -> { + if (isNotValidDeadline()) { + emitValidationErrorMessage("์ตœ์†Œ ํ•˜๋ฃจ ์ด์ƒ ์„ค๋ฌธ ๋‚ ์งœ๋ฅผ ์ง€์ •ํ•˜์„ธ์š”.") + return false + } + } + } + return true + } + + fun setSurveyFormState(nextState: SurveyFormState) { _currentSurveyFormState.value = nextState } + + fun setSurveyEventSelection(event: Event) { _eventSelection.value = event } + + fun setSurveyTitle(title: String) { _surveyTitle.value = title } + + fun setSurveyContent(content: String) { _surveyContent.value = content } + + fun setSurveyQuestionTitle(title: String) { _questionTitle.value = title } + + fun setSurveyQuestionType(type: QuestionType) { _questionType.value = type } + + fun setNextQuestionNumber() { _currentQuestionNumber.value += 1 } + + fun setPreviousQuestionNumber() { _currentQuestionNumber.value -= 1 } + + fun setLastQuestion() { + val lastIndex = _questionList.value.size + _currentQuestionNumber.value = lastIndex + _totalQuestionNumber.value = lastIndex + clearSurveyQuestionTitle() + setDefaultSurveyQuestionType() + } + + fun setQuestion() { + val currentNumber = _currentQuestionNumber.value + val totalSize = _questionList.value.size + + if (currentNumber < totalSize) { + val surveyQuestion = _questionList.value[_currentQuestionNumber.value] + setSurveyQuestionTitle(surveyQuestion.questionTitle) + setSurveyQuestionType(surveyQuestion.questionType) + return + } + } + + fun addSurveyQuestion() { + _questionList.value.add( + SurveyQuestion( + questionTitle = _questionTitle.value, + questionType = _questionType.value, + ), + ) + clearSurveyQuestionTitle() + } + + fun deleteSurveyQuestion() { + val totalQuestionNumber = _totalQuestionNumber.value + if (totalQuestionNumber < 1) { + emitValidationErrorMessage("์‚ญ์ œํ•  ๋ฌธํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค.") + return + } + + val currentQuestionNumber = _currentQuestionNumber.value + val lastQuestionNumber = _questionList.value.lastIndex // ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ ๋ฒˆํ˜ธ + if (currentQuestionNumber <= lastQuestionNumber) { + val questionList = _questionList.value + questionList.removeAt(currentQuestionNumber) + } // ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์งˆ๋ฌธ์„ ์‚ญ์ œํ•˜๋Š” ๊ฒฝ์šฐ๋Š” skip + + if (currentQuestionNumber > 0) { // 5/5 -> 4/4, 1/4 -> 1/3, 2/3 -> 1/2 + setPreviousQuestionNumber() + } + _totalQuestionNumber.value -= 1 + } + + fun editSurveyQuestion() { + val questionNumber = _currentQuestionNumber.value + _questionList.value[questionNumber] = SurveyQuestion( + questionTitle = _questionTitle.value, + questionType = _questionType.value, + ) + clearSurveyQuestionTitle() + } + + fun setSurveyQuestionFromQuestionList() { + // ์ž‘์„ฑ๋œ ์งˆ๋ฌธ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ, ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์€ UI์— ๋…ธ์ถœ + val lastSurveyQuestion = _questionList.value.last() + setSurveyQuestionTitle(lastSurveyQuestion.questionTitle) + setSurveyQuestionType(lastSurveyQuestion.questionType) + } + + fun setSurveyTimeDeadline(time: LocalTime) { _timeDeadline.value = time } + + fun setSurveyDateDeadline(date: LocalDate) { _dateDeadline.value = date } + + private fun setDefaultSurveyQuestionType() { _questionType.value = QuestionType.SUBJECTIVE } + + private fun clearSurveyQuestionTitle() { _questionTitle.value = "" } + + private fun isNotValidSurveyQuestion() = _questionTitle.value.isBlank() + + private fun isNotValidEventSelection() = + _eventSelection.value.eventId == EVENT_SELECTION_INIT.eventId + + private fun isNotValidInformation() = + _surveyTitle.value.isBlank() || _surveyContent.value.isBlank() + + private fun isNotValidDeadline() = _dateDeadline.value <= DateUtil.generateNowDate() + + private fun emitValidationErrorMessage(message: String) { + viewModelScope.launch { + _surveyRegistrationEvent.emit( + SurveyRegistrationEvent.ValidationError(message), + ) + } + } + + sealed class SurveyRegistrationEvent { + data class ValidationError(val message: String) : SurveyRegistrationEvent() + data class Failure(val error: Throwable) : SurveyRegistrationEvent() + data object Success : SurveyRegistrationEvent() + } + + companion object { + val EVENT_SELECTION_INIT = Event("", "", "", "", LocalDateTime.MIN, LocalDateTime.MAX) + } +} diff --git a/feature/management-survey/src/main/res/values/strings.xml b/feature/management-survey/src/main/res/values/strings.xml new file mode 100644 index 000000000..21a76b761 --- /dev/null +++ b/feature/management-survey/src/main/res/values/strings.xml @@ -0,0 +1,56 @@ + + + ์ผ์ • ๋“ฑ๋ก + ์ˆ˜์ • ์™„๋ฃŒ + ์„ค๋ฌธ ๋“ฑ๋ก + / 4 + ๋ฌธํ•ญ ์ถ”๊ฐ€ + ๋ฌธํ•ญ ์‚ญ์ œ + ์„ค๋ฌธ ๋“ฑ๋กํ•˜๊ธฐ + ์„ค๋ฌธ ์‚ญ์ œ + ์ทจ์†Œ + ์™„๋ฃŒ + ์„ ํƒ + ์„ค๋ฌธ ๊ธฐ๊ฐ„ ์„ค์ • + ์„ค๋ฌธ์˜ ๋งˆ๊ฐ ๊ธฐํ•œ์„ ์„ค์ •ํ•˜์„ธ์š”. + ์‹œ์ž‘ ๋‚ ์งœ + ์‹œ์ž‘ ์‹œ๊ฐ„ + ์ข…๋ฃŒ ๋‚ ์งœ + ์ข…๋ฃŒ ์‹œ๊ฐ„ + ๋‚ ์งœ + ์‹œ๊ฐ„ + ์„ค๋ฌธ ์ œ๋ชฉ ์ž…๋ ฅ + ์„ค๋ฌธ์„ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ์ œ๋ชฉ๊ณผ ์†Œ๊ฐœ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์„ค๋ฌธ ์ œ๋ชฉ + ์„ค๋ฌธ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š” + ์„ค๋ฌธ ์†Œ๊ฐœ + ์„ค๋ฌธ ์†Œ๊ฐœ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ์„ค๋ฌธ ๋ฌธํ•ญ ์ž‘์„ฑ + ๋ถ€์›๋“ค์ด ์•Œ์•„๋ณด๊ธฐ ์‰ฝ๋„๋ก ์ž์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. + ๋ฌธํ•ญ ์งˆ๋ฌธ + ์„ค๋ฌธ ๋ฌธํ•ญ์˜ ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”. + ๋ฌธํ•ญ ์œ ํ˜• + ์ฃผ๊ด€์‹ + ๊ฐ๊ด€์‹ + ์ฃผ๊ด€์‹ ๋‹ต๋ณ€์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. + ์ข‹์Œ + ๋ณดํ†ต + ๋‚˜์จ + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์™„๋ฒฝํ•˜๊ณ , \nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋งŽ์•˜์Œ. + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์›ํ™œํ•˜๊ณ ,\nํ–‰์‚ฌ ๋‚ด์šฉ์ด ์ ๋‹นํ•จ. + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์•„์‰ฌ์› ๊ณ ,\nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋ถ€์กฑํ•จ. + ํ–‰์‚ฌ ์„ ํƒ + ์„ค๋ฌธ์„ ๋“ฑ๋กํ•  ํ–‰์‚ฌ๋ฅผ ์„ ํƒํ•˜์„ธ์š”. + ์„ค๋ฌธ ํ™•์ธ + ์ด๋ฆ„ + ๋‹ค์Œ + ์ผ์ • + "์ด์ „ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + "๋‹ค์Œ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + ์„ค๋ฌธ ์ˆ˜์ • + ์ด์ „ + ๋‹ค์Œ ์งˆ๋ฌธ + ์ด์ „ ์งˆ๋ฌธ + Survey Form Registration Content Animated Content + ์„ค๋ฌธ ๋“ฑ๋ก + diff --git a/feature/management/.gitignore b/feature/management/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/management/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/management/build.gradle.kts b/feature/management/build.gradle.kts new file mode 100644 index 000000000..34f851607 --- /dev/null +++ b/feature/management/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.management" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:designsystem")) + implementation(project(":core:designresource")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/management/consumer-rules.pro b/feature/management/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/management/proguard-rules.pro b/feature/management/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/management/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/management/src/androidTest/java/com/wap/wapp/feature/management/ExampleInstrumentedTest.kt b/feature/management/src/androidTest/java/com/wap/wapp/feature/management/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..e9a091450 --- /dev/null +++ b/feature/management/src/androidTest/java/com/wap/wapp/feature/management/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.management + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.Assert.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("com.wap.wapp.feature.manage.test", appContext.packageName) + } +} diff --git a/feature/management/src/main/AndroidManifest.xml b/feature/management/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/management/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementEventCard.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementEventCard.kt new file mode 100644 index 000000000..565ff8ba7 --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementEventCard.kt @@ -0,0 +1,165 @@ +package com.wap.wapp.feature.management + +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.fillMaxSize +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.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappButton +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.model.event.Event + +@Composable +internal fun ManagementEventCard( + eventsState: ManagementViewModel.EventsState, + onCardClicked: (String) -> Unit, + onAddEventButtonClicked: () -> Unit, +) { + Card( + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + modifier = Modifier.padding(horizontal = 8.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp), + ) { + Text( + text = stringResource(R.string.event), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + ManagementEventContent( + eventsState = eventsState, + onCardClicked = onCardClicked, + onAddEventButtonClicked = onAddEventButtonClicked, + ) + } + } +} + +@Composable +private fun ManagementEventContent( + eventsState: ManagementViewModel.EventsState, + onCardClicked: (String) -> Unit, + onAddEventButtonClicked: () -> Unit, +) { + when (eventsState) { + is ManagementViewModel.EventsState.Loading -> CircleLoader( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + ) + + is ManagementViewModel.EventsState.Success -> { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.heightIn(max = 166.dp), + ) { + itemsIndexed( + items = eventsState.events, + key = { index, event -> event.eventId }, + ) { currentIndex, event -> + ManagementEventItem( + item = event, + cardColor = ManagementCardColor(currentIndex = currentIndex), + onCardClicked = onCardClicked, + ) + } + } + + WappButton( + textRes = R.string.event_registration, + onClick = { onAddEventButtonClicked() }, + ) + } + + is ManagementViewModel.EventsState.Failure -> {} + } +} + +@Composable +private fun ManagementEventItem( + item: Event, + cardColor: Color, + onCardClicked: (String) -> Unit, +) { + Card( + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxSize() + .clickable { onCardClicked(item.eventId) }, + colors = CardDefaults.cardColors(containerColor = cardColor), + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .weight(1f), + ) { + Column { + Text( + text = item.title, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + maxLines = 1, + ) + + Text( + text = item.content, + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.white, + maxLines = 1, + ) + } + + Text( + text = stringResource( + id = R.string.event_duration, + item.startDateTime.format(DateUtil.yyyyMMddFormatter), + ), + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.white, + ) + } + + Icon( + painter = painterResource(com.wap.wapp.core.designresource.R.drawable.ic_forward), + contentDescription = stringResource(R.string.detail_icon_description), + tint = WappTheme.colors.yellow34, + modifier = Modifier.size(20.dp), + ) + } + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementGuestScreen.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementGuestScreen.kt new file mode 100644 index 000000000..0fd0ec626 --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementGuestScreen.kt @@ -0,0 +1,55 @@ +package com.wap.wapp.feature.management + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton + +@Composable +internal fun ManagementGuestScreen(onButtonClicked: () -> Unit) { + Surface( + color = WappTheme.colors.backgroundBlack, + modifier = Modifier.fillMaxSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(R.string.management_guest_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Text( + text = stringResource(R.string.management_guest_content), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + + WappButton( + textRes = R.string.go_to_signin, + onClick = onButtonClicked, + modifier = Modifier.padding(horizontal = 32.dp), + ) + } + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementScreen.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementScreen.kt new file mode 100644 index 000000000..462291d24 --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementScreen.kt @@ -0,0 +1,134 @@ +package com.wap.wapp.feature.management + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappLeftMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.model.user.UserRole +import com.wap.wapp.feature.management.validation.ManagementValidationScreen +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun ManagementRoute( + navigateToEventEdit: (String) -> Unit, + navigateToEventRegistration: () -> Unit, + navigateToSurveyRegistration: () -> Unit, + navigateToSurveyFormEdit: (String) -> Unit, + navigateToSignIn: () -> Unit, + viewModel: ManagementViewModel = hiltViewModel(), +) { + var showValidationScreen by rememberSaveable { mutableStateOf(false) } + var showGuestScreen by rememberSaveable { mutableStateOf(false) } + val snackBarHostState = remember { SnackbarHostState() } + val surveyFormsState by viewModel.surveyFormList.collectAsStateWithLifecycle() + val eventsState by viewModel.eventList.collectAsStateWithLifecycle() + + LaunchedEffect(true) { + viewModel.getUserRole() // ์œ ์ € ๊ถŒํ•œ ๊ฒ€์ƒ‰ + + viewModel.userRole.collectLatest { userRoleUiState -> + when (userRoleUiState) { + is ManagementViewModel.UserRoleUiState.Init -> {} + is ManagementViewModel.UserRoleUiState.Success -> { + when (userRoleUiState.userRole) { + UserRole.GUEST -> { showGuestScreen = true } + UserRole.MEMBER -> { showValidationScreen = true } + UserRole.MANAGER -> viewModel.getEventSurveyList() + } + } + } + } + + viewModel.errorFlow.collectLatest { throwable -> + snackBarHostState.showSnackbar(message = throwable.toSupportingText()) + } + } + + if (showGuestScreen) { // ๋น„ํšŒ์›์ธ ๊ฒฝ์šฐ + ManagementGuestScreen(onButtonClicked = navigateToSignIn) + return + } + + if (showValidationScreen) { // ํšŒ์›์ธ ๊ฒฝ์šฐ + ManagementValidationScreen( + onValidationSuccess = { + showValidationScreen = false + viewModel.getUserRole() // ๋งค๋‹ˆ์ € ๊ถŒํ•œ ์žฌ ๊ฒ€์ฆ + }, + ) + return + } + + ManagementScreen( + snackBarHostState = snackBarHostState, + surveyFormsState = surveyFormsState, + eventsState = eventsState, + navigateToEventRegistration = navigateToEventRegistration, + navigateToSurveyRegistration = navigateToSurveyRegistration, + navigateToSurveyFormEdit = navigateToSurveyFormEdit, + navigateToEventEdit = navigateToEventEdit, + ) +} + +@Composable +internal fun ManagementScreen( + snackBarHostState: SnackbarHostState, + surveyFormsState: ManagementViewModel.SurveyFormsState, + eventsState: ManagementViewModel.EventsState, + navigateToEventEdit: (String) -> Unit, + navigateToEventRegistration: () -> Unit, + navigateToSurveyRegistration: () -> Unit, + navigateToSurveyFormEdit: (String) -> Unit, +) { + Scaffold( + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { + item { + WappLeftMainTopBar( + titleRes = R.string.management, + contentRes = R.string.management_content, + ) + + ManagementEventCard( + eventsState = eventsState, + onCardClicked = navigateToEventEdit, + onAddEventButtonClicked = navigateToEventRegistration, + ) + + ManagementSurveyCard( + surveyFormsState = surveyFormsState, + onCardClicked = navigateToSurveyFormEdit, + onAddSurveyButtonClicked = navigateToSurveyRegistration, + ) + } + } + } +} + +@Composable +internal fun ManagementCardColor(currentIndex: Int): Color = + if (currentIndex % 2 == 0) { + WappTheme.colors.black82 + } else { + WappTheme.colors.black42 + } diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementSurveyCard.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementSurveyCard.kt new file mode 100644 index 000000000..60f279819 --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementSurveyCard.kt @@ -0,0 +1,170 @@ +package com.wap.wapp.feature.management + +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.fillMaxSize +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.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappButton +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.designresource.R +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.feature.management.R.string + +@Composable +internal fun ManagementSurveyCard( + surveyFormsState: ManagementViewModel.SurveyFormsState, + onCardClicked: (String) -> Unit, + onAddSurveyButtonClicked: () -> Unit, +) { + Card( + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors(containerColor = WappTheme.colors.black25), + modifier = Modifier + .padding(vertical = 20.dp) + .padding(horizontal = 8.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(string.survey), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + ManagementSurveyContent( + surveyFormsState = surveyFormsState, + onCardClicked = onCardClicked, + onAddSurveyButtonClicked = onAddSurveyButtonClicked, + ) + } + } +} + +@Composable +private fun ManagementSurveyContent( + surveyFormsState: ManagementViewModel.SurveyFormsState, + onCardClicked: (String) -> Unit, + onAddSurveyButtonClicked: () -> Unit, +) { + when (surveyFormsState) { + is ManagementViewModel.SurveyFormsState.Loading -> CircleLoader( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + ) + + is ManagementViewModel.SurveyFormsState.Success -> { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.heightIn(max = 166.dp), + ) { + itemsIndexed( + items = surveyFormsState.surveyForms, + key = { index, survey -> survey.surveyFormId }, + ) { currentIndex, survey -> + ManagementSurveyItemCard( + item = survey, + cardColor = ManagementCardColor(currentIndex = currentIndex), + onCardClicked = { surveyId -> onCardClicked(surveyId) }, + ) + } + } + + WappButton( + textRes = string.add_survey, + onClick = { onAddSurveyButtonClicked() }, + ) + } + + is ManagementViewModel.SurveyFormsState.Failure -> {} + } +} + +@Composable +private fun ManagementSurveyItemCard( + item: SurveyForm, + cardColor: Color, + onCardClicked: (String) -> Unit, +) { + Card( + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxSize() + .clickable { onCardClicked(item.surveyFormId) }, + colors = CardDefaults.cardColors(containerColor = cardColor), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .weight(1f), + ) { + Column { + Text( + text = item.title, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + maxLines = 1, + ) + + Text( + text = item.content, + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.white, + maxLines = 1, + ) + } + + Text( + text = stringResource( + id = string.surveyForm_deadline, + item.deadline.format(DateUtil.yyyyMMddFormatter), + ), + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.white, + maxLines = 1, + ) + } + + Icon( + painter = painterResource(id = R.drawable.ic_forward), + contentDescription = stringResource(string.detail_icon_description), + tint = WappTheme.colors.yellow34, + modifier = Modifier.size(20.dp), + ) + } + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementViewModel.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementViewModel.kt new file mode 100644 index 000000000..96daae7ab --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/ManagementViewModel.kt @@ -0,0 +1,93 @@ +package com.wap.wapp.feature.management + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil.generateNowDate +import com.wap.wapp.core.domain.usecase.event.GetMonthEventListUseCase +import com.wap.wapp.core.domain.usecase.survey.GetSurveyFormListUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserRoleUseCase +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.model.user.UserRole +import dagger.hilt.android.lifecycle.HiltViewModel +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 +import javax.inject.Inject + +@HiltViewModel +class ManagementViewModel @Inject constructor( + private val getUserRoleUseCase: GetUserRoleUseCase, + private val getSurveyFormListUseCase: GetSurveyFormListUseCase, + private val getMonthEventListUseCase: GetMonthEventListUseCase, +) : ViewModel() { + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow: SharedFlow = _errorFlow.asSharedFlow() + + private val _userRole: MutableStateFlow = + MutableStateFlow(UserRoleUiState.Init) + val userRole: StateFlow = _userRole.asStateFlow() + + private val _surveyFormList: MutableStateFlow = + MutableStateFlow(SurveyFormsState.Loading) + val surveyFormList: StateFlow = _surveyFormList.asStateFlow() + + private val _eventList: MutableStateFlow = MutableStateFlow(EventsState.Loading) + val eventList: StateFlow = _eventList.asStateFlow() + + fun getUserRole() = viewModelScope.launch { + getUserRoleUseCase() + .onSuccess { userRole -> + _userRole.value = UserRoleUiState.Success(userRole) + } + .onFailure { exception -> + _errorFlow.emit(exception) + } + } + + fun getEventSurveyList() = viewModelScope.launch { + getMonthEventList() + getSurveyFormList() + } + + private suspend fun getMonthEventList() { + _eventList.value = EventsState.Loading + getMonthEventListUseCase(generateNowDate()).onSuccess { events -> + _eventList.value = EventsState.Success(events) + }.onFailure { exception -> + _errorFlow.emit(exception) + _eventList.value = EventsState.Failure(exception) + } + } + + private suspend fun getSurveyFormList() { + _surveyFormList.value = SurveyFormsState.Loading + getSurveyFormListUseCase().onSuccess { surveyForms -> + _surveyFormList.value = SurveyFormsState.Success(surveyForms) + }.onFailure { exception -> + _errorFlow.emit(exception) + _eventList.value = EventsState.Failure(exception) + } + } + + sealed class UserRoleUiState { + data object Init : UserRoleUiState() + data class Success(val userRole: UserRole) : UserRoleUiState() + } + + sealed class EventsState { + data object Loading : EventsState() + data class Success(val events: List) : EventsState() + data class Failure(val throwable: Throwable) : EventsState() + } + + sealed class SurveyFormsState { + data object Loading : SurveyFormsState() + data class Success(val surveyForms: List) : SurveyFormsState() + data class Failure(val throwable: Throwable) : SurveyFormsState() + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/navigation/ManagementNavigation.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/navigation/ManagementNavigation.kt new file mode 100644 index 000000000..3a5307f8b --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/navigation/ManagementNavigation.kt @@ -0,0 +1,32 @@ +package com.wap.wapp.feature.management.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.management.ManagementRoute + +const val managementNavigationRoute = "management_route" + +fun NavController.navigateToManagement(navOptions: NavOptions? = navOptions {}) { + this.navigate(managementNavigationRoute, navOptions) +} + +fun NavGraphBuilder.managementScreen( + navigateToEventEdit: (String) -> Unit, + navigateToEventRegistration: () -> Unit, + navigateToSurveyRegistration: () -> Unit, + navigateToSurveyFormEdit: (String) -> Unit, + navigateToSignIn: () -> Unit, +) { + composable(route = managementNavigationRoute) { + ManagementRoute( + navigateToEventEdit = navigateToEventEdit, + navigateToEventRegistration = navigateToEventRegistration, + navigateToSurveyRegistration = navigateToSurveyRegistration, + navigateToSurveyFormEdit = navigateToSurveyFormEdit, + navigateToSignIn = navigateToSignIn, + ) + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationScreen.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationScreen.kt new file mode 100644 index 000000000..9f70cdcaf --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationScreen.kt @@ -0,0 +1,102 @@ +package com.wap.wapp.feature.management.validation + +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.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +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.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappTextField +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.feature.management.R +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun ManagementValidationScreen( + viewModel: ManagementValidationViewModel = hiltViewModel(), + onValidationSuccess: () -> Unit, +) { + val code by viewModel.managementCode.collectAsStateWithLifecycle() + val isError by viewModel.isError.collectAsStateWithLifecycle() + val errorSupportingText by viewModel.errorSupportingText.collectAsStateWithLifecycle() + val snackBarHostState = remember { SnackbarHostState() } + val focusManager = LocalFocusManager.current + + LaunchedEffect(true) { + viewModel.managementCodeUiState.collectLatest { + when (it) { + is ManagementValidationViewModel.ManagementCodeUiState.Init -> {} + + is ManagementValidationViewModel.ManagementCodeUiState.Success -> + onValidationSuccess() + + is ManagementValidationViewModel.ManagementCodeUiState.Failure -> + snackBarHostState.showSnackbar(it.throwable.toSupportingText()) + } + } + } + + Scaffold( + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + ) { paddingValues -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .addFocusCleaner(focusManager), + ) { + Text( + text = stringResource(R.string.management_dialog_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + ) + + Text( + text = stringResource(R.string.management_dialog_content), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + + WappTextField( + value = code, + onValueChanged = viewModel::setManagementCode, + label = R.string.code, + isError = isError, + supportingText = stringResource(id = errorSupportingText), + ) + + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + + WappButton( + onClick = { viewModel.checkManagementCode() }, + isEnabled = code.isNotBlank(), + modifier = Modifier.padding(horizontal = 32.dp), + ) + } + } +} diff --git a/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationViewModel.kt b/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationViewModel.kt new file mode 100644 index 000000000..77551333f --- /dev/null +++ b/feature/management/src/main/java/com/wap/wapp/feature/management/validation/ManagementValidationViewModel.kt @@ -0,0 +1,59 @@ +package com.wap.wapp.feature.management.validation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.model.CodeValidation +import com.wap.wapp.core.domain.usecase.management.CheckManagementCodeUseCase +import com.wap.wapp.feature.management.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ManagementValidationViewModel @Inject constructor( + private val checkManagementCodeUseCase: CheckManagementCodeUseCase, +) : ViewModel() { + private val _managementCodeUiState: MutableStateFlow = + MutableStateFlow(ManagementCodeUiState.Init) + val managementCodeUiState: StateFlow + get() = _managementCodeUiState + + private val _managementCode: MutableStateFlow = MutableStateFlow("") + val managementCode: StateFlow get() = _managementCode + + private val _isError: MutableStateFlow = MutableStateFlow(false) + val isError: StateFlow get() = _isError + + private val _errorSupportingText: MutableStateFlow = + MutableStateFlow(R.string.management_dialog_hint) + val errorSupportingText: StateFlow get() = _errorSupportingText + + fun checkManagementCode() = viewModelScope.launch { + checkManagementCodeUseCase(_managementCode.value) + .onSuccess { + when (it) { + CodeValidation.VALID -> { + _managementCodeUiState.value = ManagementCodeUiState.Success + } + + CodeValidation.INVALID -> { + _isError.value = true + _errorSupportingText.value = R.string.management_incorrect_code + } + } + } + .onFailure { throwable -> + _managementCodeUiState.value = ManagementCodeUiState.Failure(throwable) + } + } + + fun setManagementCode(code: String) { _managementCode.value = code } + + sealed class ManagementCodeUiState { + data object Init : ManagementCodeUiState() + data object Success : ManagementCodeUiState() + data class Failure(val throwable: Throwable) : ManagementCodeUiState() + } +} diff --git a/feature/management/src/main/res/values/strings.xml b/feature/management/src/main/res/values/strings.xml new file mode 100644 index 000000000..8233ac569 --- /dev/null +++ b/feature/management/src/main/res/values/strings.xml @@ -0,0 +1,55 @@ + + ๊ด€๋ฆฌ + ์ผ์ • ๋ฐ ์„ค๋ฌธ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•ด์š”! + ์šด์˜์ง„ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ์šด์˜์ง„๋งŒ ํ•ด๋‹น ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š” + code + Hint : WAPP + ์ž˜๋ชป๋œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. + ์„ค๋ฌธ ์ถ”๊ฐ€ + ์ž์„ธํžˆ๋ณด๊ธฐ ๋ฒ„ํŠผ + ์„ค๋ฌธ + ์ผ์ • + ์–ธ์ œ ์ง„ํ–‰๋˜๋Š” ์ผ์ •์ธ๊ฐ€์š”? + ๋ช‡์‹œ๋ถ€ํ„ฐ ๋ช‡์‹œ๊นŒ์ง€ ์ง„ํ–‰๋˜๋Š” ์ผ์ •์ธ๊ฐ€์š”? + ์ˆ˜์ • ์™„๋ฃŒ + ์„ค๋ฌธ ๋“ฑ๋ก + / 4 + ๋ฌธํ•ญ ์ถ”๊ฐ€ + ์„ค๋ฌธ ๋“ฑ๋กํ•˜๊ธฐ + ์ผ์ • ์ถ”๊ฐ€ + ์„ค๋ฌธ ๊ธฐ๊ฐ„ ์„ค์ • + ์„ค๋ฌธ์˜ ๋งˆ๊ฐ ๊ธฐํ•œ์„ ์„ค์ •ํ•˜์„ธ์š”. + ์„ค๋ฌธ ์ œ๋ชฉ ์ž…๋ ฅ + ์„ค๋ฌธ์„ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ์ œ๋ชฉ๊ณผ ์†Œ๊ฐœ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์„ค๋ฌธ ์ œ๋ชฉ + ์„ค๋ฌธ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š” + ์„ค๋ฌธ ์†Œ๊ฐœ + ์„ค๋ฌธ ์†Œ๊ฐœ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ์„ค๋ฌธ ๋ฌธํ•ญ ์ž‘์„ฑ + ๋ถ€์›๋“ค์ด ์•Œ์•„๋ณด๊ธฐ ์‰ฝ๋„๋ก ์ž์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. + ๋ฌธํ•ญ ์งˆ๋ฌธ + ์„ค๋ฌธ ๋ฌธํ•ญ์˜ ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”. + ๋ฌธํ•ญ ์œ ํ˜• + ์ฃผ๊ด€์‹ + ๊ฐ๊ด€์‹ + ์ฃผ๊ด€์‹ ๋‹ต๋ณ€์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. + ์ข‹์Œ + ๋ณดํ†ต + ๋‚˜์จ + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์™„๋ฒฝํ•˜๊ณ , \nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋งŽ์•˜์Œ. + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์›ํ™œํ•˜๊ณ ,\nํ–‰์‚ฌ ๋‚ด์šฉ์ด ์ ๋‹นํ•จ. + ํ–‰์‚ฌ ์ง„ํ–‰์ด ์•„์‰ฌ์› ๊ณ ,\nํ–‰์‚ฌ ๋‚ด์šฉ์ด ๋ถ€์กฑํ•จ. + ํ–‰์‚ฌ ์„ ํƒ + ์„ค๋ฌธ์„ ๋“ฑ๋กํ•  ํ–‰์‚ฌ๋ฅผ ์„ ํƒํ•˜์„ธ์š”. + ์„ค๋ฌธ ํ™•์ธ + ์ด๋ฆ„ + ๋‹ค์Œ + "์ด์ „ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + "๋‹ค์Œ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ™”์‚ดํ‘œ ์ž…๋‹ˆ๋‹ค." + %s ๋งˆ๊ฐ + %s ์‹œ์ž‘ + ๋กœ๊ทธ์ธ ํ•˜๋Ÿฌ๊ฐ€๊ธฐ + ์•— ํšŒ์›์ด ์•„๋‹ˆ์‹œ๋„ค์š” ! + ์šด์˜์ง„๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์š”. + diff --git a/feature/management/src/test/java/com/wap/wapp/feature/management/ExampleUnitTest.kt b/feature/management/src/test/java/com/wap/wapp/feature/management/ExampleUnitTest.kt new file mode 100644 index 000000000..f17ac8371 --- /dev/null +++ b/feature/management/src/test/java/com/wap/wapp/feature/management/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.management + +import org.junit.Assert.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/feature/notice/.gitignore b/feature/notice/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/notice/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/notice/build.gradle.kts b/feature/notice/build.gradle.kts new file mode 100644 index 000000000..38ca1a748 --- /dev/null +++ b/feature/notice/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.wap.wapp.feature") +} + +android { + namespace = "com.wap.wapp.feature.notice" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/notice/consumer-rules.pro b/feature/notice/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/notice/proguard-rules.pro b/feature/notice/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/notice/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/notice/src/androidTest/java/com/wap/wapp/feature/notice/ExampleInstrumentedTest.kt b/feature/notice/src/androidTest/java/com/wap/wapp/feature/notice/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..1f0b6b1ab --- /dev/null +++ b/feature/notice/src/androidTest/java/com/wap/wapp/feature/notice/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.notice + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.feature.notice.test", appContext.packageName) + } +} diff --git a/feature/notice/src/main/AndroidManifest.xml b/feature/notice/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/notice/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/notice/src/main/java/com/wap/wapp/feature/notice/BottomSheetContent.kt b/feature/notice/src/main/java/com/wap/wapp/feature/notice/BottomSheetContent.kt new file mode 100644 index 000000000..b2080d045 --- /dev/null +++ b/feature/notice/src/main/java/com/wap/wapp/feature/notice/BottomSheetContent.kt @@ -0,0 +1,212 @@ +package com.wap.wapp.feature.notice + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +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.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.unit.Dp +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.wapp.core.commmon.util.DateUtil.HHmmFormatter +import com.wap.wapp.core.commmon.util.DateUtil.MONTH_DATE_START_INDEX +import com.wap.wapp.core.commmon.util.DateUtil.yyyyMMddFormatter +import com.wap.wapp.core.model.event.Event +import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun BottomSheetContent( + expandableHeight: Dp, + bottomSheetState: SheetState, + events: NoticeViewModel.EventsState, + selectedDate: LocalDate, +) { + val date = yyyyMMddFormatter.format(selectedDate).substring(MONTH_DATE_START_INDEX) + val dayOfWeek = selectedDate.dayOfWeek.getDisplayName( + TextStyle.FULL, + Locale.KOREAN, + ) + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.height(expandableHeight), + ) { + Text( + text = "$date $dayOfWeek", + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.padding(start = 15.dp, bottom = 5.dp), + ) + + when (events) { + is NoticeViewModel.EventsState.Loading -> + CircleLoader(modifier = Modifier.fillMaxWidth()) + + is NoticeViewModel.EventsState.Success -> EventsList( + bottomSheetState = bottomSheetState, + events = events.events, + ) + + is NoticeViewModel.EventsState.Failure -> Unit + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EventsList( + bottomSheetState: SheetState, + events: List, +) { + if (events.isNotEmpty()) { + LazyColumn( + contentPadding = PaddingValues(horizontal = 15.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + ) { + items( + items = events, + key = { event -> event.eventId }, + ) { event -> + EventItem( + bottomSheetState = bottomSheetState, + event = event, + ) + } + } + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_cat), + contentDescription = stringResource(id = R.string.cat_content_description), + ) + + Text( + text = stringResource(id = R.string.bottomSheet_no_event), + style = WappTheme.typography.titleMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 30.dp), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EventItem( + bottomSheetState: SheetState, + event: Event, +) { + var eventItemToggle by remember { mutableStateOf(false) } + + LaunchedEffect(bottomSheetState.currentValue) { + eventItemToggle = bottomSheetState.currentValue == SheetValue.Expanded + } + + Column( + modifier = Modifier + .animateContentSize() + .clickable { eventItemToggle = !eventItemToggle }, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .animateContentSize() + .padding(vertical = 10.dp) + .height(IntrinsicSize.Min) + .fillMaxWidth(), + ) { + Text( + text = event.startDateTime.toLocalTime().format(HHmmFormatter), + style = WappTheme.typography.contentBold, + color = WappTheme.colors.white, + ) + + Spacer( + modifier = Modifier + .width(4.dp) + .animateContentSize() + .fillMaxHeight() + .clip(RoundedCornerShape(10.dp)) + .background(WappTheme.colors.yellow34), + ) + + Column(horizontalAlignment = Alignment.Start) { + Text( + text = event.title, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + ) + + AnimatedVisibility(eventItemToggle) { + Column(horizontalAlignment = Alignment.Start) { + Text( + text = event.location, + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.yellow34, + ) + + Text( + text = event.content, + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.grayBD, + ) + } + } + + Text( + text = event.startDateTime.format(HHmmFormatter) + " ~ " + + event.endDateTime.format(HHmmFormatter), + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.yellow34, + ) + } + } + + Divider(color = WappTheme.colors.gray82) + } +} diff --git a/feature/notice/src/main/java/com/wap/wapp/feature/notice/Calendar.kt b/feature/notice/src/main/java/com/wap/wapp/feature/notice/Calendar.kt new file mode 100644 index 000000000..abf870e94 --- /dev/null +++ b/feature/notice/src/main/java/com/wap/wapp/feature/notice/Calendar.kt @@ -0,0 +1,376 @@ +package com.wap.wapp.feature.notice + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.wapp.core.commmon.util.DateUtil.DAYS_IN_WEEK +import com.wap.wapp.core.commmon.util.DateUtil.DaysOfWeek +import com.wap.wapp.core.commmon.util.DateUtil.YEAR_MONTH_END_INDEX +import com.wap.wapp.core.commmon.util.DateUtil.YEAR_MONTH_START_INDEX +import com.wap.wapp.core.commmon.util.DateUtil.generateNowDate +import com.wap.wapp.core.commmon.util.DateUtil.yyyyMMddFormatter +import com.wap.wapp.core.designresource.R.drawable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun Calendar( + coroutineScope: CoroutineScope, + bottomSheetState: SheetState, + selectedDate: LocalDate, + monthEventsState: NoticeViewModel.EventsState, + measureDefaultModifier: Modifier, + measureExpandableModifier: Modifier, + onDateSelected: (LocalDate) -> Unit, + onCalendarMonthChanged: () -> Unit, +) { + Column( + modifier = measureDefaultModifier, + ) { + CalendarHeader( + coroutineScope = coroutineScope, + bottomSheetState = bottomSheetState, + selectedDate = selectedDate, + onDateSelected = onDateSelected, + onCalendarMonthChanged = onCalendarMonthChanged, + modifier = measureExpandableModifier, + ) + + when (monthEventsState) { + is NoticeViewModel.EventsState.Loading -> + CircleLoader(modifier = Modifier.fillMaxSize()) + + is NoticeViewModel.EventsState.Success -> { + val eventDates = monthEventsState.events.map { + it.startDateTime.toLocalDate() + } + CalendarBody( + selectedDate = selectedDate, + eventsDate = eventDates, + onDateSelected = onDateSelected, + ) + } + + is NoticeViewModel.EventsState.Failure -> {} + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CalendarHeader( + coroutineScope: CoroutineScope, + bottomSheetState: SheetState, + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, + onCalendarMonthChanged: () -> Unit, + modifier: Modifier, +) = Box( + modifier = modifier, +) { + val date = selectedDate.format(yyyyMMddFormatter) + + Image( + painter = painterResource(id = R.drawable.ic_threelines), + contentDescription = + stringResource(R.string.calendar_content_description), + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { + toggleBottomSheetState( + coroutineScope, + bottomSheetState, + ) + } + .padding(start = 16.dp), + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.Center), + ) { + Image( + painter = painterResource(id = drawable.ic_back), + contentDescription = stringResource(id = R.string.back_month_arrow_content_description), + modifier = Modifier + .padding(end = 20.dp) + .clickable { + onDateSelected(selectedDate.minusMonths(1)) + onCalendarMonthChanged() + }, + ) + + Text( + text = date.substring( + YEAR_MONTH_START_INDEX, + YEAR_MONTH_END_INDEX, + ), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + + Image( + painter = painterResource(id = drawable.ic_forward), + contentDescription = + stringResource(id = R.string.forward_month_arrow_content_description), + modifier = Modifier + .padding(start = 20.dp) + .clickable { + onDateSelected(selectedDate.plusMonths(1)) + onCalendarMonthChanged() + }, + ) + } + + AnimatedVisibility( + visible = selectedDate != generateNowDate(), + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 16.dp), + ) { + Image( + painter = painterResource(id = drawable.ic_return), + contentDescription = + stringResource(R.string.return_today_content_description), + modifier = Modifier.clickable { + onDateSelected(generateNowDate()) + onCalendarMonthChanged() + }, + ) + } +} + +@Composable +private fun CalendarBody( + selectedDate: LocalDate, + eventsDate: List, + onDateSelected: (LocalDate) -> Unit, +) { + CalendarWeekDays() + CalendarMonthItem( + eventDates = eventsDate, + selectedDate = selectedDate, + onDateSelected = onDateSelected, + ) +} + +@Composable +private fun CalendarWeekDays(modifier: Modifier = Modifier) { + Row(modifier = modifier) { + DaysOfWeek.values().forEach { dayOfWeek -> + val textColor = when (dayOfWeek) { + DaysOfWeek.SATURDAY -> WappTheme.colors.blueA3 + DaysOfWeek.SUNDAY -> WappTheme.colors.red + else -> WappTheme.colors.white + } + + Text( + text = dayOfWeek.displayName, + textAlign = TextAlign.Center, + color = textColor, + modifier = Modifier + .weight(1f) + .padding(vertical = 10.dp), + ) + } + } +} + +@Composable +private fun CalendarMonthItem( + selectedDate: LocalDate, + eventDates: List, + onDateSelected: (LocalDate) -> Unit, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(DAYS_IN_WEEK), + modifier = Modifier.fillMaxWidth(), + ) { + val visibleDaysFromLastMonth = calculateVisibleDaysFromLastMonth(selectedDate) + val beforeMonthDaysToShow = generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth, + selectedDate, + ) + itemsIndexed(beforeMonthDaysToShow) { index, day -> + CalendarDayText( + text = day.toString(), + color = getDayColor(index + 1).copy(alpha = ALPHA_DIM), + ) + } + + val thisMonthLastDate = selectedDate.lengthOfMonth() + val thisMonthFirstDayOfWeek = selectedDate.withDayOfMonth(1).dayOfWeek + val thisMonthDaysToShow: List = (1..thisMonthLastDate).toList() + items(thisMonthDaysToShow) { day -> + val currentLocalDate = LocalDate.of( + selectedDate.year, + selectedDate.month, + day, + ) + + val isEvent = (currentLocalDate in eventDates) + val isSelected = (day == selectedDate.dayOfMonth) + CalendarDayText( + text = day.toString(), + color = getDayColor(day + thisMonthFirstDayOfWeek.value), + isEvent = isEvent, + isSelected = isSelected, + modifier = Modifier.clickable { onDateSelected(currentLocalDate) }, + ) + } + + val remainingDays = + DAYS_IN_WEEK - (visibleDaysFromLastMonth + thisMonthDaysToShow.size) % DAYS_IN_WEEK + val nextMonthDaysToShow = IntRange(1, remainingDays).toList() + items(nextMonthDaysToShow) { day -> + CalendarDayText( + text = day.toString(), + color = + getDayColor(visibleDaysFromLastMonth + thisMonthDaysToShow.size + day) + .copy(alpha = ALPHA_DIM), + ) + } + } +} + +@Composable +private fun CalendarDayText( + text: String, + color: Color, + isSelected: Boolean = false, + isEvent: Boolean = false, + modifier: Modifier = Modifier, +) { + var columnModifier = modifier + .padding( + horizontal = 10.dp, + vertical = 5.dp, + ) + + if (isSelected) { + columnModifier = columnModifier.background( + color = WappTheme.colors.gray82.copy(alpha = 0.4F), + shape = RoundedCornerShape(5.dp), + ) + } + + columnModifier = columnModifier.padding(vertical = 5.dp) + + Column( + modifier = columnModifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + textAlign = TextAlign.Center, + color = color, + ) + } + EventDot(isEvent) + } +} + +@Composable +private fun EventDot(isEvent: Boolean) { + var boxModifier = Modifier + .padding(top = 5.dp) + .size(5.dp) + + if (isEvent) { + boxModifier = boxModifier + .aspectRatio(1f) + .background( + color = WappTheme.colors.yellow34, + shape = CircleShape, + ) + } + + Box(modifier = boxModifier) +} + +@Composable +private fun getDayColor(day: Int): Color = when (day % DAYS_IN_WEEK) { + SUNDAY -> WappTheme.colors.red + SATURDAY -> WappTheme.colors.blueA3 + else -> WappTheme.colors.white +} + +private fun generateBeforeMonthDaysToShow( + visibleDaysFromLastMonth: Int, + currentDate: LocalDate, +): List { + val beforeMonth = currentDate.minusMonths(1) + val beforeMonthLastDay = beforeMonth.lengthOfMonth() + return IntRange(beforeMonthLastDay - visibleDaysFromLastMonth + 1, beforeMonthLastDay).toList() +} + +@OptIn(ExperimentalMaterial3Api::class) +private fun toggleBottomSheetState( + coroutineScope: CoroutineScope, + sheetState: SheetState, +) = coroutineScope.launch { + when (sheetState.currentValue) { + SheetValue.Expanded -> sheetState.partialExpand() + SheetValue.PartiallyExpanded -> sheetState.expand() + SheetValue.Hidden -> sheetState.expand() + } +} + +private fun calculateVisibleDaysFromLastMonth(currentDate: LocalDate): Int { + val firstDayOfWeek: DayOfWeek = currentDate.withDayOfMonth(1).dayOfWeek + + var count = 0 + for (day in DaysOfWeek.values()) { + if (day.name == firstDayOfWeek.getDisplayName(TextStyle.FULL, Locale.US).uppercase()) { + break + } + count += 1 + } + + return count +} + +private const val ALPHA_DIM = 0.3F +private const val SUNDAY = 1 +private const val SATURDAY = 0 diff --git a/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeScreen.kt b/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeScreen.kt new file mode 100644 index 000000000..9441abeb6 --- /dev/null +++ b/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeScreen.kt @@ -0,0 +1,119 @@ +package com.wap.wapp.feature.notice + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.notice.NoticeViewModel.EventsState +import java.time.LocalDate + +@Composable +internal fun NoticeRoute( + viewModel: NoticeViewModel = hiltViewModel(), +) { + val MonthEvents by viewModel.monthEvents.collectAsStateWithLifecycle() + val selectedDateEvents by viewModel.selectedDateEvents.collectAsStateWithLifecycle() + val selectedDate by viewModel.selectedDate.collectAsStateWithLifecycle() + val onDateSelected = viewModel::updateSelectedDate + val onCalendarMonthChanged = viewModel::getMonthEvents + + NoticeScreen( + monthEvents = MonthEvents, + selectedDateEvents = selectedDateEvents, + selectedDate = selectedDate, + onDateSelected = onDateSelected, + onCalendarMonthChanged = onCalendarMonthChanged, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NoticeScreen( + monthEvents: EventsState, + selectedDateEvents: EventsState, + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, + onCalendarMonthChanged: () -> Unit, +) { + var defaultHeight: Dp by remember { mutableStateOf(0.dp) } + var expandableHeight: Dp by remember { mutableStateOf(0.dp) } + val coroutineScope = rememberCoroutineScope() + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = SheetState( + skipPartiallyExpanded = false, + initialValue = SheetValue.PartiallyExpanded, + skipHiddenState = true, + ), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(WappTheme.colors.black20), + ) { + BottomSheetScaffold( + scaffoldState = scaffoldState, + sheetContainerColor = WappTheme.colors.black25, + sheetPeekHeight = defaultHeight, + sheetShadowElevation = 20.dp, + sheetContent = { + BottomSheetContent( + expandableHeight = expandableHeight, + bottomSheetState = scaffoldState.bottomSheetState, + events = selectedDateEvents, + selectedDate = selectedDate, + ) + }, + ) { + Calendar( + coroutineScope = coroutineScope, + bottomSheetState = scaffoldState.bottomSheetState, + selectedDate = selectedDate, + monthEventsState = monthEvents, + onDateSelected = onDateSelected, + onCalendarMonthChanged = onCalendarMonthChanged, + measureDefaultModifier = Modifier + .fillMaxWidth() + .background(WappTheme.colors.black20) + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + + defaultHeight = (constraints.maxHeight - placeable.height).toDp() + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + }, + measureExpandableModifier = Modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + expandableHeight = + constraints.maxHeight.toDp() - (placeable.height.toDp() * 2) + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } + .padding(vertical = 10.dp) + .fillMaxWidth(), + ) + } + } +} diff --git a/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeViewModel.kt b/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeViewModel.kt new file mode 100644 index 000000000..01b8a1321 --- /dev/null +++ b/feature/notice/src/main/java/com/wap/wapp/feature/notice/NoticeViewModel.kt @@ -0,0 +1,64 @@ +package com.wap.wapp.feature.notice + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.event.GetDateEventListUseCase +import com.wap.wapp.core.domain.usecase.event.GetMonthEventListUseCase +import com.wap.wapp.core.model.event.Event +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class NoticeViewModel @Inject constructor( + private val getMonthEventListUseCase: GetMonthEventListUseCase, + private val getDateEventListUseCase: GetDateEventListUseCase, +) : ViewModel() { + private val _monthEvents = MutableStateFlow(EventsState.Loading) + val monthEvents: StateFlow = _monthEvents.asStateFlow() + + private val _selectedDateEvents = MutableStateFlow(EventsState.Loading) + val selectedDateEvents: StateFlow = _selectedDateEvents.asStateFlow() + + private val _selectedDate = MutableStateFlow(DateUtil.generateNowDate()) + val selectedDate: StateFlow = _selectedDate.asStateFlow() + + init { + getMonthEvents() + getSelectedDateEvents() + } + + fun getMonthEvents() { + _monthEvents.value = EventsState.Loading + viewModelScope.launch { + getMonthEventListUseCase(_selectedDate.value) + .onSuccess { _monthEvents.value = EventsState.Success(it) } + .onFailure { _monthEvents.value = EventsState.Failure(it) } + } + } + + private fun getSelectedDateEvents() { + _selectedDateEvents.value = EventsState.Loading + viewModelScope.launch { + getDateEventListUseCase(_selectedDate.value).onSuccess { eventList -> + _selectedDateEvents.value = EventsState.Success(eventList) + }.onFailure { _selectedDateEvents.value = EventsState.Failure(it) } + } + } + + fun updateSelectedDate(newSelectedDate: LocalDate) { + _selectedDate.value = newSelectedDate + getSelectedDateEvents() + } + + sealed class EventsState { + data object Loading : EventsState() + data class Success(val events: List) : EventsState() + data class Failure(val throwable: Throwable) : EventsState() + } +} diff --git a/feature/notice/src/main/java/com/wap/wapp/feature/notice/navigation/NoticeNavigation.kt b/feature/notice/src/main/java/com/wap/wapp/feature/notice/navigation/NoticeNavigation.kt new file mode 100644 index 000000000..5e0c104da --- /dev/null +++ b/feature/notice/src/main/java/com/wap/wapp/feature/notice/navigation/NoticeNavigation.kt @@ -0,0 +1,20 @@ +package com.wap.wapp.feature.notice.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.notice.NoticeRoute + +const val noticeNavigationRoute = "notice_route" + +fun NavController.navigateToNotice(navOptions: NavOptions? = navOptions {}) { + this.navigate(noticeNavigationRoute, navOptions) +} + +fun NavGraphBuilder.noticeScreen() { + composable(route = noticeNavigationRoute) { + NoticeRoute() + } +} diff --git a/feature/notice/src/main/res/drawable/ic_cat.xml b/feature/notice/src/main/res/drawable/ic_cat.xml new file mode 100644 index 000000000..1bcf5a587 --- /dev/null +++ b/feature/notice/src/main/res/drawable/ic_cat.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/notice/src/main/res/drawable/ic_threelines.xml b/feature/notice/src/main/res/drawable/ic_threelines.xml new file mode 100644 index 000000000..c8bc40a68 --- /dev/null +++ b/feature/notice/src/main/res/drawable/ic_threelines.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/notice/src/main/res/values/strings.xml b/feature/notice/src/main/res/values/strings.xml new file mode 100644 index 000000000..9240c522d --- /dev/null +++ b/feature/notice/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + ๊ณต์ง€์‚ฌํ•ญ ๋ชฉ๋ก๋งŒ์„ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜, ์บ˜๋ฆฐ๋”๋ฅผ ๋‹ค์‹œ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + ๊ณต์ง€์‚ฌํ•ญ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋‹ฌ๋ ฅ์ž…๋‹ˆ๋‹ค. + ์ผ์ •์ด ์—†์„ ๋•Œ ๋ณด์—ฌ์ฃผ๋Š” ์ž‘๊ณ  ๊ท€์—ฌ์šด ๊ณ ์–‘์ด ์ž…๋‹ˆ๋‹ค. + ์ผ์ •์ด ์—†์Šต๋‹ˆ๋‹ค. + "์ด์ „ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ฒ„ํŠผ ์ž…๋‹ˆ๋‹ค." + "๋‹ค์Œ ๋‹ฌ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ฒ„ํŠผ ์ž…๋‹ˆ๋‹ค." + ์˜ค๋Š˜ ๋‚ ์งœ๋กœ ์ด๋™ํ•˜๋Š” ๋ฒ„ํŠผ ์ž…๋‹ˆ๋‹ค. + diff --git a/feature/notice/src/test/java/com/wap/wapp/feature/notice/ExampleUnitTest.kt b/feature/notice/src/test/java/com/wap/wapp/feature/notice/ExampleUnitTest.kt new file mode 100644 index 000000000..53c8a38ff --- /dev/null +++ b/feature/notice/src/test/java/com/wap/wapp/feature/notice/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.notice + +import org.junit.Assert.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/feature/profile/.gitignore b/feature/profile/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/profile/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts new file mode 100644 index 000000000..5a67a0ba6 --- /dev/null +++ b/feature/profile/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.wap.wapp.feature") +} + +android { + namespace = "com.wap.wapp.feature.profile" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/profile/consumer-rules.pro b/feature/profile/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/profile/proguard-rules.pro b/feature/profile/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/profile/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/profile/src/androidTest/java/com/wap/wapp/feature/profile/ExampleInstrumentedTest.kt b/feature/profile/src/androidTest/java/com/wap/wapp/feature/profile/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..bed016f25 --- /dev/null +++ b/feature/profile/src/androidTest/java/com/wap/wapp/feature/profile/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.profile + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.feature.profile.test", appContext.packageName) + } +} diff --git a/feature/profile/src/main/AndroidManifest.xml b/feature/profile/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/profile/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileScreen.kt new file mode 100644 index 000000000..79e135e6f --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileScreen.kt @@ -0,0 +1,194 @@ +package com.wap.wapp.feature.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappLeftMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.core.designresource.R.string +import com.wap.wapp.core.model.user.UserProfile +import com.wap.wapp.core.model.user.UserRole +import com.wap.wapp.feature.profile.ProfileViewModel.UserRoleState +import com.wap.wapp.feature.profile.component.WappProfileCard +import com.wap.wapp.feature.profile.profilesetting.component.GuestProfile +import com.wap.wapp.feature.profile.profilesetting.component.UserProfile +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun ProfileRoute( + viewModel: ProfileViewModel = hiltViewModel(), + navigateToProfileSetting: (String) -> Unit, + navigateToAttendance: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSurveyDetail: (String) -> Unit, +) { + val todayEventsState by viewModel.todayEvents.collectAsStateWithLifecycle() + val recentEventsState by viewModel.recentEvents.collectAsStateWithLifecycle() + val userRespondedSurveysState by viewModel.userRespondedSurveys.collectAsStateWithLifecycle() + val userRoleState by viewModel.userRole.collectAsStateWithLifecycle() + val userProfile by viewModel.userProfile.collectAsStateWithLifecycle() + val snackBarHostState = remember { SnackbarHostState() } + + LaunchedEffect(true) { + viewModel.errorFlow.collectLatest { throwable -> + snackBarHostState.showSnackbar( + message = throwable.toSupportingText(), + ) + } + } + + ProfileScreen( + todayEventsState = todayEventsState, + recentEventsState = recentEventsState, + userRoleState = userRoleState, + userProfile = userProfile, + userRespondedSurveysState = userRespondedSurveysState, + snackBarHostState = snackBarHostState, + navigateToProfileSetting = navigateToProfileSetting, + navigateToAttendance = navigateToAttendance, + navigateToSignIn = navigateToSignIn, + navigateToSurveyDetail = navigateToSurveyDetail, + ) +} + +@Composable +internal fun ProfileScreen( + userRoleState: UserRoleState, + userProfile: UserProfile, + todayEventsState: ProfileViewModel.EventsState, + recentEventsState: ProfileViewModel.EventAttendanceStatusState, + userRespondedSurveysState: ProfileViewModel.SurveysState, + snackBarHostState: SnackbarHostState, + navigateToProfileSetting: (String) -> Unit, + navigateToAttendance: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSurveyDetail: (String) -> Unit, +) { + val scrollState = rememberScrollState() + + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + snackbarHost = { SnackbarHost(snackBarHostState) }, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .verticalScroll(scrollState) + .background(WappTheme.colors.backgroundBlack), + ) { + when (userRoleState) { + is UserRoleState.Loading -> { + Spacer(modifier = Modifier.weight(1f)) + CircleLoader( + modifier = Modifier + .fillMaxSize() + .weight(1f), + ) + Spacer(modifier = Modifier.weight(1f)) + } + + is UserRoleState.Success -> { + WappLeftMainTopBar( + titleRes = string.profile, + contentRes = R.string.profile_content, + settingButtonDescriptionRes = R.string.profile_setting_description, + showSettingButton = userRoleState.userRole != UserRole.GUEST, + onClickSettingButton = { navigateToProfileSetting(userProfile.userId) }, + ) + + when (userRoleState.userRole) { + UserRole.MANAGER -> { + WappProfileCard( + position = stringResource(R.string.manager), + githubImage = drawable.ic_manager_github, + catImage = drawable.ic_manager_cat, + brush = Brush.horizontalGradient( + listOf( + WappTheme.colors.blue2FF, + WappTheme.colors.blue4FF, + WappTheme.colors.blue1FF, + ), + ), + userName = "${userProfile.userName} ๋‹˜", + ) + + UserProfile( + todayEventsState = todayEventsState, + recentEventsState = recentEventsState, + userRespondedSurveysState = userRespondedSurveysState, + attendanceCardBoardColor = WappTheme.colors.blue4FF, + navigateToAttendance = navigateToAttendance, + navigateToSurveyDetail = navigateToSurveyDetail, + ) + } + + UserRole.MEMBER -> { + WappProfileCard( + position = stringResource(R.string.normal), + githubImage = drawable.ic_normal_github, + catImage = drawable.ic_normal_cat, + brush = Brush.horizontalGradient( + listOf( + WappTheme.colors.yellow3C, + WappTheme.colors.yellow34, + WappTheme.colors.yellowA4, + ), + ), + userName = "${userProfile.userName} ๋‹˜", + ) + + UserProfile( + todayEventsState = todayEventsState, + recentEventsState = recentEventsState, + userRespondedSurveysState = userRespondedSurveysState, + attendanceCardBoardColor = WappTheme.colors.yellow34, + navigateToAttendance = navigateToAttendance, + navigateToSurveyDetail = navigateToSurveyDetail, + ) + } + + UserRole.GUEST -> { + WappProfileCard( + position = stringResource(R.string.guest), + githubImage = drawable.ic_guest_github, + catImage = drawable.ic_guest_cat, + brush = Brush.horizontalGradient( + listOf( + WappTheme.colors.grayA2, + WappTheme.colors.gray7C, + WappTheme.colors.gray4A, + ), + ), + userName = stringResource(id = R.string.non_user), + ) + + GuestProfile(navigateToSignIn = navigateToSignIn) + } + } + } + } + } + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileViewModel.kt new file mode 100644 index 000000000..4bf54a4c7 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/ProfileViewModel.kt @@ -0,0 +1,171 @@ +package com.wap.wapp.feature.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.domain.usecase.attendancestatus.GetEventListAttendanceStatusUseCase +import com.wap.wapp.core.domain.usecase.event.GetDateEventListUseCase +import com.wap.wapp.core.domain.usecase.event.GetRecentEventListUseCase +import com.wap.wapp.core.domain.usecase.survey.GetUserRespondedSurveyListUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserProfileUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserRoleUseCase +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.core.model.survey.Survey +import com.wap.wapp.core.model.user.UserProfile +import com.wap.wapp.core.model.user.UserRole +import com.wap.wapp.feature.profile.model.EventAttendanceStatus +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +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 +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val getUserRoleUseCase: GetUserRoleUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, + private val getRecentEventListUseCase: GetRecentEventListUseCase, + private val getDateEventListUseCase: GetDateEventListUseCase, + private val getEventListAttendanceStatusUseCase: GetEventListAttendanceStatusUseCase, + private val getUserRespondedSurveyListUseCase: GetUserRespondedSurveyListUseCase, +) : ViewModel() { + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow: SharedFlow = _errorFlow.asSharedFlow() + + private val _todayEvents = MutableStateFlow(EventsState.Loading) + val todayEvents: StateFlow = _todayEvents.asStateFlow() + + private val _recentEvents = + MutableStateFlow(EventAttendanceStatusState.Loading) + val recentEvents: StateFlow = _recentEvents.asStateFlow() + + private val _userRespondedSurveys = MutableStateFlow(SurveysState.Loading) + val userRespondedSurveys: StateFlow = _userRespondedSurveys.asStateFlow() + + private val _userRole = MutableStateFlow(UserRoleState.Loading) + val userRole: StateFlow = _userRole.asStateFlow() + + private val _userProfile = MutableStateFlow(DEFAULT_USER_PROFILE) + val userProfile: StateFlow = _userProfile.asStateFlow() + + init { + checkUserInformationAndGetEvents() + } + + private fun checkUserInformationAndGetEvents() = viewModelScope.launch { + getUserRoleUseCase() + .onFailure { exception -> _errorFlow.emit(exception) } + .onSuccess { userRole -> + when (userRole) { + // ๋น„ํšŒ์› ์ผ ๊ฒฝ์šฐ, ๋ฐ”๋กœ UserCard ๊ฐฑ์‹  + UserRole.GUEST -> _userRole.value = UserRoleState.Success(userRole) + + // ์ผ๋ฐ˜ ํšŒ์› ํ˜น์€ ์šด์˜์ง„ ์ผ ๊ฒฝ์šฐ, + // ์˜ค๋Š˜ ์ผ์ • ์ •๋ณด, UserProfile ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ ๋’ค ํ•œ๊บผ๋ฒˆ์— ๊ฐฑ์‹  + UserRole.MEMBER, UserRole.MANAGER -> { + getTodayDateEvents() + val userProfile = async { getUserProfileUseCase() } + + userProfile.await().onSuccess { + _userRole.value = UserRoleState.Success(userRole) + _userProfile.value = it + launch { getRecentEventsForAttendanceCheck() } + getUserRespondedSurveys() + }.onFailure { exception -> _errorFlow.emit(exception) } + } + } + } + } + + private fun getTodayDateEvents() { + _todayEvents.value = EventsState.Loading + viewModelScope.launch { + getDateEventListUseCase(DateUtil.generateNowDate()).onSuccess { eventList -> + _todayEvents.value = EventsState.Success(eventList) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + } + + private suspend fun getUserRespondedSurveys() { + getUserRespondedSurveyListUseCase(_userProfile.value.userId).onSuccess { surveyList -> + _userRespondedSurveys.value = SurveysState.Success(surveyList) + }.onFailure { exception -> _errorFlow.emit(exception) } + } + + private suspend fun getRecentEventsForAttendanceCheck() { + val registeredAt = _userProfile.value.registeredAt + val (registeredYear, registeredSemester) = registeredAt.split(" ") + val registrationDate = + createRegistrationDate(registeredYear.toInt(), registeredSemester) + + getRecentEventListUseCase(registrationDate) + .onSuccess { eventList -> + getEventListAttendanceStatus( + eventList = eventList, + userId = _userProfile.value.userId, + ) + }.onFailure { _errorFlow.emit(it) } + } + + // ์ตœ๊ทผ ์ผ์ •๋“ค ์ค‘, ์œ ์ €๊ฐ€ ์ถœ์„์„ ํ–ˆ๋Š” ์ง€ ์•ˆํ–ˆ๋Š” ์ง€๋ฅผ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + private suspend fun getEventListAttendanceStatus( + eventList: List, + userId: String, + ) = getEventListAttendanceStatusUseCase( + eventIdList = eventList.map { it.eventId }, + userId = userId, + ).onSuccess { attendanceStatusList -> + val eventAttendanceStatusList = eventList.zip(attendanceStatusList) + .map { (event, attendanceStatus) -> + EventAttendanceStatus( + eventId = event.eventId, + title = event.title, + content = event.content, + startDateTime = event.startDateTime, + isAttendance = attendanceStatus.isAttendance(), + ) + } + _recentEvents.value = + EventAttendanceStatusState.Success(eventAttendanceStatusList) + }.onFailure { _errorFlow.emit(it) } + + private fun createRegistrationDate(year: Int, semester: String): LocalDate { + // ํ•™๊ธฐ์— ๋”ฐ๋ฅธ ๊ธฐ์ค€ ๋‚ ์งœ ์„ค์ • (์˜ˆ: 1ํ•™๊ธฐ๋Š” 3์›” 1์ผ, 2ํ•™๊ธฐ๋Š” 9์›” 1์ผ) + val semesterNumber = semester.removeSuffix("ํ•™๊ธฐ").toInt() + + if (semesterNumber == 1) { + return LocalDate.of(year, 3, 1) + } + return LocalDate.of(year, 9, 1) + } + + sealed class EventsState { + data object Loading : EventsState() + data class Success(val events: List) : EventsState() + } + + sealed class EventAttendanceStatusState { + data object Loading : EventAttendanceStatusState() + data class Success(val events: List) : EventAttendanceStatusState() + } + + sealed class SurveysState { + data object Loading : SurveysState() + data class Success(val surveys: List) : SurveysState() + } + + sealed class UserRoleState { + data object Loading : UserRoleState() + data class Success(val userRole: UserRole) : UserRoleState() + } + + companion object { + val DEFAULT_USER_PROFILE = UserProfile("", "", "", "") + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappAttendanceRow.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappAttendanceRow.kt new file mode 100644 index 000000000..130cb0060 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappAttendanceRow.kt @@ -0,0 +1,56 @@ +package com.wap.wapp.feature.profile.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.designresource.R +import com.wap.wapp.feature.profile.model.EventAttendanceStatus + +@Composable +internal fun WappAttendacneRow( + eventAttendanceStatus: EventAttendanceStatus, + onClick: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.clickable { onClick() }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { + WappAttendanceBadge(eventAttendanceStatus.isAttendance) + Text( + text = eventAttendanceStatus.title, + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.white, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 10.dp), + ) + } + Text( + text = eventAttendanceStatus.startDateTime.format(DateUtil.MMddFormatter), + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.gray95, + modifier = Modifier.padding(start = 10.dp), + ) + } +} + +@Composable +private fun WappAttendanceBadge(isAttendance: Boolean) { + val drawableId = if (isAttendance) R.drawable.ic_attendance else R.drawable.ic_absent + Image(painter = painterResource(id = drawableId), contentDescription = "") +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappProfileCard.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappProfileCard.kt new file mode 100644 index 000000000..b4b576bf7 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappProfileCard.kt @@ -0,0 +1,77 @@ +package com.wap.wapp.feature.profile.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.Column +import androidx.compose.foundation.layout.Row +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.material.Card +import androidx.compose.material.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.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme + +@Composable +internal fun WappProfileCard( + position: String, + githubImage: Int, + catImage: Int, + brush: Brush, + userName: String, +) { + Card( + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .fillMaxWidth() + .height(130.dp) + .padding(horizontal = 10.dp), + ) { + Box(modifier = Modifier.background(brush = brush)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.CenterStart), + ) { + Image( + painter = painterResource(id = githubImage), + contentDescription = "", + modifier = Modifier.padding(start = 20.dp), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(start = 5.dp), + ) { + Text( + text = userName, + style = WappTheme.typography.contentRegular.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + ) + + Text( + text = position, + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.white, + ) + } + } + + Image( + painter = painterResource(id = catImage), + contentDescription = null, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 10.dp, bottom = 5.dp), + ) + } + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappSurveyHistoryRow.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappSurveyHistoryRow.kt new file mode 100644 index 000000000..4acbd4380 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/component/WappSurveyHistoryRow.kt @@ -0,0 +1,54 @@ +package com.wap.wapp.feature.profile.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.designresource.R +import com.wap.wapp.core.model.survey.Survey + +@Composable +internal fun WappSurveyHistoryRow( + survey: Survey, + modifier: Modifier = Modifier, + onClick: (String) -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .padding(horizontal = 10.dp) + .clickable { onClick(survey.surveyId) }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { + Image( + painter = painterResource(id = R.drawable.ic_yellow_check), + contentDescription = "", + ) + + Text( + text = survey.title, + style = WappTheme.typography.labelRegular, + color = WappTheme.colors.white, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 10.dp), + ) + } + Image( + painter = painterResource(id = R.drawable.ic_small_right_arrow), + contentDescription = "", + modifier = Modifier.padding(start = 10.dp), + ) + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/model/EventAttendanceStatus.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/model/EventAttendanceStatus.kt new file mode 100644 index 000000000..f624fba65 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/model/EventAttendanceStatus.kt @@ -0,0 +1,11 @@ +package com.wap.wapp.feature.profile.model + +import java.time.LocalDateTime + +data class EventAttendanceStatus( + val content: String, + val eventId: String, + val title: String, + val startDateTime: LocalDateTime, + val isAttendance: Boolean = false, +) diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/navigation/ProfileNavigation.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/navigation/ProfileNavigation.kt new file mode 100644 index 000000000..0bfff2eb3 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/navigation/ProfileNavigation.kt @@ -0,0 +1,30 @@ +package com.wap.wapp.feature.profile.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.profile.ProfileRoute + +const val profileNavigationRoute = "profile_route" + +fun NavController.navigateToProfile(navOptions: NavOptions? = navOptions {}) { + this.navigate(profileNavigationRoute, navOptions) +} + +fun NavGraphBuilder.profileScreen( + navigateToProfileSetting: (String) -> Unit, + navigateToAttendance: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSurveyDetail: (String) -> Unit, +) { + composable(route = profileNavigationRoute) { + ProfileRoute( + navigateToProfileSetting = navigateToProfileSetting, + navigateToAttendance = navigateToAttendance, + navigateToSignIn = navigateToSignIn, + navigateToSurveyDetail = navigateToSurveyDetail, + ) + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/Const.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/Const.kt new file mode 100644 index 000000000..0fc43b110 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/Const.kt @@ -0,0 +1,7 @@ +package com.wap.wapp.feature.profile.profilesetting + +internal const val PRIVACY_POLICY_URL = + "https://www.notion.so/46beb7c4f3c2417bbec20eafd610d580?pvs=11" +internal const val FAQ_URL = "https://www.notion.so/46beb7c4f3c2417bbec20eafd610d580?pvs=11" +internal const val INQUIRY_URL = "https://forms.gle/DhVoqtEPkLJJrHa28" +internal const val TERMS_AND_POLICIES_URL = "https://www.notion.so/042dc914a6a34093a51658693e009411" diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingScreen.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingScreen.kt new file mode 100644 index 000000000..f2c7e360d --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingScreen.kt @@ -0,0 +1,197 @@ +package com.wap.wapp.feature.profile.profilesetting + +import android.content.Context +import android.content.Intent +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.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappRowBar +import com.wap.designsystem.component.WappSubTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R +import com.wap.wapp.feature.profile.R.string +import com.wap.wapp.feature.profile.profilesetting.ProfileSettingViewModel.EventResult.Failure +import com.wap.wapp.feature.profile.profilesetting.ProfileSettingViewModel.EventResult.Success +import com.wap.wapp.feature.profile.profilesetting.component.ProfileSettingDialog + +@Composable +internal fun ProfileSettingRoute( + userId: String, + navigateToProfile: () -> Unit, + navigateToSignIn: () -> Unit, + viewModel: ProfileSettingViewModel = hiltViewModel(), +) { + val snackBarHostState = remember { SnackbarHostState() } + + LaunchedEffect(true) { + viewModel.eventFlow.collect { eventResult -> + when (eventResult) { + is Failure -> + snackBarHostState.showSnackbar(eventResult.throwable.toSupportingText()) + is Success -> navigateToSignIn() + } + } + } + + ProfileSettingScreen( + withdrawal = { viewModel.withdrawal(userId) }, + signOut = viewModel::signOut, + snackBarHostState = snackBarHostState, + navigateToProfile = navigateToProfile, + ) +} + +@Composable +internal fun ProfileSettingScreen( + navigateToProfile: () -> Unit, + withdrawal: () -> Unit, + signOut: () -> Unit, + snackBarHostState: SnackbarHostState, +) { + var showWithdrawalDialog by remember { mutableStateOf(false) } + var showLogoutDialog by remember { mutableStateOf(false) } + val dividerColor = WappTheme.colors.black42 + val scrollState = rememberScrollState() + val context = LocalContext.current + + if (showWithdrawalDialog) { + ProfileSettingDialog( + onDismissRequest = { showWithdrawalDialog = false }, + onConfirmRequest = withdrawal, + title = string.withdrawal, + ) + } + + if (showLogoutDialog) { + ProfileSettingDialog( + onDismissRequest = { showLogoutDialog = false }, + onConfirmRequest = signOut, + title = string.logout, + ) + } + + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + snackbarHost = { SnackbarHost(snackBarHostState) }, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .verticalScroll(scrollState) + .background(color = WappTheme.colors.backgroundBlack), + ) { + WappSubTopBar( + titleRes = string.more, + showLeftButton = true, + onClickLeftButton = navigateToProfile, + modifier = Modifier.padding(top = 16.dp), + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp), + modifier = Modifier.padding(start = 15.dp, top = 20.dp, bottom = 25.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_account_setting), + contentDescription = "", + ) + + Text( + text = stringResource(id = string.account_setting), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + } + + WappRowBar( + title = stringResource(id = string.logout), + onClicked = { showLogoutDialog = true }, + ) + + Divider(color = dividerColor) + + WappRowBar( + title = stringResource(id = string.withdrawal), + onClicked = { showWithdrawalDialog = true }, + ) + + Divider(color = dividerColor) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp), + modifier = Modifier.padding(start = 15.dp, top = 25.dp, bottom = 25.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_profile_more), + contentDescription = "", + ) + + Text( + text = stringResource(id = string.more), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + ) + } + + WappRowBar( + title = stringResource(id = string.inquiry), + onClicked = { navigateToUri(context, INQUIRY_URL) }, + ) + + Divider(color = dividerColor) + + WappRowBar( + title = stringResource(id = string.terms_and_policies), + onClicked = { navigateToUri(context, TERMS_AND_POLICIES_URL) }, + ) + + Divider(color = dividerColor) + + WappRowBar( + title = stringResource(id = string.privacy_policy), + onClicked = { navigateToUri(context, PRIVACY_POLICY_URL) }, + ) + + Divider(color = dividerColor) + } + } +} + +private fun navigateToUri(context: Context, url: String) = startActivity( + context, + generateUriIntent(url), + null, +) + +private fun generateUriIntent(url: String) = Intent(Intent.ACTION_VIEW, url.toUri()) diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingViewModel.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingViewModel.kt new file mode 100644 index 000000000..a0e11e3ce --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/ProfileSettingViewModel.kt @@ -0,0 +1,36 @@ +package com.wap.wapp.feature.profile.profilesetting + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.auth.DeleteUserUseCase +import com.wap.wapp.core.domain.usecase.auth.SignOutUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProfileSettingViewModel @Inject constructor( + private val signOutUseCase: SignOutUseCase, + private val deleteUserUseCase: DeleteUserUseCase, +) : ViewModel() { + private val _eventFlow: MutableSharedFlow = MutableSharedFlow() + val eventFlow: SharedFlow = _eventFlow.asSharedFlow() + + fun signOut() = viewModelScope.launch { + signOutUseCase().onSuccess { _eventFlow.emit(EventResult.Success) } + .onFailure { _eventFlow.emit(EventResult.Failure(it)) } + } + + fun withdrawal(userId: String) = viewModelScope.launch { + deleteUserUseCase(userId).onSuccess { _eventFlow.emit(EventResult.Success) } + .onFailure { _eventFlow.emit(EventResult.Failure(it)) } + } + + sealed class EventResult { + data class Failure(val throwable: Throwable) : EventResult() + data object Success : EventResult() + } +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/GuestProfile.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/GuestProfile.kt new file mode 100644 index 000000000..62ba49dda --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/GuestProfile.kt @@ -0,0 +1,77 @@ +package com.wap.wapp.feature.profile.profilesetting.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.profile.R + +@Composable +internal fun GuestProfile(navigateToSignIn: () -> Unit) { + Text( + text = SpannableGuestText(), + color = WappTheme.colors.white, + style = WappTheme.typography.titleRegular.copy(fontSize = 26.sp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(top = 60.dp), + ) + + Card( + shape = RoundedCornerShape(10.dp), + backgroundColor = WappTheme.colors.yellow34, + modifier = Modifier + .padding(horizontal = 15.dp) + .padding(top = 40.dp) + .height(50.dp) + .fillMaxWidth() + .clickable { navigateToSignIn() }, + ) { + Text( + text = stringResource(id = R.string.navigate_to_login), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier.wrapContentHeight(), + ) + } +} + +@Composable +private fun SpannableGuestText() = buildAnnotatedString { + append("๋กœ๊ทธ์ธํ•˜์—ฌ\n") + withStyle( + style = SpanStyle( + color = WappTheme.colors.yellow34, + textDecoration = TextDecoration.Underline, + ), + ) { + append("WAPP") + } + append(" ์™€ ") + withStyle( + style = SpanStyle( + color = WappTheme.colors.yellow34, + textDecoration = TextDecoration.Underline, + ), + ) { + append("์ถ”์–ต") + } + append("์„ ์Œ“์•„๋ณด์„ธ์š”!") +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/ProfileSettingDialog.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/ProfileSettingDialog.kt new file mode 100644 index 000000000..c0e6bd3d0 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/ProfileSettingDialog.kt @@ -0,0 +1,142 @@ +package com.wap.wapp.feature.profile.profilesetting.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +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.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.profile.R.string + +@Composable +internal fun ProfileSettingDialog( + onDismissRequest: () -> Unit, + onConfirmRequest: () -> Unit = {}, + @StringRes title: Int, +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = stringResource(title), + style = WappTheme.typography.contentBold.copy(fontSize = 20.sp), + color = WappTheme.colors.yellow34, + modifier = Modifier.padding(top = 16.dp), + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Text( + text = generateDialogContentString(title = title), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp), + ) { + Button( + onClick = onConfirmRequest, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.yellow34, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(string.complete), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.black, + ) + } + + Button( + onClick = onDismissRequest, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = WappTheme.colors.black25, + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .weight(1f) + .border( + width = 1.dp, + color = WappTheme.colors.yellow34, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = stringResource(string.cancel), + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.yellow34, + ) + } + } + } + } +} + +@Composable +private fun generateDialogContentString(@StringRes title: Int) = buildAnnotatedString { + append("์ •๋ง๋กœ ") + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append(stringResource(title)) + } + append("์„ ์›ํ•˜์‹ ๋‹ค๋ฉด ") + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + color = WappTheme.colors.yellow34, + ), + ) { + append("์™„๋ฃŒ") + } + append(" ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”.") +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/UserProfile.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/UserProfile.kt new file mode 100644 index 000000000..d2d0488c0 --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/component/UserProfile.kt @@ -0,0 +1,275 @@ +package com.wap.wapp.feature.profile.profilesetting.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.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.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.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.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.NothingToShow +import com.wap.designsystem.component.WappCard +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.core.model.event.Event +import com.wap.wapp.feature.profile.ProfileViewModel +import com.wap.wapp.feature.profile.R +import com.wap.wapp.feature.profile.component.WappAttendacneRow +import com.wap.wapp.feature.profile.component.WappSurveyHistoryRow + +@Composable +internal fun UserProfile( + todayEventsState: ProfileViewModel.EventsState, + recentEventsState: ProfileViewModel.EventAttendanceStatusState, + userRespondedSurveysState: ProfileViewModel.SurveysState, + attendanceCardBoardColor: Color, + navigateToAttendance: () -> Unit, + navigateToSurveyDetail: (String) -> Unit, +) { + Column(modifier = Modifier.padding(horizontal = 10.dp)) { + ProfileAttendanceCard( + todayEventsState = todayEventsState, + attendanceCardBoardColor = attendanceCardBoardColor, + modifier = Modifier.padding(top = 20.dp), + navigateToAttendance = navigateToAttendance, + ) + + MyAttendanceStatus( + recentEventsState = recentEventsState, + modifier = Modifier.padding(top = 20.dp), + ) + + MySurveyHistory( + userRespondedSurveysState = userRespondedSurveysState, + navigateToSurveyDetail = navigateToSurveyDetail, + modifier = Modifier.padding(vertical = 20.dp), + ) + } +} + +@Composable +private fun ProfileAttendanceCard( + todayEventsState: ProfileViewModel.EventsState, + attendanceCardBoardColor: Color, + modifier: Modifier, + navigateToAttendance: () -> Unit, +) { + when (todayEventsState) { + is ProfileViewModel.EventsState.Loading -> CircleLoader(modifier = Modifier.fillMaxSize()) + is ProfileViewModel.EventsState.Success -> { + val cardModifier = if (todayEventsState.events.isNotEmpty()) { + modifier + .border( + width = 2.dp, + color = attendanceCardBoardColor, + shape = RoundedCornerShape(10.dp), + ) + .fillMaxWidth() + .height(130.dp) + .clickable { navigateToAttendance() } + } else { + modifier + .fillMaxWidth() + .height(130.dp) + } + + WappCard( + modifier = cardModifier, + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(id = R.string.wap_attendance), + style = WappTheme.typography.captionBold.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + ) + + Image( + painter = painterResource(id = drawable.ic_check), + contentDescription = "", + modifier = Modifier.padding(start = 10.dp), + ) + } + Text( + text = DateUtil.generateNowDate().format(DateUtil.yyyyMMddFormatter), + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 20.dp), + ) + + if (todayEventsState.events.isEmpty()) { + Text( + text = stringResource(id = R.string.no_event_today), + style = WappTheme.typography.contentRegular.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 5.dp), + ) + } else { + Text( + text = generateTodayEventString(events = todayEventsState.events), + style = WappTheme.typography.contentRegular.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + modifier = Modifier.padding(top = 5.dp), + ) + } + } + if (todayEventsState.events.isNotEmpty()) { + Image( + painter = painterResource(id = drawable.ic_forward), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 12.dp), + ) + } + } + } + } + } +} + +@Composable +private fun MyAttendanceStatus( + recentEventsState: ProfileViewModel.EventAttendanceStatusState, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = stringResource(id = R.string.my_attendance), + style = WappTheme.typography.titleBold.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + modifier = Modifier.padding(start = 5.dp), + ) + + WappCard( + modifier = Modifier + .fillMaxWidth() + .height(130.dp) + .padding(top = 10.dp), + ) { + when (recentEventsState) { + is ProfileViewModel.EventAttendanceStatusState.Loading -> CircleLoader( + modifier = Modifier + .padding(vertical = 10.dp) + .height(130.dp), + ) + + is ProfileViewModel.EventAttendanceStatusState.Success -> { + if (recentEventsState.events.isEmpty()) { + NothingToShow(title = R.string.no_events_recently) + return@WappCard + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .padding(10.dp) + .height(130.dp), + ) { + items( + items = recentEventsState.events, + key = { event -> event.eventId }, + ) { event -> + WappAttendacneRow(eventAttendanceStatus = event) + } + } + } + } + } + } +} + +@Composable +private fun MySurveyHistory( + userRespondedSurveysState: ProfileViewModel.SurveysState, + navigateToSurveyDetail: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = stringResource(id = R.string.survey_i_did), + style = WappTheme.typography.titleBold.copy(fontSize = 20.sp), + color = WappTheme.colors.white, + modifier = Modifier.padding(start = 5.dp), + ) + + WappCard( + modifier = Modifier + .fillMaxWidth() + .height(130.dp) + .padding(top = 10.dp), + ) { + when (userRespondedSurveysState) { + is ProfileViewModel.SurveysState.Loading -> CircleLoader( + modifier = Modifier + .padding(vertical = 10.dp) + .height(130.dp), + ) + + is ProfileViewModel.SurveysState.Success -> { + if (userRespondedSurveysState.surveys.isEmpty()) { + NothingToShow(title = R.string.no_surveys_after_sign_up) + return@WappCard + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .padding(10.dp) + .height(130.dp), + ) { + items( + items = userRespondedSurveysState.surveys, + key = { survey -> survey.surveyId }, + ) { survey -> + WappSurveyHistoryRow( + survey, + onClick = navigateToSurveyDetail, + ) + } + } + } + } + } + } +} + +@Composable +private fun generateTodayEventString(events: List) = buildAnnotatedString { + append("์˜ค๋Š˜์€ ") + + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + textDecoration = TextDecoration.Underline, + ), + ) { + append(events.map { it.title }.joinToString(separator = ", ")) + } + + append(" ๋‚  ์ด์—์š”!") +} diff --git a/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/navigation/ProfileSettingNavigation.kt b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/navigation/ProfileSettingNavigation.kt new file mode 100644 index 000000000..620e3d00b --- /dev/null +++ b/feature/profile/src/main/java/com/wap/wapp/feature/profile/profilesetting/navigation/ProfileSettingNavigation.kt @@ -0,0 +1,38 @@ +package com.wap.wapp.feature.profile.profilesetting.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 androidx.navigation.navOptions +import com.wap.wapp.feature.profile.profilesetting.ProfileSettingRoute + +const val profileSettingNavigationRoute = "profile_setting_route/{userId}" + +fun NavController.navigateToProfileSetting( + userId: String, + navOptions: NavOptions? = navOptions {}, +) { + this.navigate("profile_setting_route/$userId", navOptions) +} + +fun NavGraphBuilder.profileSettingScreen( + navigateToSignIn: () -> Unit, + navigateToProfile: () -> Unit, +) { + composable( + route = profileSettingNavigationRoute, + arguments = listOf( + navArgument("userId") { type = NavType.StringType }, + ), + ) { navBackStackEntry -> + val userId = navBackStackEntry.arguments?.getString("userId") ?: "" + ProfileSettingRoute( + userId = userId, + navigateToSignIn = navigateToSignIn, + navigateToProfile = navigateToProfile, + ) + } +} diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml new file mode 100644 index 000000000..d56ef8a9e --- /dev/null +++ b/feature/profile/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + ๋งˆ์ด ํŽ˜์ด์ง€ ์„ค์ •์œผ๋กœ ๊ฐ€๋Š” ํ†ฑ๋‹ˆ๋ฐ”ํ€ด ๋ชจ์–‘ ๋ฒ„ํŠผ์ž…๋‹ˆ๋‹ค. + ์ผ๋ฐ˜ ํšŒ์› + ์šด์˜์ง„ + ๊ฒŒ์ŠคํŠธ + ํ”„๋กœํ•„ + ๋‚ด๊ฐ€ ์ฐธ๊ฐ€ํ•œ ์ผ์ •์ด๋‚˜ ์„ค๋ฌธ์„ ํ™•์ธํ•ด์š”! + ๋น„ํšŒ์› + ๋‚˜์˜ ์ถœ๊ฒฐ ํ˜„ํ™ฉ + ๊ฒฐ์„ + ๋‚ด๊ฐ€ ํ•œ ์„ค๋ฌธ + ๊ณ„์ • ์„ค์ • + ์•Œ๋žŒ ์„ค์ • + ๋กœ๊ทธ์•„์›ƒ + ํšŒ์›ํƒˆํ‡ด + ๋ฌธ์˜ํ•˜๊ธฐ + ์™„๋ฃŒ + ์ทจ์†Œ + ๋”๋ณด๊ธฐ + FAQ + ์•ฝ๊ด€ ๋ฐ ์ •์ฑ… + ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ ๋ฐฉ์นจ + ๋กœ๊ทธ์ธ ํ•˜๋Ÿฌ ๊ฐ€๊ธฐ + WAP ์ถœ์„ + ์˜ค๋Š˜์€ ๋ณ„ ๋‹ค๋ฅธ ํ–‰์‚ฌ๊ฐ€ ์—†์–ด์š”! + ๊ฐ€์ž…ํ•œ ์ดํ›„๋กœ ์ฐธ์—ฌํ•œ ์„ค๋ฌธ์ด ์—†์–ด์š”! + ๊ฐ€์ž…ํ•œ ์ดํ›„๋กœ ์ง„ํ–‰๋œ ์ผ์ •์ด ์—†์–ด์š”! + diff --git a/feature/profile/src/test/java/com/wap/wapp/feature/profile/ExampleUnitTest.kt b/feature/profile/src/test/java/com/wap/wapp/feature/profile/ExampleUnitTest.kt new file mode 100644 index 000000000..496031f6f --- /dev/null +++ b/feature/profile/src/test/java/com/wap/wapp/feature/profile/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.profile + +import org.junit.Assert.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/feature/splash/.gitignore b/feature/splash/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/splash/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/splash/build.gradle.kts b/feature/splash/build.gradle.kts new file mode 100644 index 000000000..fcd1e02dd --- /dev/null +++ b/feature/splash/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("com.wap.wapp.feature") + id("com.wap.wapp.hilt") +} + +android { + namespace = "com.wap.wapp.feature.splash" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:common")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/splash/consumer-rules.pro b/feature/splash/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/splash/proguard-rules.pro b/feature/splash/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/splash/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/splash/src/androidTest/java/com/wap/wapp/feature/splash/ExampleInstrumentedTest.kt b/feature/splash/src/androidTest/java/com/wap/wapp/feature/splash/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..f15d27445 --- /dev/null +++ b/feature/splash/src/androidTest/java/com/wap/wapp/feature/splash/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.splash + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.feature.splash.test", appContext.packageName) + } +} diff --git a/feature/splash/src/main/AndroidManifest.xml b/feature/splash/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/splash/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplahScreen.kt b/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplahScreen.kt new file mode 100644 index 000000000..cecf83ca3 --- /dev/null +++ b/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplahScreen.kt @@ -0,0 +1,157 @@ +package com.wap.wapp.feature.splash + +import android.app.Activity +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +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.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.size +import androidx.compose.material.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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.commmon.extensions.showToast +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R +import com.wap.wapp.feature.splash.R.string + +@Composable +internal fun SplashRoute( + viewModel: SplashViewModel = hiltViewModel(), + navigateToAuth: () -> Unit, + navigateToNotice: () -> Unit, +) { + val context = LocalContext.current as Activity + val isIconLogoVisible by viewModel.isIconLogoVisible.collectAsStateWithLifecycle() + val isIconLogoGoUp by viewModel.isIconLogoGoUp.collectAsStateWithLifecycle() + + LaunchedEffect(true) { + viewModel.splashUiEvent.collect { event -> + when (event) { + is SplashViewModel.SplashEvent.SignInUser -> navigateToNotice() + is SplashViewModel.SplashEvent.NonSignInUser -> navigateToAuth() + is SplashViewModel.SplashEvent.Failure -> { + navigateToAuth() + context.showToast(event.throwable.toSupportingText()) + } + } + } + } + SplashScreen( + isIconLogoVisible = isIconLogoVisible, + isIconLogoGoUp = isIconLogoGoUp, + ) +} + +@Composable +internal fun SplashScreen( + isIconLogoVisible: Boolean, + isIconLogoGoUp: Boolean, +) { + val ANIMATION_MILLS = 400 + + Column( + modifier = Modifier + .fillMaxSize() + .background(WappTheme.colors.backgroundBlack), + ) { + AnimatedContent( + targetState = isIconLogoVisible, + transitionSpec = { + scaleIn(tween(ANIMATION_MILLS, ANIMATION_MILLS)) togetherWith + scaleOut(tween(ANIMATION_MILLS)) + }, + ) { isIconLogoVisible -> + if (!isIconLogoVisible) { + SplashTypoLogo() + } else { + SplashIconLogo(isIconLogoGoUp) + } + } + } +} + +@Composable +private fun SplashTypoLogo() { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize(), + ) { + Image( + painter = painterResource(id = com.wap.wapp.feature.splash.R.drawable.ic_wapp_logo), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(width = 230.dp, height = 230.dp), + contentDescription = stringResource(id = string.wapp_icon_description), + ) + } +} + +@Composable +private fun SplashIconLogo(isIconLogoGoUp: Boolean) { + val animatedPadding by animateDpAsState( + targetValue = if (isIconLogoGoUp) 200.dp else 0.dp, + animationSpec = tween(1000), + ) + + Column(modifier = Modifier.fillMaxSize()) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .weight(1f) + .padding(bottom = animatedPadding), + ) { + Image( + painter = painterResource(id = R.drawable.img_white_cat), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(width = 230.dp, height = 230.dp), + contentDescription = stringResource(id = string.wapp_icon_description), + ) + + Row( + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Column { + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = stringResource(id = string.application_name), + style = WappTheme.typography.titleBold, + fontSize = 48.sp, + color = WappTheme.colors.white, + ) + } + Text( + text = stringResource(id = string.application_name), + fontSize = 48.sp, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.yellow34, + ) + } + } + } +} diff --git a/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplashViewModel.kt b/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplashViewModel.kt new file mode 100644 index 000000000..2a14d9c2a --- /dev/null +++ b/feature/splash/src/main/java/com/wap/wapp/feature/splash/SplashViewModel.kt @@ -0,0 +1,57 @@ +package com.wap.wapp.feature.splash + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.auth.IsUserSignInUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SplashViewModel @Inject constructor( + private val isUserSignInUseCase: IsUserSignInUseCase, +) : ViewModel() { + private val _splashUiEvent = MutableSharedFlow() + val splashUiEvent = _splashUiEvent.asSharedFlow() + + private var _isIconLogoVisible = MutableStateFlow(false) + var isIconLogoVisible = _isIconLogoVisible.asStateFlow() + + private var _isIconLogoGoUp = MutableStateFlow(false) + var isIconLogoGoUp = _isIconLogoGoUp.asStateFlow() + + init { + viewModelScope.launch { + delay(1500) + _isIconLogoVisible.value = true + delay(1000) + isUserSignIn() + } + } + + private suspend fun isUserSignIn() { + isUserSignInUseCase() + .onSuccess { isSignIn -> + if (isSignIn) { + _splashUiEvent.emit(SplashEvent.SignInUser) + } else { + _isIconLogoGoUp.value = true + delay(1000) + _splashUiEvent.emit(SplashEvent.NonSignInUser) + } + }.onFailure { throwable -> + _splashUiEvent.emit(SplashEvent.Failure(throwable)) + } + } + + sealed class SplashEvent { + data object SignInUser : SplashEvent() + data object NonSignInUser : SplashEvent() + data class Failure(val throwable: Throwable) : SplashEvent() + } +} diff --git a/feature/splash/src/main/java/com/wap/wapp/feature/splash/navigation/SplashNavigation.kt b/feature/splash/src/main/java/com/wap/wapp/feature/splash/navigation/SplashNavigation.kt new file mode 100644 index 000000000..b94b9a96f --- /dev/null +++ b/feature/splash/src/main/java/com/wap/wapp/feature/splash/navigation/SplashNavigation.kt @@ -0,0 +1,26 @@ +package com.wap.wapp.feature.splash.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navOptions +import com.wap.wapp.feature.splash.SplashRoute + +const val splashNavigationRoute = "splash_route" + +fun NavController.navigateToSplash(navOptions: NavOptions? = navOptions {}) { + this.navigate(splashNavigationRoute, navOptions) +} + +fun NavGraphBuilder.splashScreen( + navigateToAuth: () -> Unit, + navigateToNotice: () -> Unit, +) { + composable(route = splashNavigationRoute) { + SplashRoute( + navigateToAuth = navigateToAuth, + navigateToNotice = navigateToNotice, + ) + } +} diff --git a/feature/splash/src/main/res/drawable/ic_splash_logo.png b/feature/splash/src/main/res/drawable/ic_splash_logo.png new file mode 100644 index 000000000..a02b63cd9 Binary files /dev/null and b/feature/splash/src/main/res/drawable/ic_splash_logo.png differ diff --git a/feature/splash/src/main/res/drawable/ic_wapp_logo.png b/feature/splash/src/main/res/drawable/ic_wapp_logo.png new file mode 100644 index 000000000..01b140fa7 Binary files /dev/null and b/feature/splash/src/main/res/drawable/ic_wapp_logo.png differ diff --git a/feature/splash/src/main/res/values/strings.xml b/feature/splash/src/main/res/values/strings.xml new file mode 100644 index 000000000..bdedd8ee6 --- /dev/null +++ b/feature/splash/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + WAPP + WAPP Icon + WAPP ICON + diff --git a/feature/splash/src/test/java/com/wap/wapp/feature/splash/ExampleUnitTest.kt b/feature/splash/src/test/java/com/wap/wapp/feature/splash/ExampleUnitTest.kt new file mode 100644 index 000000000..abe9e31e2 --- /dev/null +++ b/feature/splash/src/test/java/com/wap/wapp/feature/splash/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.splash + +import org.junit.Assert.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/feature/survey-check/.gitignore b/feature/survey-check/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/survey-check/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/survey-check/build.gradle.kts b/feature/survey-check/build.gradle.kts new file mode 100644 index 000000000..725af0993 --- /dev/null +++ b/feature/survey-check/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.wap.wapp.feature") +} + +android { + namespace = "com.wap.wapp.feature.survey.check" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/survey-check/consumer-rules.pro b/feature/survey-check/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/survey-check/proguard-rules.pro b/feature/survey-check/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/survey-check/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/survey-check/src/main/AndroidManifest.xml b/feature/survey-check/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/feature/survey-check/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckScreen.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckScreen.kt new file mode 100644 index 000000000..1be8c7218 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckScreen.kt @@ -0,0 +1,78 @@ +package com.wap.wapp.feature.survey.check + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets +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.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappRightMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun SurveyCheckScreen( + viewModel: SurveyCheckViewModel = hiltViewModel(), + navigateToSurveyDetail: (String) -> Unit, + navigateToSurvey: () -> Unit, +) { + val surveyListUiState = viewModel.surveyListUiState.collectAsStateWithLifecycle().value + val snackBarHostState = remember { SnackbarHostState() } + + LaunchedEffect(true) { + viewModel.errorFlow.collectLatest { + snackBarHostState.showSnackbar(it.toSupportingText()) + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + topBar = { + WappRightMainTopBar( + titleRes = R.string.survey_check, + contentRes = R.string.survey_check_content, + showBackButton = true, + onClickBackButton = navigateToSurvey, + ) + }, + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + when (surveyListUiState) { + is SurveyCheckViewModel.SurveyListUiState.Init -> { + CircleLoader(modifier = Modifier.fillMaxSize()) + } + + is SurveyCheckViewModel.SurveyListUiState.Success -> { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 20.dp) + .padding(paddingValues), + ) { + val surveyList = surveyListUiState.surveyList + items(surveyList) { survey -> + SurveyItemCard( + onCardClicked = navigateToSurveyDetail, + survey = survey, + ) + } + } + } + } + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckViewModel.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckViewModel.kt new file mode 100644 index 000000000..54a4a2f20 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyCheckViewModel.kt @@ -0,0 +1,46 @@ +package com.wap.wapp.feature.survey.check + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.survey.GetSurveyListUseCase +import com.wap.wapp.core.model.survey.Survey +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SurveyCheckViewModel @Inject constructor( + private val getSurveyListUseCase: GetSurveyListUseCase, +) : ViewModel() { + private val _surveyListUiState: MutableStateFlow = + MutableStateFlow(SurveyListUiState.Init) + val surveyListUiState = _surveyListUiState.asStateFlow() + + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow = _errorFlow.asSharedFlow() + + init { + getSurveyList() + } + + private fun getSurveyList() { + viewModelScope.launch { + getSurveyListUseCase() + .onSuccess { surveyList -> + _surveyListUiState.value = SurveyListUiState.Success(surveyList) + } + .onFailure { throwable -> + _errorFlow.emit(throwable) + } + } + } + + sealed class SurveyListUiState { + data object Init : SurveyListUiState() + data class Success(val surveyList: List) : SurveyListUiState() + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyItemCard.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyItemCard.kt new file mode 100644 index 000000000..7fde7fc95 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/SurveyItemCard.kt @@ -0,0 +1,67 @@ +package com.wap.wapp.feature.survey.check + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.model.survey.Survey + +@Composable +internal fun SurveyItemCard( + onCardClicked: (String) -> Unit, + survey: Survey, +) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + modifier = Modifier + .fillMaxWidth() + .clickable { onCardClicked(survey.surveyId) }, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = survey.title, + color = WappTheme.colors.white, + style = WappTheme.typography.titleBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + Text( + text = survey.calculateSurveyedAt(), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.captionMedium, + modifier = Modifier.padding(start = 8.dp), + ) + } + + Text( + text = "${survey.userName} โ€ข ${survey.eventName}", + color = WappTheme.colors.grayBD, + style = WappTheme.typography.labelMedium, + maxLines = 1, + ) + } + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/ObjectiveQuestionCard.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/ObjectiveQuestionCard.kt new file mode 100644 index 000000000..70d019c67 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/ObjectiveQuestionCard.kt @@ -0,0 +1,91 @@ +package com.wap.wapp.feature.survey.check.detail + +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.model.survey.Rating +import com.wap.wapp.core.model.survey.SurveyAnswer +import com.wap.wapp.core.model.survey.toDescription + +@Composable +internal fun ObjectiveQuestionCard(surveyAnswer: SurveyAnswer) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = surveyAnswer.questionTitle, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + Rating.values().forEach { rating -> + if (rating.toString() == surveyAnswer.questionAnswer) { + ObjectiveAnswerIndicator(rating = rating, selected = true) + } else { + ObjectiveAnswerIndicator(rating = rating, selected = false) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ObjectiveAnswerIndicator( + rating: Rating, + selected: Boolean, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = rating.toDescription().title, + style = WappTheme.typography.captionRegular, + color = WappTheme.colors.white, + ) + + FilterChip( + selected = selected, + onClick = { }, + label = { }, + colors = FilterChipDefaults.filterChipColors( + containerColor = WappTheme.colors.black42, + selectedContainerColor = WappTheme.colors.yellow34, + ), + modifier = Modifier.size(height = 40.dp, width = 80.dp), + ) + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SubjectiveQuestionCard.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SubjectiveQuestionCard.kt new file mode 100644 index 000000000..0303056a0 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SubjectiveQuestionCard.kt @@ -0,0 +1,53 @@ +package com.wap.wapp.feature.survey.check.detail + +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.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.model.survey.SurveyAnswer + +@Composable +internal fun SubjectiveQuestionCard(surveyAnswer: SurveyAnswer) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + text = surveyAnswer.questionTitle, + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = surveyAnswer.questionAnswer, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.yellow34, + textAlign = TextAlign.Start, + ) + + Divider( + color = WappTheme.colors.gray82, + modifier = Modifier.padding(top = 5.dp), + ) + } + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailScreen.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailScreen.kt new file mode 100644 index 000000000..c8f080ee7 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailScreen.kt @@ -0,0 +1,137 @@ +package com.wap.wapp.feature.survey.check.detail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappButton +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.feature.survey.check.navigation.SurveyDetailBackStack +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun SurveyDetailRoute( + viewModel: SurveyDetailViewModel = hiltViewModel(), + surveyId: String, + backStack: String, + navigateToSurveyCheck: () -> Unit, + navigateToProfile: () -> Unit, +) { + val surveyUiState by viewModel.surveyUiState.collectAsStateWithLifecycle() + val snackBarHostState = remember { SnackbarHostState() } + val navigateToPrevPage = { + if (backStack == SurveyDetailBackStack.SURVEY_CHECK.name) { + navigateToSurveyCheck() + } else { + navigateToProfile() + } + } + + LaunchedEffect(true) { + viewModel.getSurvey(surveyId) + } + + LaunchedEffect(true) { + viewModel.errorFlow.collectLatest { + snackBarHostState.showSnackbar(it.toSupportingText()) + } + } + + SurveyDetailScreen( + snackBarHostState = snackBarHostState, + surveyUiState = surveyUiState, + onDoneButtonClicked = navigateToPrevPage, + onBackButtonClicked = navigateToPrevPage, + ) +} + +@Composable +internal fun SurveyDetailScreen( + snackBarHostState: SnackbarHostState, + surveyUiState: SurveyDetailViewModel.SurveyUiState, + onDoneButtonClicked: () -> Unit, + onBackButtonClicked: () -> Unit, +) { + val scrollState = rememberScrollState() + + Scaffold( + topBar = { + SurveyDetailTopBar( + onBackButtonClicked = onBackButtonClicked, + ) + }, + contentWindowInsets = WindowInsets(0.dp), + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(top = 16.dp, start = 8.dp, end = 8.dp), + ) { + Column( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + when (surveyUiState) { + is SurveyDetailViewModel.SurveyUiState.Loading -> { + Spacer(modifier = Modifier.weight(1f)) + + CircleLoader(modifier = Modifier.fillMaxSize()) + + Spacer(modifier = Modifier.weight(1f)) + } + + is SurveyDetailViewModel.SurveyUiState.Success -> { + SurveyInformationCard( + title = surveyUiState.survey.title, + content = surveyUiState.survey.content, + userName = surveyUiState.survey.userName, + eventName = surveyUiState.survey.eventName, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + surveyUiState.survey.surveyAnswerList.forEach { surveyAnswer -> + when (surveyAnswer.questionType) { + QuestionType.OBJECTIVE -> ObjectiveQuestionCard(surveyAnswer) + + QuestionType.SUBJECTIVE -> SubjectiveQuestionCard(surveyAnswer) + } + } + } + } + } + } + + WappButton( + onClick = onDoneButtonClicked, + textRes = com.wap.wapp.core.designsystem.R.string.done, + modifier = Modifier.padding(vertical = 20.dp), + ) + } + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailTopBar.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailTopBar.kt new file mode 100644 index 000000000..5028b1cac --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailTopBar.kt @@ -0,0 +1,52 @@ +package com.wap.wapp.feature.survey.check.detail + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.survey.check.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SurveyDetailTopBar( + onBackButtonClicked: () -> Unit, +) { + CenterAlignedTopAppBar( + windowInsets = WindowInsets(0.dp), + title = { + Text( + text = stringResource(R.string.check_survey), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = WappTheme.typography.contentBold, + color = WappTheme.colors.white, + ) + }, + navigationIcon = { + Icon( + painter = painterResource(id = com.wap.wapp.core.designsystem.R.drawable.ic_back), + contentDescription = + stringResource(com.wap.wapp.core.designsystem.R.string.back_button), + tint = WappTheme.colors.white, + modifier = Modifier + .padding(start = 16.dp) + .clickable { onBackButtonClicked() }, + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = WappTheme.colors.black25, + ), + ) +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailViewModel.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailViewModel.kt new file mode 100644 index 000000000..f3198e964 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyDetailViewModel.kt @@ -0,0 +1,44 @@ +package com.wap.wapp.feature.survey.check.detail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.survey.GetSurveyUseCase +import com.wap.wapp.core.model.survey.Survey +import dagger.hilt.android.lifecycle.HiltViewModel +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 +import javax.inject.Inject + +@HiltViewModel +class SurveyDetailViewModel @Inject constructor( + private val getSurveyUseCase: GetSurveyUseCase, +) : ViewModel() { + private val _errorFlow: MutableSharedFlow = MutableSharedFlow() + val errorFlow: SharedFlow = _errorFlow.asSharedFlow() + + private val _surveyUiState: MutableStateFlow = + MutableStateFlow(SurveyUiState.Loading) + val surveyUiState: StateFlow = _surveyUiState.asStateFlow() + + fun getSurvey(surveyId: String) { + viewModelScope.launch { + getSurveyUseCase(surveyId) + .onSuccess { survey -> + _surveyUiState.value = SurveyUiState.Success(survey) + } + .onFailure { throwable -> + _errorFlow.emit(throwable) + } + } + } + + sealed class SurveyUiState { + data object Loading : SurveyUiState() + data class Success(val survey: Survey) : SurveyUiState() + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyInformationCard.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyInformationCard.kt new file mode 100644 index 000000000..5b37a0889 --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/detail/SurveyInformationCard.kt @@ -0,0 +1,106 @@ +package com.wap.wapp.feature.survey.check.detail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.feature.survey.check.R + +@Composable +internal fun SurveyInformationCard( + title: String, + content: String, + userName: String, + eventName: String, +) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column { + Text( + text = title, + style = WappTheme.typography.titleBold, + fontSize = 22.sp, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + Divider() + } + + Text( + text = content, + style = WappTheme.typography.contentRegular, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + ) + + SurveyInformationContent( + title = stringResource(R.string.event), + content = eventName, + ) + + SurveyInformationContent( + title = stringResource(R.string.name), + content = userName, + ) + } + } +} + +@Composable +private fun SurveyInformationContent( + title: String, + content: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = WappTheme.typography.contentBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Start, + modifier = Modifier.weight(1f), + ) + + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black42, + ), + modifier = Modifier.weight(1f), + ) { + Text( + text = content, + style = WappTheme.typography.contentBold, + color = WappTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } + } +} diff --git a/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/navigation/SurveyCheckNavigation.kt b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/navigation/SurveyCheckNavigation.kt new file mode 100644 index 000000000..aede9359e --- /dev/null +++ b/feature/survey-check/src/main/java/com/wap/wapp/feature/survey/check/navigation/SurveyCheckNavigation.kt @@ -0,0 +1,65 @@ +package com.wap.wapp.feature.survey.check.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 androidx.navigation.navOptions +import com.wap.wapp.feature.survey.check.SurveyCheckScreen +import com.wap.wapp.feature.survey.check.detail.SurveyDetailRoute + +fun NavController.navigateToSurveyDetail( + surveyId: String, + backStack: SurveyDetailBackStack = SurveyDetailBackStack.SURVEY_CHECK, + navOptions: NavOptions? = navOptions {}, +) = this.navigate(SurveyCheckRoute.surveyDetailRoute(surveyId, backStack.name), navOptions) + +fun NavController.navigateToSurveyCheck(navOptions: NavOptions? = navOptions {}) = + this.navigate(SurveyCheckRoute.surveyCheckRoute, navOptions) + +fun NavGraphBuilder.surveyCheckNavGraph( + navigateToSurveyDetail: (String) -> Unit, + navigateToSurveyCheck: () -> Unit, + navigateToSurvey: () -> Unit, + navigateToProfile: () -> Unit, +) { + composable(route = SurveyCheckRoute.surveyCheckRoute) { + SurveyCheckScreen( + navigateToSurveyDetail = navigateToSurveyDetail, + navigateToSurvey = navigateToSurvey, + ) + } + + composable( + route = SurveyCheckRoute.surveyDetailRoute("{id}", "{backStack}"), + arguments = listOf( + navArgument("id") { + type = NavType.StringType + }, + navArgument("backStack") { + type = NavType.StringType + }, + ), + ) { navBackStackEntry -> + val surveyId = navBackStackEntry.arguments?.getString("id") ?: "" + val backStack = navBackStackEntry.arguments?.getString("backStack") ?: "SURVEY_CHECK" + SurveyDetailRoute( + surveyId = surveyId, + backStack = backStack, + navigateToSurveyCheck = navigateToSurveyCheck, + navigateToProfile = navigateToProfile, + ) + } +} + +object SurveyCheckRoute { + const val surveyCheckRoute = "survey/check" + fun surveyDetailRoute(surveyId: String, backStack: String) = + "survey/detail/$surveyId/$backStack" +} + +enum class SurveyDetailBackStack { + SURVEY_CHECK, PROFILE +} diff --git a/feature/survey-check/src/main/res/values/strings.xml b/feature/survey-check/src/main/res/values/strings.xml new file mode 100644 index 000000000..1d7a88ea7 --- /dev/null +++ b/feature/survey-check/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + ์„ค๋ฌธ ํ™•์ธ + ์ด๋ฆ„ + ๋‹ค์Œ + ์ผ์ • + ์„ค๋ฌธ ํ™•์ธ + ๊ตฌ์„ฑ์›์ด ์ž‘์„ฑํ•œ ์„ค๋ฌธ์„ ํ™•์ธํ•˜์„ธ์š” ! + diff --git a/feature/survey/.gitignore b/feature/survey/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/survey/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/survey/build.gradle.kts b/feature/survey/build.gradle.kts new file mode 100644 index 000000000..6f7d13e80 --- /dev/null +++ b/feature/survey/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.wap.wapp.feature") +} + +android { + namespace = "com.wap.wapp.feature.survey" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":core:designresource")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + implementation(project(":core:common")) + + implementation(libs.bundles.androidx) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} diff --git a/feature/survey/consumer-rules.pro b/feature/survey/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/survey/proguard-rules.pro b/feature/survey/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/survey/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/feature/survey/src/androidTest/java/com/wap/wapp/feature/survey/ExampleInstrumentedTest.kt b/feature/survey/src/androidTest/java/com/wap/wapp/feature/survey/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..e9b37d72c --- /dev/null +++ b/feature/survey/src/androidTest/java/com/wap/wapp/feature/survey/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.wap.wapp.feature.survey + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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("com.wap.wapp.feature.survey.test", appContext.packageName) + } +} diff --git a/feature/survey/src/main/AndroidManifest.xml b/feature/survey/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e10007615 --- /dev/null +++ b/feature/survey/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyContent.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyContent.kt new file mode 100644 index 000000000..a16a4f39c --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyContent.kt @@ -0,0 +1,93 @@ +package com.wap.wapp.feature.survey + +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.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.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.designresource.R +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.feature.survey.R.string + +@Composable +internal fun SurveyContent( + surveyFormList: List, + isManager: Boolean, + paddingValues: PaddingValues, + selectedSurveyForm: (String) -> Unit, + onSurveyCheckButtonClicked: () -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.End, + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .padding(paddingValues), + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + items(surveyFormList) { surveyForm -> + SurveyFormItemCard( + surveyForm = surveyForm, + selectedSurveyForm = selectedSurveyForm, + ) + } + } + + if (isManager) { + SurveyCheckButton(onSurveyCheckButtonClicked = onSurveyCheckButtonClicked) + } + } +} + +@Composable +private fun SurveyCheckButton( + onSurveyCheckButtonClicked: () -> Unit, +) { + ElevatedButton( + modifier = Modifier.height(48.dp), + onClick = { onSurveyCheckButtonClicked() }, + colors = ButtonDefaults.buttonColors( + contentColor = WappTheme.colors.black, + containerColor = WappTheme.colors.yellow34, + disabledContentColor = WappTheme.colors.white, + disabledContainerColor = WappTheme.colors.grayA2, + ), + shape = RoundedCornerShape(10.dp), + content = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(string.go_to_survey_check), + style = WappTheme.typography.contentRegular, + ) + + Icon( + painter = painterResource(id = R.drawable.ic_magnifier), + contentDescription = stringResource(string.survey_check_description), + ) + } + }, + ) +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyFormItemCard.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyFormItemCard.kt new file mode 100644 index 000000000..3d1ab42f6 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyFormItemCard.kt @@ -0,0 +1,65 @@ +package com.wap.wapp.feature.survey + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.model.survey.SurveyForm + +@Composable +internal fun SurveyFormItemCard( + surveyForm: SurveyForm, + selectedSurveyForm: (String) -> Unit, +) { + Card( + colors = CardDefaults.cardColors( + containerColor = WappTheme.colors.black25, + ), + modifier = Modifier + .fillMaxWidth() + .clickable { selectedSurveyForm(surveyForm.surveyFormId) }, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = surveyForm.title, + color = WappTheme.colors.white, + modifier = Modifier.weight(1f), + style = WappTheme.typography.titleBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = surveyForm.calculateDeadline(), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.captionMedium, + modifier = Modifier.padding(start = 8.dp), + ) + } + Text( + text = surveyForm.content, + color = WappTheme.colors.grayBD, + maxLines = 1, + style = WappTheme.typography.labelMedium, + ) + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyGuestScreen.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyGuestScreen.kt new file mode 100644 index 000000000..d210ff689 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyGuestScreen.kt @@ -0,0 +1,57 @@ +package com.wap.wapp.feature.survey + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton + +@Composable +internal fun SurveyGuestScreen( + onButtonClicked: () -> Unit, +) { + Surface( + color = WappTheme.colors.backgroundBlack, + modifier = Modifier.fillMaxSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(R.string.survey_guset_title), + style = WappTheme.typography.titleBold, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Text( + text = stringResource(R.string.survey_guest_content), + style = WappTheme.typography.captionMedium, + color = WappTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + + WappButton( + textRes = R.string.go_to_signin, + onClick = onButtonClicked, + modifier = Modifier.padding(horizontal = 32.dp), + ) + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyScreen.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyScreen.kt new file mode 100644 index 000000000..0c3951f50 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyScreen.kt @@ -0,0 +1,116 @@ +package com.wap.wapp.feature.survey + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.CircleLoader +import com.wap.designsystem.component.WappLeftMainTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.model.user.UserRole +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun SurveyScreen( + viewModel: SurveyViewModel, + navigateToSignIn: () -> Unit, + navigateToSurveyAnswer: (String) -> Unit, + navigateToSurveyCheck: () -> Unit, +) { + val context = LocalContext.current + val surveyFormListUiState = viewModel.surveyFormListUiState.collectAsStateWithLifecycle().value + val snackBarHostState = remember { SnackbarHostState() } + var isGuest by rememberSaveable { mutableStateOf(false) } + var isManager by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(true) { + viewModel.surveyEvent.collectLatest { + when (it) { + is SurveyViewModel.SurveyUiEvent.Failure -> { + snackBarHostState.showSnackbar(it.throwable.toSupportingText()) + } + + is SurveyViewModel.SurveyUiEvent.AlreadySubmitted -> { + snackBarHostState.showSnackbar( + context.getString(R.string.alreay_submitted_description), + ) + } + + is SurveyViewModel.SurveyUiEvent.NotSubmitted -> { + navigateToSurveyAnswer(it.surveyFormId) + } + } + } + } + + LaunchedEffect(true) { + viewModel.userRoleUiState.collectLatest { userRoleUiState -> + when (userRoleUiState) { + is SurveyViewModel.UserRoleUiState.Init -> {} + is SurveyViewModel.UserRoleUiState.Success -> { + when (userRoleUiState.userRole) { + UserRole.GUEST -> { + isGuest = true + } + + // ๋น„ํšŒ์›์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ๋ชฉ๋ก ํ˜ธ์ถœ + UserRole.MEMBER -> viewModel.getSurveyFormList() + + UserRole.MANAGER -> { + viewModel.getSurveyFormList() + isManager = true + } + } + } + } + } + } + + if (isGuest) { + SurveyGuestScreen(onButtonClicked = navigateToSignIn) + return + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = WappTheme.colors.backgroundBlack, + snackbarHost = { SnackbarHost(snackBarHostState) }, + topBar = { + WappLeftMainTopBar( + titleRes = R.string.survey, + contentRes = R.string.survey_content, + ) + }, + contentWindowInsets = WindowInsets(0.dp), + ) { paddingValues -> + when (surveyFormListUiState) { + is SurveyViewModel.SurveyFormListUiState.Init -> {} + + is SurveyViewModel.SurveyFormListUiState.Loading -> + CircleLoader(modifier = Modifier.fillMaxSize()) + + is SurveyViewModel.SurveyFormListUiState.Success -> { + SurveyContent( + surveyFormList = surveyFormListUiState.surveyFormList, + isManager = isManager, + paddingValues = paddingValues, + selectedSurveyForm = viewModel::isSubmittedSurvey, + onSurveyCheckButtonClicked = navigateToSurveyCheck, + ) + } + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyViewModel.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyViewModel.kt new file mode 100644 index 000000000..29b0b6bc9 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/SurveyViewModel.kt @@ -0,0 +1,95 @@ +package com.wap.wapp.feature.survey + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.survey.GetSurveyFormListUseCase +import com.wap.wapp.core.domain.usecase.survey.IsSubmittedSurveyUseCase +import com.wap.wapp.core.domain.usecase.user.GetUserRoleUseCase +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.core.model.user.UserRole +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SurveyViewModel @Inject constructor( + private val getUserRoleUseCase: GetUserRoleUseCase, + private val getSurveyFormListUseCase: GetSurveyFormListUseCase, + private val isSubmittedSurveyUseCase: IsSubmittedSurveyUseCase, +) : ViewModel() { + private val _surveyFormListUiState: MutableStateFlow = + MutableStateFlow(SurveyFormListUiState.Init) + val surveyFormListUiState = _surveyFormListUiState.asStateFlow() + + private val _surveyEvent: MutableSharedFlow = MutableSharedFlow() + val surveyEvent = _surveyEvent.asSharedFlow() + + private val _userRoleUiState: MutableStateFlow = + MutableStateFlow(UserRoleUiState.Init) + val userRoleUiState = _userRoleUiState.asStateFlow() + + init { + getUserRole() + } + + private fun getUserRole() = viewModelScope.launch { + getUserRoleUseCase() + .onSuccess { userRole -> + _userRoleUiState.value = UserRoleUiState.Success(userRole) + } + .onFailure { throwable -> + _surveyEvent.emit(SurveyUiEvent.Failure(throwable)) + } + } + + fun getSurveyFormList() = viewModelScope.launch { + _surveyFormListUiState.value = SurveyFormListUiState.Loading + + getSurveyFormListUseCase() + .onSuccess { surveyFormList -> + val filteredSurveyFormList = surveyFormList.filter { survey -> + survey.isBeforeDeadline() + } + _surveyFormListUiState.value = + SurveyFormListUiState.Success(filteredSurveyFormList) + } + .onFailure { throwable -> + _surveyEvent.emit(SurveyUiEvent.Failure(throwable)) + } + } + + fun isSubmittedSurvey(surveyFormId: String) = viewModelScope.launch { + isSubmittedSurveyUseCase(surveyFormId) + .onSuccess { isSubmittedSurvey -> + if (isSubmittedSurvey) { + _surveyEvent.emit(SurveyUiEvent.AlreadySubmitted) + } else { + _surveyEvent.emit(SurveyUiEvent.NotSubmitted(surveyFormId)) + } + } + .onFailure { throwable -> + _surveyEvent.emit(SurveyUiEvent.Failure(throwable)) + } + } + + sealed class UserRoleUiState { + data object Init : UserRoleUiState() + data class Success(val userRole: UserRole) : UserRoleUiState() + } + + sealed class SurveyFormListUiState { + data object Init : SurveyFormListUiState() + data object Loading : SurveyFormListUiState() + data class Success(val surveyFormList: List) : SurveyFormListUiState() + } + + sealed class SurveyUiEvent { + data class Failure(val throwable: Throwable) : SurveyUiEvent() + data object AlreadySubmitted : SurveyUiEvent() + data class NotSubmitted(val surveyFormId: String) : SurveyUiEvent() + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/ObjectiveSurveyForm.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/ObjectiveSurveyForm.kt new file mode 100644 index 000000000..8a1f03a2b --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/ObjectiveSurveyForm.kt @@ -0,0 +1,110 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.compose.foundation.BorderStroke +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.wapp.core.model.survey.Rating +import com.wap.wapp.core.model.survey.toDescription + +@Composable +internal fun ObjectiveSurveyForm( + questionTitle: String, + answer: Rating, + onAnswerSelected: (Rating) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(40.dp), + ) { + Text( + text = questionTitle, + style = WappTheme.typography.titleRegular, + color = WappTheme.colors.white, + fontSize = 22.sp, + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ObjectiveAnswerCard( + rating = Rating.GOOD, + selectedRating = answer, + onCardSelected = onAnswerSelected, + ) + + ObjectiveAnswerCard( + rating = Rating.MEDIOCRE, + selectedRating = answer, + onCardSelected = onAnswerSelected, + ) + + ObjectiveAnswerCard( + rating = Rating.BAD, + selectedRating = answer, + onCardSelected = onAnswerSelected, + ) + } + } +} + +@Composable +private fun ObjectiveAnswerCard( + rating: Rating, + selectedRating: Rating, + onCardSelected: (Rating) -> Unit, +) { + Card( + colors = CardDefaults.cardColors(containerColor = WappTheme.colors.black25), + modifier = Modifier + .fillMaxWidth() + .clickable { onCardSelected(rating) }, + border = BorderStroke( + color = if (rating == selectedRating) { + WappTheme.colors.yellow34 + } else { + WappTheme.colors.black25 + }, + width = 1.dp, + ), + ) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + ) { + Text( + modifier = Modifier.weight(1f), + text = rating.toDescription().title, + color = WappTheme.colors.white, + style = WappTheme.typography.contentBold, + textAlign = TextAlign.Center, + ) + + Text( + text = rating.toDescription().content, + color = WappTheme.colors.white, + style = WappTheme.typography.labelRegular, + modifier = Modifier.weight(4f), + textAlign = TextAlign.End, + ) + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SubjectiveSurveyForm.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SubjectiveSurveyForm.kt new file mode 100644 index 000000000..d01e0fe6c --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SubjectiveSurveyForm.kt @@ -0,0 +1,76 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappRoundedTextField +import com.wap.wapp.feature.survey.R + +@Composable +internal fun SubjectiveSurveyForm( + questionTitle: String, + answer: String, + onAnswerChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = questionTitle, + style = WappTheme.typography.titleMedium, + color = WappTheme.colors.white, + fontSize = 22.sp, + modifier = Modifier.padding(bottom = 40.dp), + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + WappRoundedTextField( + value = answer, + onValueChange = onAnswerChanged, + placeholder = R.string.subjective_answer_hint, + modifier = Modifier + .height(200.dp) + .fillMaxWidth(), + ) + + val textCount = answer.length + TextCounter(textCount = textCount) + } + } +} + +@Composable +private fun TextCounter( + textCount: Int, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = textCount.toString(), + color = WappTheme.colors.yellow34, + style = WappTheme.typography.labelMedium, + ) + + Text( + text = stringResource(R.string.text_counter_content), + color = WappTheme.colors.white, + style = WappTheme.typography.labelMedium, + ) + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerForm.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerForm.kt new file mode 100644 index 000000000..d9f14ef81 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerForm.kt @@ -0,0 +1,121 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.modifier.addFocusCleaner +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.core.model.survey.Rating +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.feature.survey.R + +@Composable +internal fun SurveyAnswerForm( + surveyForm: SurveyForm, + modifier: Modifier, + questionNumber: Int, + subjectiveAnswer: String, + objectiveAnswer: Rating, + onSubjectiveAnswerChanged: (String) -> Unit, + onObjectiveAnswerSelected: (Rating) -> Unit, + onNextQuestionButtonClicked: () -> Unit, + onPreviousQuestionButtonClicked: () -> Unit, +) { + val focusManager = LocalFocusManager.current + + Column( + modifier = modifier + .fillMaxSize() + .addFocusCleaner(focusManager), + ) { + val surveyQuestion = surveyForm.surveyQuestionList[questionNumber] + val lastQuestionNumber = surveyForm.surveyQuestionList.lastIndex + + SurveyAnswerStateIndicator( + index = questionNumber + 1, + size = lastQuestionNumber + 1, + ) + + AnimatedContent( + targetState = questionNumber, + transitionSpec = { + if (targetState > initialState) { + slideInHorizontally(initialOffsetX = { it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() + } else { + slideInHorizontally(initialOffsetX = { -it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { it }) + fadeOut() + } + }, + label = stringResource(R.string.survey_answer_form_animated_content_label), + ) { questionNumber -> + Column { + when (surveyForm.surveyQuestionList[questionNumber].questionType) { + QuestionType.SUBJECTIVE -> { + SubjectiveSurveyForm( + questionTitle = surveyQuestion.questionTitle, + answer = subjectiveAnswer, + onAnswerChanged = onSubjectiveAnswerChanged, + modifier = Modifier.weight(1f), + ) + } + + QuestionType.OBJECTIVE -> { + ObjectiveSurveyForm( + questionTitle = surveyQuestion.questionTitle, + answer = objectiveAnswer, + onAnswerSelected = onObjectiveAnswerSelected, + modifier = Modifier.weight(1f), + ) + } + } + + val isGreaterThanFirstQuestion = questionNumber > 0 + val isLastQuestion = questionNumber == lastQuestionNumber // ๋งˆ์ง€๋ง‰ ์‘๋‹ต์ผ ๊ฒฝ์šฐ, ์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + WappButton( + textRes = R.string.previous, + onClick = onPreviousQuestionButtonClicked, + isEnabled = isGreaterThanFirstQuestion, + modifier = Modifier.weight(1f), + ) + + WappButton( + textRes = if (isLastQuestion) R.string.submit else R.string.next, + onClick = { onNextQuestionButtonClicked() }, + isEnabled = checkQuestionTypeAndSubjectiveAnswer( + questionType = surveyQuestion.questionType, + subjectiveAnswer = subjectiveAnswer, + ), + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +private fun checkQuestionTypeAndSubjectiveAnswer( + questionType: QuestionType, + subjectiveAnswer: String, +): Boolean { + if (questionType == QuestionType.OBJECTIVE) { + return true + } + return subjectiveAnswer.length >= MINIMUM_ANSWER_LENGTH +} + +private const val MINIMUM_ANSWER_LENGTH = 8 diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerScreen.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerScreen.kt new file mode 100644 index 000000000..004184fa9 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerScreen.kt @@ -0,0 +1,184 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappSubTopBar +import com.wap.wapp.core.commmon.extensions.toSupportingText +import com.wap.wapp.core.designresource.R.drawable +import com.wap.wapp.core.model.survey.Rating +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.feature.survey.R +import com.wap.wapp.feature.survey.answer.SurveyAnswerViewModel.SurveyFormUiState +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun SurveyAnswerScreen( + viewModel: SurveyAnswerViewModel, + navigateToSurvey: () -> Unit, + surveyFormId: String, +) { + val surveyAnswerState = viewModel.surveyAnswerState.collectAsStateWithLifecycle().value + val surveyFormUiState = viewModel.surveyFormUiState.collectAsStateWithLifecycle().value + val surveyAnswerList = viewModel.surveyAnswerList.collectAsStateWithLifecycle().value + val eventName = viewModel.eventName.collectAsStateWithLifecycle().value + val questionNumber = viewModel.questionNumber.collectAsStateWithLifecycle().value + val subjectiveAnswer = viewModel.subjectiveAnswer.collectAsStateWithLifecycle().value + val objectiveAnswer = viewModel.objectiveAnswer.collectAsStateWithLifecycle().value + val snackBarHostState = remember { SnackbarHostState() } + + LaunchedEffect(true) { + viewModel.getSurveyForm(surveyFormId) + + viewModel.surveyAnswerEvent.collectLatest { + when (it) { + is SurveyAnswerViewModel.SurveyAnswerUiEvent.SubmitSuccess -> + navigateToSurvey() + + is SurveyAnswerViewModel.SurveyAnswerUiEvent.Failure -> + snackBarHostState.showSnackbar(it.throwable.toSupportingText()) + } + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + containerColor = WappTheme.colors.backgroundBlack, + contentWindowInsets = WindowInsets(0.dp), + topBar = { + WappSubTopBar( + titleRes = R.string.survey_answer, + showLeftButton = true, + modifier = Modifier.padding(top = 16.dp), // ํ•˜๋‹จ์€ Content Padding์— ์˜์กด + leftButtonDrawableRes = drawable.ic_close, + onClickLeftButton = { + when (surveyAnswerState) { + SurveyAnswerState.SURVEY_OVERVIEW -> navigateToSurvey() + SurveyAnswerState.SURVEY_ANSWER -> viewModel.setSurveyAnswerState( + SurveyAnswerState.SURVEY_OVERVIEW, + ) + } + }, + ) + }, + ) { paddingValues -> + when (surveyFormUiState) { + is SurveyFormUiState.Init -> {} + + is SurveyFormUiState.Success -> { + SurveyAnswerContent( + surveyAnswerState = surveyAnswerState, + surveyForm = surveyFormUiState.surveyForm, + eventName = eventName, + questionNumber = questionNumber, + subjectiveAnswer = subjectiveAnswer, + objectiveAnswer = objectiveAnswer, + onSubjectiveAnswerChanged = viewModel::setSubjectiveAnswer, + onObjectiveAnswerSelected = viewModel::setObjectiveAnswer, + onStartSurveyButtonClicked = { + viewModel.setSurveyAnswerState(SurveyAnswerState.SURVEY_ANSWER) + }, + onNextQuestionButtonClicked = { + if (questionNumber < surveyAnswerList.size) { // ์ž‘์„ฑํ•œ ๋‹ต๋ณ€์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ + viewModel.editSurveyAnswer() + } else { + viewModel.addSurveyAnswer() + } + + val lastQuestionNumber = + surveyFormUiState.surveyForm.surveyQuestionList.lastIndex + if (questionNumber == lastQuestionNumber) { // ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์— ๋‹ต๋ณ€์„ ํ•œ ๊ฒฝ์šฐ + viewModel.submitSurvey() + return@SurveyAnswerContent + } + + viewModel.setNextQuestionAndAnswer() // ๋‹ค์Œ ์งˆ๋ฌธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + }, + onPreviousQuestionButtonClicked = { + // ์‘๋‹ต์˜ ๊ฐฏ์ˆ˜๊ฐ€ ์งˆ๋ฌธ์˜ ๊ฐฏ์ˆ˜๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ + if (questionNumber >= surveyAnswerList.size) { + viewModel.addSurveyAnswer() + } else { + viewModel.editSurveyAnswer() + } + viewModel.setPreviousQuestionAndAnswer() + }, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(top = 16.dp, bottom = 20.dp, start = 20.dp, end = 20.dp), + ) + } + } + } +} + +@Composable +private fun SurveyAnswerContent( + surveyAnswerState: SurveyAnswerState, + surveyForm: SurveyForm, + eventName: String, + questionNumber: Int, // ์ „์ฒด ์งˆ๋ฌธ ์ค‘, ํ˜„์žฌ ์งˆ๋ฌธ ๋ฒˆํ˜ธ + subjectiveAnswer: String, + objectiveAnswer: Rating, + onSubjectiveAnswerChanged: (String) -> Unit, + onObjectiveAnswerSelected: (Rating) -> Unit, + onStartSurveyButtonClicked: () -> Unit, + onNextQuestionButtonClicked: () -> Unit, + onPreviousQuestionButtonClicked: () -> Unit, + modifier: Modifier, +) { + AnimatedContent( + targetState = surveyAnswerState, + transitionSpec = { + if (targetState.ordinal > initialState.ordinal) { + slideInHorizontally(initialOffsetX = { it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() + } else { + slideInHorizontally(initialOffsetX = { -it }) + fadeIn() togetherWith + slideOutHorizontally(targetOffsetX = { it }) + fadeOut() + } + }, + ) { answerState -> + when (answerState) { + SurveyAnswerState.SURVEY_OVERVIEW -> { + SurveyOverview( + surveyForm = surveyForm, + modifier = modifier.padding(top = 16.dp), + eventName = eventName, + onStartSurveyButtonClicked = onStartSurveyButtonClicked, + ) + } + + SurveyAnswerState.SURVEY_ANSWER -> { + SurveyAnswerForm( + surveyForm = surveyForm, + modifier = modifier, + questionNumber = questionNumber, + subjectiveAnswer = subjectiveAnswer, + objectiveAnswer = objectiveAnswer, + onSubjectiveAnswerChanged = onSubjectiveAnswerChanged, + onObjectiveAnswerSelected = onObjectiveAnswerSelected, + onNextQuestionButtonClicked = onNextQuestionButtonClicked, + onPreviousQuestionButtonClicked = onPreviousQuestionButtonClicked, + ) + } + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerState.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerState.kt new file mode 100644 index 000000000..a2f363748 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerState.kt @@ -0,0 +1,5 @@ +package com.wap.wapp.feature.survey.answer + +enum class SurveyAnswerState { + SURVEY_OVERVIEW, SURVEY_ANSWER +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerStateIndicator.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerStateIndicator.kt new file mode 100644 index 000000000..e00d723c3 --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerStateIndicator.kt @@ -0,0 +1,73 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme + +// TODO ๊ณต์šฉ ๋ชจ๋“ˆ๋กœ ์˜ฎ๊ธธ ์˜ˆ์ • +@Composable +internal fun SurveyAnswerStateIndicator(index: Int, size: Int) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .wrapContentSize() + .padding(bottom = 40.dp), + ) { + SurveyAnswerStateProgressBar(index = index, size = size) + SurveyAnswerStateText(index = index, size = size) + } +} + +@Composable +private fun SurveyAnswerStateText(index: Int, size: Int) { + Row { + Text( + text = index.toString(), + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.yellow34, + ) + Text( + text = "/ $size", + style = WappTheme.typography.contentMedium, + color = WappTheme.colors.white, + ) + } +} + +@Composable +private fun SurveyAnswerStateProgressBar(index: Int, size: Int) { + val progress by animateFloatAsState( + targetValue = index.toFloat() / size.toFloat(), + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioMediumBouncy, + ), + ) + + LinearProgressIndicator( + color = WappTheme.colors.yellow34, + trackColor = WappTheme.colors.white, + progress = progress, + strokeCap = StrokeCap.Round, + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + ) +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerViewModel.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerViewModel.kt new file mode 100644 index 000000000..a96fedcda --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyAnswerViewModel.kt @@ -0,0 +1,204 @@ +package com.wap.wapp.feature.survey.answer + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wap.wapp.core.domain.usecase.event.GetEventUseCase +import com.wap.wapp.core.domain.usecase.survey.GetSurveyFormUseCase +import com.wap.wapp.core.domain.usecase.survey.PostSurveyUseCase +import com.wap.wapp.core.model.survey.QuestionType +import com.wap.wapp.core.model.survey.Rating +import com.wap.wapp.core.model.survey.SurveyAnswer +import com.wap.wapp.core.model.survey.SurveyForm +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SurveyAnswerViewModel @Inject constructor( + private val getEventUseCase: GetEventUseCase, + private val getSurveyFormUseCase: GetSurveyFormUseCase, + private val postSurveyUseCase: PostSurveyUseCase, +) : ViewModel() { + private val _surveyAnswerState: MutableStateFlow = + MutableStateFlow(SurveyAnswerState.SURVEY_OVERVIEW) + val surveyAnswerState = _surveyAnswerState.asStateFlow() + + // ์ถœ๋ ฅ SurveyForm State + private val _surveyFormUiState: MutableStateFlow = + MutableStateFlow(SurveyFormUiState.Init) + val surveyFormUiState = _surveyFormUiState.asStateFlow() + + // ์‚ฌ์šฉ์ž ์ž…๋ ฅ SurveyForm State + private val _surveyForm: MutableStateFlow = MutableStateFlow(SurveyForm()) + + private val _eventName: MutableStateFlow = MutableStateFlow("") + val eventName = _eventName.asStateFlow() + + private val _surveyAnswerEvent: MutableSharedFlow = MutableSharedFlow() + val surveyAnswerEvent = _surveyAnswerEvent.asSharedFlow() + + private val _questionNumber: MutableStateFlow = MutableStateFlow(0) + val questionNumber = _questionNumber.asStateFlow() + + // ์„ค๋ฌธ ์‘๋‹ต ๋ฆฌ์ŠคํŠธ + private val _surveyAnswerList: MutableStateFlow> = + MutableStateFlow(mutableListOf()) + val surveyAnswerList = _surveyAnswerList.asStateFlow() + + private val _subjectiveAnswer: MutableStateFlow = MutableStateFlow("") + val subjectiveAnswer = _subjectiveAnswer.asStateFlow() + + private val _objectiveAnswer: MutableStateFlow = MutableStateFlow(Rating.GOOD) + val objectiveAnswer = _objectiveAnswer.asStateFlow() + + fun getSurveyForm(surveyFormId: String) { + viewModelScope.launch { + getSurveyFormUseCase(surveyFormId = surveyFormId) + .onSuccess { surveyForm -> + _surveyFormUiState.value = SurveyFormUiState.Success(surveyForm) + _surveyForm.value = surveyForm + getEvent() + } + .onFailure { throwable -> + _surveyAnswerEvent.emit(SurveyAnswerUiEvent.Failure(throwable)) + } + } + } + + private fun getEvent() = viewModelScope.launch { + getEventUseCase(eventId = _surveyForm.value.eventId) + .onSuccess { event -> + _eventName.value = event.title + } + .onFailure { throwable -> + _surveyAnswerEvent.emit(SurveyAnswerUiEvent.Failure(throwable)) + } + } + + fun addSurveyAnswer() { + val questionNumber = _questionNumber.value + val surveyQuestion = _surveyForm.value.surveyQuestionList[questionNumber] + + when (surveyQuestion.questionType) { // ์ƒˆ๋กœ์šด ์งˆ๋ฌธ์— ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ + QuestionType.SUBJECTIVE -> { + _surveyAnswerList.value.add( // ํ˜„์žฌ ์งˆ๋ฌธ ๋‹ต๋ณ€ ๋ฆฌ์ŠคํŠธ์— ์ €์žฅ + SurveyAnswer( + questionType = surveyQuestion.questionType, + questionTitle = surveyQuestion.questionTitle, + questionAnswer = _subjectiveAnswer.value, + ), + ) + clearSubjectiveAnswer() // ์งˆ๋ฌธ ์ƒํƒœ ์ดˆ๊ธฐํ™” + } + + QuestionType.OBJECTIVE -> { + _surveyAnswerList.value.add( + SurveyAnswer( + questionType = surveyQuestion.questionType, + questionTitle = surveyQuestion.questionTitle, + questionAnswer = _objectiveAnswer.value.toString(), + ), + ) + clearObjectiveAnswer() + } + } + } + + fun editSurveyAnswer() { + val questionNumber = _questionNumber.value + val surveyQuestion = _surveyForm.value.surveyQuestionList[questionNumber] + val surveyAnswerList = _surveyAnswerList.value + + when (surveyQuestion.questionType) { // ์ƒˆ๋กœ์šด ์งˆ๋ฌธ์— ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ + QuestionType.SUBJECTIVE -> { + surveyAnswerList[questionNumber] = SurveyAnswer( + questionType = surveyQuestion.questionType, + questionTitle = surveyQuestion.questionTitle, + questionAnswer = _subjectiveAnswer.value, + ) + clearSubjectiveAnswer() // ์งˆ๋ฌธ ์ƒํƒœ ์ดˆ๊ธฐํ™” + } + + QuestionType.OBJECTIVE -> { + surveyAnswerList[questionNumber] = SurveyAnswer( + questionType = surveyQuestion.questionType, + questionTitle = surveyQuestion.questionTitle, + questionAnswer = _objectiveAnswer.value.toString(), + ) + clearObjectiveAnswer() + } + } + } + + fun submitSurvey() { + val surveyForm = _surveyForm.value + val surveyAnswerList = _surveyAnswerList.value + + viewModelScope.launch { + postSurveyUseCase( + surveyFormId = surveyForm.surveyFormId, + eventId = surveyForm.eventId, + title = surveyForm.title, + content = surveyForm.content, + surveyAnswerList = surveyAnswerList, + ).onSuccess { + _surveyAnswerEvent.emit(SurveyAnswerUiEvent.SubmitSuccess) + }.onFailure { throwable -> + _surveyAnswerEvent.emit(SurveyAnswerUiEvent.Failure(throwable)) + } + } + } + + fun setSubjectiveAnswer(answer: String) { _subjectiveAnswer.value = answer } + + fun setObjectiveAnswer(answer: Rating) { _objectiveAnswer.value = answer } + + fun setNextQuestionAndAnswer() { + _questionNumber.value += 1 + if (_questionNumber.value < _surveyAnswerList.value.size) { // ๋‹ค์Œ ์งˆ๋ฌธ์ด ์•„๋ฏธ ์ž‘์„ฑ๋œ ์งˆ๋ฌธ์ธ ๊ฒฝ์šฐ + setSurveyAnswer() + } + } + + fun setPreviousQuestionAndAnswer() { + _questionNumber.value -= 1 + setSurveyAnswer() + } + + private fun setSurveyAnswer() { + val surveyAnswer = _surveyAnswerList.value[_questionNumber.value] + when (surveyAnswer.questionType) { + QuestionType.SUBJECTIVE -> { + setSubjectiveAnswer(surveyAnswer.questionAnswer) + } + + QuestionType.OBJECTIVE -> { + when (surveyAnswer.questionAnswer) { + "GOOD" -> setObjectiveAnswer(Rating.GOOD) + "MEDIOCRE" -> setObjectiveAnswer(Rating.MEDIOCRE) + "BAD" -> setObjectiveAnswer(Rating.BAD) + } + } + } + } + + fun setSurveyAnswerState(state: SurveyAnswerState) { _surveyAnswerState.value = state } + + private fun clearSubjectiveAnswer() { _subjectiveAnswer.value = "" } + + private fun clearObjectiveAnswer() { _objectiveAnswer.value = Rating.GOOD } + + sealed class SurveyFormUiState { + data object Init : SurveyFormUiState() + data class Success(val surveyForm: SurveyForm) : SurveyFormUiState() + } + + sealed class SurveyAnswerUiEvent { + data class Failure(val throwable: Throwable) : SurveyAnswerUiEvent() + data object SubmitSuccess : SurveyAnswerUiEvent() + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyOverview.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyOverview.kt new file mode 100644 index 000000000..6019f92bc --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/answer/SurveyOverview.kt @@ -0,0 +1,96 @@ +package com.wap.wapp.feature.survey.answer + +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.fillMaxWidth +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.stringResource +import androidx.compose.ui.unit.dp +import com.wap.designsystem.WappTheme +import com.wap.designsystem.component.WappButton +import com.wap.designsystem.component.WappTitle +import com.wap.wapp.core.commmon.util.DateUtil +import com.wap.wapp.core.model.survey.SurveyForm +import com.wap.wapp.feature.survey.R + +@Composable +internal fun SurveyOverview( + surveyForm: SurveyForm, + modifier: Modifier, + eventName: String, + onStartSurveyButtonClicked: () -> Unit, +) { + Column(modifier = modifier) { + WappTitle( + title = surveyForm.title, + content = surveyForm.content, + ) + + Column( + modifier = Modifier + .padding(top = 40.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SurveyOverviewText(category = stringResource(R.string.event), content = eventName) + + SurveyOverviewText( + category = stringResource(R.string.deadline), + content = surveyForm.deadline.format(DateUtil.yyyyMMddFormatter), + ) + + SurveyOverviewText( + category = stringResource(R.string.questionTotalNumber), + content = surveyForm.surveyQuestionList.size.toString(), + ) + } + + WappButton( + textRes = R.string.start_survey, + onClick = onStartSurveyButtonClicked, + ) + } +} + +@Composable +private fun SurveyOverviewText(category: String, content: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = category, + color = WappTheme.colors.white, + style = WappTheme.typography.titleBold, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Box( + modifier = Modifier + .width(200.dp) + .clip(RoundedCornerShape(8.dp)) + .background(WappTheme.colors.black25), + ) { + Text( + text = content, + color = WappTheme.colors.white, + style = WappTheme.typography.contentMedium, + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = 10.dp, horizontal = 10.dp), + ) + } + } +} diff --git a/feature/survey/src/main/java/com/wap/wapp/feature/survey/navigation/SurveyNavigation.kt b/feature/survey/src/main/java/com/wap/wapp/feature/survey/navigation/SurveyNavigation.kt new file mode 100644 index 000000000..66e6c64cd --- /dev/null +++ b/feature/survey/src/main/java/com/wap/wapp/feature/survey/navigation/SurveyNavigation.kt @@ -0,0 +1,63 @@ +package com.wap.wapp.feature.survey.navigation + +import androidx.hilt.navigation.compose.hiltViewModel +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 androidx.navigation.navOptions +import com.wap.wapp.feature.survey.SurveyScreen +import com.wap.wapp.feature.survey.answer.SurveyAnswerScreen + +fun NavController.navigateToSurvey(navOptions: NavOptions? = navOptions {}) { + this.navigate(SurveyRoute.route, navOptions) +} + +fun NavController.navigateToSurveyAnswer( + surveyFormId: String, + navOptions: NavOptions? = navOptions {}, +) { + navigate(SurveyRoute.answerRoute(surveyFormId), navOptions) +} + +fun NavGraphBuilder.surveyNavGraph( + navigateToSurveyAnswer: (String) -> Unit, + navigateToSurvey: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSurveyCheck: () -> Unit, +) { + composable(route = SurveyRoute.route) { + SurveyScreen( + viewModel = hiltViewModel(), + navigateToSignIn = navigateToSignIn, + navigateToSurveyAnswer = { surveyFormId -> + navigateToSurveyAnswer(surveyFormId) + }, + navigateToSurveyCheck = navigateToSurveyCheck, + ) + } + + composable( + route = SurveyRoute.answerRoute("{id}"), + arguments = listOf( + navArgument("id") { + type = NavType.StringType + }, + ), + ) { navBackStackEntry -> + val surveyFormId = navBackStackEntry.arguments?.getString("id") ?: "" + SurveyAnswerScreen( + viewModel = hiltViewModel(), + navigateToSurvey = navigateToSurvey, + surveyFormId = surveyFormId, + ) + } +} + +object SurveyRoute { + const val route: String = "survey" + + fun answerRoute(surveyFormId: String): String = "$route/$surveyFormId" +} diff --git a/feature/survey/src/main/res/values/strings.xml b/feature/survey/src/main/res/values/strings.xml new file mode 100644 index 000000000..8617a5d6c --- /dev/null +++ b/feature/survey/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + Hello blank fragment + ์„ค๋ฌธ + ์ œ์ถœ + ๋‹ค์Œ + ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์‘๋‹ต์„ ์ž‘์„ฑํ•˜์„ธ์š” + ์„ค๋ฌธ ์ž‘์„ฑ + ์ฐธ๊ฐ€ํ•œ ํ–‰์‚ฌ์— ๋Œ€ํ•ด ์„ค๋ฌธ์— ์‘๋‹ตํ•˜์„ธ์š”! + ์„ค๋ฌธ ์‘๋‹ต + /8์ž ์ด์ƒ + ์ด๋ฏธ ์ž‘์„ฑํ•œ ์„ค๋ฌธ์ž…๋‹ˆ๋‹ค. + ๋กœ๊ทธ์ธ ํ•˜๋Ÿฌ๊ฐ€๊ธฐ + ์•— ํšŒ์›์ด ์•„๋‹ˆ์‹œ๋„ค์š” ! + ํšŒ์›๋งŒ ์„ค๋ฌธ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”. + ๋น„ํšŒ์›์œผ๋กœ ๋‘˜๋Ÿฌ๋ณด๊ธฐ + ์ž‘์„ฑ๋œ ์„ค๋ฌธ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ + ์ž‘์„ฑ๋œ ์„ค๋ฌธ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ ์•„์ด์ฝ˜ + ์„ค๋ฌธ ์‹œ์ž‘ํ•˜๊ธฐ + ์ด์ „ + ํ–‰์‚ฌ + ๋งˆ๊ฐ์ผ + ์งˆ๋ฌธ ์ˆ˜ + SurveyAnswerForm Navigation + diff --git a/feature/survey/src/test/java/com/wap/wapp/feature/survey/ExampleUnitTest.kt b/feature/survey/src/test/java/com/wap/wapp/feature/survey/ExampleUnitTest.kt new file mode 100644 index 000000000..de8c8f1e0 --- /dev/null +++ b/feature/survey/src/test/java/com/wap/wapp/feature/survey/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.wap.wapp.feature.survey + +import org.junit.Assert.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/gradle.properties b/gradle.properties new file mode 100644 index 000000000..0e41e6815 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# 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. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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 +android.enableJetifier=false +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.configureondemand=true +org.gradle.configuration-cache=true +org.gradle.parallel=true +kotlin.incremental=true + +# Temporary workaround for Kotlin Compiler OutOfMemoryError -> https://jb.gg/intellij-platform-kotlin-oom +kotlin.incremental.useClasspathSnapshot=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..b3ff0eac0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,134 @@ +[versions] +compileSdk = "34" +minSdk = "26" +targetSdk = "34" +versionName = "1.0.0" +versionCode = "1" + +gradleplugin = "8.1.2" + +kotlin = "1.9.10" +ksp = "1.9.10-1.0.13" + +androidx-core = "1.12.0" +androidx-appcompat = "1.6.1" +androidx-fragment = "1.6.1" +androidx-activity = "1.8.2" +androidx-lifecycle = "2.6.2" +androidx-compose-hilt = "1.0.0" +androidx-contstraintlayout = "2.1.4" +androidx-test-junit = "1.1.5" +androidx-test-espresso = "3.5.1" +androidx-navigation = "2.7.4" + +compose = "1.5.2" +compose-material = "1.5.2" +compose-foundation = "1.5.2" +compose-icon = "1.5.2" +compose-compiler = "1.5.3" +compose-material3-windowSizeClass = "1.1.2" +compose-material3 = "1.1.2" +compose-lifecycle = "2.6.0-beta01" + +google-firebase = "32.3.1" +google-services-plugin = "4.4.0" +google-crashlytics-plguin = "2.9.9" + +junit = "4.13.2" +material = "1.9.0" +ktlint-gradle = "11.3.1" + +hilt = "2.48" + +lottie-compose = "6.2.0" + + +[libraries] +android-build = { module = "com.android.tools.build:gradle", version.ref = "gradleplugin" } +kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +hilt-gradle = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } +androidx-navigation-safeargs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "androidx-navigation" } + +kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } + +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-contstraintlayout" } +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } +androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } +androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "androidx-navigation" } +androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "androidx-navigation" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } + +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } +compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-icon" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } +compose-material3-windowSizeClass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "compose-material3-windowSizeClass" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +compose-hilt = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-compose-hilt" } +compose-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } + +google-services-gradle = { group = "com.google.gms", name = "google-services", version.ref = "google-services-plugin" } +firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "google-crashlytics-plguin" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "google-firebase" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +firebase-auth = { module = "com.google.firebase:firebase-auth-ktx" } +firebase-firestore = { module = "com.google.firebase:firebase-firestore-ktx" } + +hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-ksp = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-plugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } + +junit = { module = "junit:junit", version.ref = "junit" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } +androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie-compose" } + +[bundles] +androidx = [ + "androidx-appcompat", + "androidx-constraintlayout", + "androidx-core", + "androidx-lifecycle-viewmodel", + "androidx-fragment", + "androidx-lifecycle-runtime", +] + +compose = [ + "compose-ui", + "compose-runtime", + "compose-foundation", + "compose-ui-tooling-preview", + "compose-material", + "compose-material-icons", + "compose-material3", + "compose-material3-windowSizeClass", + "compose-ui-tooling-preview", + "compose-viewmodel", + "compose-activity", + "compose-hilt", + "compose-lifecycle", +] + +[plugins] +android-application = { id = "com.android.application", version.ref = "gradleplugin" } +android-library = { id = "com.android.library", version.ref = "gradleplugin" } +androidx-navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "androidx-navigation" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +google-services = { id = "com.google.gms.google-services", version.ref = "google-services-plugin" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "google-crashlytics-plguin" } +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } 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..241e81237 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Sep 20 14:24:48 KST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-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/management-event/build/intermediates/ktLint/reporterProviders.bin b/management-event/build/intermediates/ktLint/reporterProviders.bin new file mode 100644 index 000000000..11e8bd367 Binary files /dev/null and b/management-event/build/intermediates/ktLint/reporterProviders.bin differ diff --git a/management-event/build/intermediates/ktLint/reporters.bin b/management-event/build/intermediates/ktLint/reporters.bin new file mode 100644 index 000000000..d5a0d6164 Binary files /dev/null and b/management-event/build/intermediates/ktLint/reporters.bin differ diff --git "a/presentation/WAPP-\354\265\234\354\242\205 \353\260\234\355\221\234.pptx" "b/presentation/WAPP-\354\265\234\354\242\205 \353\260\234\355\221\234.pptx" new file mode 100644 index 000000000..727cc7372 Binary files /dev/null and "b/presentation/WAPP-\354\265\234\354\242\205 \353\260\234\355\221\234.pptx" differ diff --git a/script/pre-commit b/script/pre-commit new file mode 100644 index 000000000..345bf7381 --- /dev/null +++ b/script/pre-commit @@ -0,0 +1,11 @@ +#!/bin/sh +echo "Running git pre-commit hook" + +./gradlew ktlintCheck --daemon + +# $? = ".gradlew ktlintFormat --daemon"์— ๋Œ€ํ•œ return ๊ฐ’ +STATUS=$? + +# ๋ฌธ์ œ์—†์ด ๋๋‚ฌ๋‹ค๋ฉด exit 0, ์•„๋‹ˆ๋ฉด 1 +[ $STATUS -ne 0 ] && exit 1 +exit 0 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..02e0f28ee --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,36 @@ +pluginManagement { + includeBuild("build-logic") + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "WAPP" +include(":app") +include(":core") +include(":core:data") +include(":core:domain") +include(":core:network") +include(":core:designsystem") +include(":core:designresource") +include(":core:common") +include(":core:model") +include(":feature") +include(":feature:auth") +include(":feature:notice") +include(":feature:survey") +include(":feature:profile") +include(":feature:management") +include(":feature:splash") +include(":feature:management-survey") +include(":feature:management-event") +include(":feature:survey-check") +include(":feature:attendance")