diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1d25749a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +max_line_length = 150 +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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/chore-task.yml b/.github/ISSUE_TEMPLATE/chore-task.yml new file mode 100644 index 00000000..bca48f1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/chore-task.yml @@ -0,0 +1,41 @@ +name: Chore Task +description: 개발 환경 설정 작업을 위한 템플릿입니다.(JIRA와 연동됩니다) +title: "chore] " +labels: ["⚙️ chore"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "⚙ 설정 작업 설명" + description: "무엇을 설정/변경하는지 설명해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "🔧 설정 작업 목록" + description: "진행할 설정 작업들을 작성해주세요" + value: | + - [ ] 환경 변수 설정 + - [ ] .. + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "관련 설정 문서, 참고 링크 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/docs-task.yml b/.github/ISSUE_TEMPLATE/docs-task.yml new file mode 100644 index 00000000..224076a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs-task.yml @@ -0,0 +1,42 @@ +name: Docs Task +description: 문서 작성 및 수정을 위한 템플릿입니다.(JIRA와 연동됩니다) +title: "docs] " +labels: ["📃 docs"] + +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "📘 문서 설명" + description: "작성/수정할 문서의 내용을 설명해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "📝 문서 작업 목록" + description: "진행할 문서 작업들을 작성해주세요" + value: | + - [ ] 문서 초안 작성 + - [ ] 코드 예시 추가 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "기존 문서, 규칙, 외부 링크 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature-task.yml b/.github/ISSUE_TEMPLATE/feature-task.yml new file mode 100644 index 00000000..0da7115b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-task.yml @@ -0,0 +1,41 @@ +name: Feature Task +description: 새로운 기능을 개발할 때 사용하는 템플릿입니다.(JIRA와 연동됩니다) +title: "feat] " +labels: ["✨ feat"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "📝 기능 설명" + description: "개발할 기능에 대해 간단히 설명해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "✅ 작업 목록" + description: "수행할 작업을 체크리스트 형식으로 작성해주세요" + value: | + - [ ] Task1 + - [ ] Task2 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "관련 문서, 디자인 링크 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/fix-task.yml b/.github/ISSUE_TEMPLATE/fix-task.yml new file mode 100644 index 00000000..879d2ac5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/fix-task.yml @@ -0,0 +1,42 @@ +name: Fix Task +description: 버그 수정용 이슈 템플릿입니다.(JIRA와 연동됩니다) +title: "fix] " +labels: ["🐞 fix"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "🐞 버그 설명" + description: "어떤 버그인지 명확히 작성해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "🔧 수정할 작업 목록" + description: "수정해야 할 항목들을 체크리스트로 작성해주세요" + value: | + - [ ] 버그 재현 + - [ ] 원인 분석 + - [ ] 수정 및 테스트 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "관련 문서, 스크린샷, 로그 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/refactor-task.yml b/.github/ISSUE_TEMPLATE/refactor-task.yml new file mode 100644 index 00000000..aa7784fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor-task.yml @@ -0,0 +1,41 @@ +name: Refactor Task +description: 리팩토링 작업을 위한 템플릿입니다.(JIRA와 연동됩니다) +title: "refactor] " +labels: ["🔨 refactor"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "♻️ 리팩토링 설명" + description: "리팩토링 대상 및 이유를 설명해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "🧹 리팩토링 작업 목록" + description: "진행할 리팩토링 작업들을 작성해주세요" + value: | + - [ ] 불필요한 코드 제거 + - [ ] 성능 개선 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "관련 문서, 레퍼런스 코드 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/test-task.yml b/.github/ISSUE_TEMPLATE/test-task.yml new file mode 100644 index 00000000..d7a81924 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test-task.yml @@ -0,0 +1,40 @@ +name: Test Task +description: 테스트 코드 작성 및 테스트 환경 구축을 위한 이슈입니다.(JIRA와 연동됩니다) +title: "test] " +labels: ["✅ test"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 상위 작업 (Ticket Number)' + description: '상위 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "✅ 테스트 설명" + description: "어떤 테스트를 수행할지 설명해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "🧪 테스트 작업 목록" + description: "진행할 테스트 항목을 정리해주세요" + value: | + - [ ] 레포지토리 생성 테스트 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "테스트 가이드, 문서 링크 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..a7456636 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ + + +## 🔗 관련 이슈 + +- Close # + +## 📙 작업 설명 + +- + +## 🧪 테스트 내역 (선택) +- [ ] 주요 기능 정상 동작 확인 +- [ ] 브라우저/기기에서 동작 확인 +- [ ] 엣지 케이스 테스트 완료 +- [ ] 기존 기능 영향 없음 + +## 📸 스크린샷 또는 시연 영상 (선택) + +|기능|미리보기|기능|미리보기| +|:--:|:--:|:--:|:--:| +| 기능 설명 || 기능 설명 || + +## 💬 추가 설명 or 리뷰 포인트 (선택) + +- diff --git a/.github/workflows/PR_Label_Assign.yml b/.github/workflows/PR_Label_Assign.yml new file mode 100644 index 00000000..cb3d52bc --- /dev/null +++ b/.github/workflows/PR_Label_Assign.yml @@ -0,0 +1,45 @@ +name: PR Title Labeler + +on: + pull_request_target: + types: [opened, edited, reopened] + +jobs: + label-pr: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Label PR based on title + uses: actions/github-script@v6 + with: + script: | + const prTitle = context.payload.pull_request.title; + + const labelMap = [ + { pattern: /^feat:/i, label: '✨ feat' }, + { pattern: /^fix:/i, label: '🐞 fix' }, + { pattern: /^chore:/i, label: '⚙️ chore' }, + { pattern: /^docs:/i, label: '📃 docs' }, + { pattern: /^refactor:/i, label: '🔨 refactor' }, + { pattern: /^test:/i, label: '✅ test' } + ]; + + const labelsToAdd = labelMap + .filter(entry => entry.pattern.test(prTitle)) + .map(entry => entry.label); + + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labelsToAdd + }); + core.info(`Added labels: ${labelsToAdd.join(', ')}`); + } else { + core.info('No matching labels found for PR title.'); + } diff --git a/.github/workflows/PR_Review_Assign.yml b/.github/workflows/PR_Review_Assign.yml new file mode 100644 index 00000000..224170ca --- /dev/null +++ b/.github/workflows/PR_Review_Assign.yml @@ -0,0 +1,14 @@ +name: Review Assign + +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + assign: + runs-on: ubuntu-latest + steps: + - uses: hkusu/review-assign-action@v1 + with: + assignees: ${{ github.actor }} # assign pull request author + reviewers: seoyoon513, easyhooon diff --git a/.github/workflows/android-cd.yml b/.github/workflows/android-cd.yml new file mode 100644 index 00000000..9ecb0f95 --- /dev/null +++ b/.github/workflows/android-cd.yml @@ -0,0 +1,107 @@ +name: Android CD + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false" + GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true + +on: + pull_request: + branches: + - main + +jobs: + cd-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # 최근 태그를 확인하기 위해 필요 + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: 17 + + - name: Generate reed.jks + run: echo '${{ secrets.REED_JAVA_KEYSTORE }}' | base64 -d > ./reed.jks + + - name: Generate local.properties + run: echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 -d > ./local.properties + + - name: Generate keystore.properties + run: echo '${{ secrets.KEYSTORE_PROPERTIES }}' | base64 -d > ./keystore.properties + + - name: Generate google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > ./app/google-services.json + + - name: Extract Version Name from ApplicationConstants.kt + run: | + set -euo pipefail + VERSION=$(grep "VERSION_NAME" build-logic/src/main/kotlin/com/ninecraft/booket/convention/ApplicationConstants.kt | sed -E 's/.*VERSION_NAME\s*=\s*"([^"]+)".*/\1/') + if [[ -z "$VERSION" ]]; then + echo "Error: ApplicationConstants.kt에서 VERSION_NAME 값을 추출하지 못했습니다." >&2 + exit 1 + fi + echo "version=v${VERSION}" >> "$GITHUB_OUTPUT" + echo "Version extracted from ApplicationConstants.kt: v${VERSION}" + id: extract_version + + - name: Generate Firebase Release Note + id: firebase_release_note + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + # PR_TITLE은 env에서 안전하게 전달됨 + # 가장 최근 태그 찾기 (현재 버전 이전의 태그) + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + # 릴리스 노트 내용 생성 + NOTES="## 🚀 변경사항: ${PR_TITLE}\n\n" + + if [ -n "$LATEST_TAG" ]; then + NOTES="${NOTES}### 이전 버전($LATEST_TAG)부터의 변경사항:\n" + # 최근 태그부터 현재까지의 커밋만 가져옴 + COMMITS=$(git log --pretty=format:"- %h %s (%an)" ${LATEST_TAG}..HEAD --no-merges) + NOTES="${NOTES}${COMMITS}" + else + NOTES="${NOTES}### 커밋 내역:\n" + # 태그가 없는 경우 최근 10개 커밋만 표시 + COMMITS=$(git log --pretty=format:"- %h %s (%an)" --no-merges -n 10) + NOTES="${NOTES}${COMMITS}\n\n(이전 릴리스 태그가 없어 최근 10개 커밋만 표시)" + fi + + # 환경 변수로 저장 + echo "notes<> $GITHUB_OUTPUT + echo -e "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Build Release AAB + run: | + ./gradlew :app:bundleRelease + + - name: Upload Release Build to Artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: app/build/outputs/bundle/release/ + if-no-files-found: error + + - name: Create Github Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.extract_version.outputs.version }} + release_name: ${{ steps.extract_version.outputs.version }} + generate_release_notes: true + + - name: Upload artifact to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{secrets.FIREBASE_RELEASE_APP_ID}} + serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + groups: reed-android-testers + file: app/build/outputs/bundle/release/app-release.aab + releaseNotes: ${{ steps.firebase_release_note.outputs.notes }} diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 00000000..5a4e56c9 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,52 @@ +name: Android CI + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false" + GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true + +on: + pull_request: + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci-build: + runs-on: ubuntu-latest + + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-ci') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-home-cache-cleanup: true + + - name: Generate local.properties + run: echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 -d > ./local.properties + + - name: Generate keystore.properties + run: echo '${{ secrets.KEYSTORE_PROPERTIES }}' | base64 -d > ./keystore.properties + + - name: Generate google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > ./app/google-services.json + + - name: Code style checks + run: | + ./gradlew ktlintCheck detekt + + - name: Run build + run: ./gradlew buildDebug --stacktrace diff --git a/.github/workflows/create-jira-issue.yml b/.github/workflows/create-jira-issue.yml new file mode 100644 index 00000000..c878dcff --- /dev/null +++ b/.github/workflows/create-jira-issue.yml @@ -0,0 +1,154 @@ +name: Create Jira Issue +on: + issues: + types: + - opened + +jobs: + create-issue: + name: Create Jira issue + runs-on: ubuntu-latest + + steps: + - name: Determine Issue Type + id: type + run: | + if echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✨ feat'; then + echo "type=feature" >> $GITHUB_OUTPUT + echo "template=feature-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🐞 fix'; then + echo "type=fix" >> $GITHUB_OUTPUT + echo "template=fix-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🔨 refactor'; then + echo "type=refactor" >> $GITHUB_OUTPUT + echo "template=refactor-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '📃 docs'; then + echo "type=docs" >> $GITHUB_OUTPUT + echo "template=docs-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '⚙️ chore'; then + echo "type=chore" >> $GITHUB_OUTPUT + echo "template=chore-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✅ test'; then + echo "type=test" >> $GITHUB_OUTPUT + echo "template=test-task.yml" >> $GITHUB_OUTPUT + else + echo "type=feature" >> $GITHUB_OUTPUT + echo "template=feature-task.yml" >> $GITHUB_OUTPUT + fi + + - name: Clean Issue Title (for Jira Summary) + id: clean + run: | + raw="${{ github.event.issue.title }}" + # Remove prefix like 'feat]', 'fix]', 'chore]', etc. + clean_title=$(echo "$raw" | sed -E 's/^[a-z]+\]\s*//I') + echo "title=$clean_title" >> $GITHUB_OUTPUT + + - name: Jira Login + uses: atlassian/gajira-login@v3 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + + - name: Checkout main code + uses: actions/checkout@v4 + with: + ref: develop + + - name: Parse Issue + uses: stefanbuck/github-issue-parser@v3 + id: issue-parser + with: + template-path: .github/ISSUE_TEMPLATE/${{ steps.type.outputs.template }} + + - name: Convert markdown to Jira Syntax + uses: peter-evans/jira2md@v1 + id: md2jira + with: + input-text: | + ### Github Issue Link + - ${{ github.event.issue.html_url }} + + ### 기능 설명 + ${{ steps.issue-parser.outputs.issueparser_description }} + + ### 작업 목록 + ${{ steps.issue-parser.outputs.issueparser_tasks }} + + ### 참고 링크 + ${{ steps.issue-parser.outputs.issueparser_links }} + mode: md2jira + + - name: Create Issue + id: create + uses: atlassian/gajira-create@v3 + with: + project: BOOK + issuetype: Task + summary: '${{ steps.clean.outputs.title }}' + description: '${{ steps.md2jira.outputs.output-text }}' + fields: | + { + "parent": { + "key": "${{ steps.issue-parser.outputs.issueparser_parentKey }}" + } + } + + - name: Checkout both branches + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Switch to develop + run: | + git fetch origin develop + git checkout develop + + - name: Generate Branch Name + id: branch + run: | + issue_number=${{ github.event.issue.number }} + issue_title="${{ github.event.issue.title }}" + slug=$(echo "$issue_title" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g' | sed 's/[^a-z0-9\-]//g') + ticket_key="${{ steps.create.outputs.issue }}" + branch_name="${ticket_key}-${{ steps.type.outputs.type }}/#${issue_number}" + echo "branch=${branch_name}" >> $GITHUB_OUTPUT + + - name: Create and push branch + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "${{ steps.branch.outputs.branch }}" + git push origin "${{ steps.branch.outputs.branch }}" + + - name: Update issue title + uses: actions-cool/issues-helper@v3 + with: + actions: 'update-issue' + token: ${{ secrets.PAT_TOKEN }} + title: '[${{ steps.create.outputs.issue }}/${{ github.event.issue.title }}' + + - name: Add comment with Jira issue link + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: 'Jira Issue Created: [${{ steps.create.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create.outputs.issue }})' + + - name: Add comment with Branch Name + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: '🔀 Branch Created: `${{ steps.branch.outputs.branch }}`' + + - name: Assign issue author + uses: actions-cool/issues-helper@v3 + with: + actions: 'add-assignees' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + assignees: ${{ github.event.issue.user.login }} diff --git a/.gitignore b/.gitignore index 83e6e812..cf4b4700 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ render.experimental.xml # Keystore files *.jks *.keystore +keystore.properties # Google Services (e.g. APIs or Firebase) google-services.json @@ -204,3 +205,5 @@ secrets.properties !/gradle/wrapper/gradle-wrapper.jar # End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio + +node_modules/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..1652f19f --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" +npx jira-prepare-commit-msg "$1" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fbbc68d5..5b32018e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,59 +1,106 @@ +@file:Suppress("INLINE_FROM_HIGHER_PLATFORM") + +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import java.util.Properties + plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) + alias(libs.plugins.booket.android.application) + alias(libs.plugins.booket.android.application.compose) + alias(libs.plugins.booket.android.hilt) + alias(libs.plugins.booket.android.firebase) } android { namespace = "com.ninecraft.booket" - compileSdk = 35 - defaultConfig { - applicationId = "com.ninecraft.booket" - minSdk = 28 - targetSdk = 35 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + signingConfigs { + create("release") { + val propertiesFile = rootProject.file("keystore.properties") + val properties = Properties() + properties.load(propertiesFile.inputStream()) + storeFile = rootProject.file(properties["STORE_FILE"] as String) + storePassword = properties["STORE_PASSWORD"] as String + keyAlias = properties["KEY_ALIAS"] as String + keyPassword = properties["KEY_PASSWORD"] as String + } } buildTypes { - release { - isMinifyEnabled = false + getByName("debug") { + isDebuggable = true + applicationIdSuffix = ".dev" + manifestPlaceholders += mapOf( + "appName" to "@string/app_name_dev", + ) + } + + getByName("release") { + isDebuggable = false + isMinifyEnabled = true + isShrinkResources = true + signingConfig = signingConfigs.getByName("release") + manifestPlaceholders += mapOf( + "appName" to "@string/app_name", + ) proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + + defaultConfig { + buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getApiKey("KAKAO_NATIVE_APP_KEY")) + manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = getApiKey("KAKAO_NATIVE_APP_KEY").trim('"') } + buildFeatures { - compose = true + buildConfig = true } } +ksp { + arg("circuit.codegen.mode", "hilt") +} + dependencies { + implementations( + projects.core.common, + projects.core.data.api, + projects.core.data.impl, + projects.core.datastore.api, + projects.core.datastore.impl, + projects.core.designsystem, + projects.core.model, + projects.core.network, + projects.core.ui, + projects.core.ocr, + + projects.feature.detail, + projects.feature.home, + projects.feature.library, + projects.feature.login, + projects.feature.main, + projects.feature.onboarding, + projects.feature.record, + projects.feature.screens, + projects.feature.search, + projects.feature.settings, + projects.feature.splash, + projects.feature.webview, + + libs.androidx.activity.compose, + libs.androidx.startup, + libs.coil.compose, + libs.kakao.auth, + libs.logger, + + libs.bundles.circuit, + ) + api(libs.circuit.codegen.annotation) + ksp(libs.circuit.codegen.ksp) +} - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) +fun getApiKey(propertyKey: String): String { + return gradleLocalProperties(rootDir, providers).getProperty(propertyKey) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..278063fc 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,20 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +# Kakao Login +-keep class com.kakao.sdk.**.model.* { ; } + +# https://github.com/square/okhttp/pull/6792 +-dontwarn org.bouncycastle.jsse.** +-dontwarn org.conscrypt.* +-dontwarn org.openjsse.** + +# refrofit2 (with r8 full mode) +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> +-keep,allowobfuscation,allowshrinking class retrofit2.Response diff --git a/app/src/androidTest/java/com/ninecraft/booket/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/ninecraft/booket/ExampleInstrumentedTest.kt deleted file mode 100644 index 10893ff1..00000000 --- a/app/src/androidTest/java/com/ninecraft/booket/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.ninecraft.booket - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * 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.ninecraft.booket", appContext.packageName) - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b995aa0..1443cd8d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,27 +2,64 @@ + + + + + + + + + + + + + + + + + android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity" + android:exported="true"> + - + + + + + + - + + diff --git a/app/src/main/java/com/ninecraft/booket/MainActivity.kt b/app/src/main/java/com/ninecraft/booket/MainActivity.kt deleted file mode 100644 index 468780cc..00000000 --- a/app/src/main/java/com/ninecraft/booket/MainActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.ninecraft.booket - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.ninecraft.booket.ui.theme.BooketAndroidTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - BooketAndroidTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - BooketAndroidTheme { - Greeting("Android") - } -} diff --git a/app/src/main/java/com/ninecraft/booket/ui/theme/Color.kt b/app/src/main/java/com/ninecraft/booket/ui/theme/Color.kt deleted file mode 100644 index 9b9979e8..00000000 --- a/app/src/main/java/com/ninecraft/booket/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ninecraft.booket.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/ninecraft/booket/ui/theme/Theme.kt b/app/src/main/java/com/ninecraft/booket/ui/theme/Theme.kt deleted file mode 100644 index 353c8bde..00000000 --- a/app/src/main/java/com/ninecraft/booket/ui/theme/Theme.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.ninecraft.booket.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun BooketAndroidTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} diff --git a/app/src/main/java/com/ninecraft/booket/ui/theme/Type.kt b/app/src/main/java/com/ninecraft/booket/ui/theme/Type.kt deleted file mode 100644 index 3a0f897c..00000000 --- a/app/src/main/java/com/ninecraft/booket/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.ninecraft.booket.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt b/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt new file mode 100644 index 00000000..4a77e70b --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt @@ -0,0 +1,24 @@ +package com.ninecraft.booket + +import android.app.Application +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.disk.DiskCache +import coil.util.DebugLogger +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class BooketApplication : Application(), ImageLoaderFactory { + override fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(this) + .diskCache { + DiskCache.Builder() + .directory(cacheDir.resolve("image_cache")) + .maxSizeBytes(10 * 1024 * 1024) + .build() + } + .logger(DebugLogger()) + .respectCacheHeaders(false) + .build() + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt new file mode 100644 index 00000000..87dc4e6f --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt @@ -0,0 +1,34 @@ +package com.ninecraft.booket.di + +import com.slack.circuit.foundation.Circuit +import com.slack.circuit.runtime.presenter.Presenter +import com.slack.circuit.runtime.ui.Ui +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped +import dagger.multibindings.Multibinds + +@Module +@InstallIn(ActivityRetainedComponent::class) +abstract class CircuitModule { + + @Multibinds + abstract fun presenterFactories(): Set + + @Multibinds + abstract fun uiFactories(): Set + + companion object { + @[Provides ActivityRetainedScoped] + fun provideCircuit( + presenterFactories: @JvmSuppressWildcards Set, + uiFactories: @JvmSuppressWildcards Set, + ): Circuit = Circuit.Builder() + .addPresenterFactories(presenterFactories) + .addUiFactories(uiFactories) + // .setAnimatedNavDecoratorFactory(CrossFadeNavDecoratorFactory()) + .build() + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt b/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt new file mode 100644 index 00000000..a79f90f6 --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt @@ -0,0 +1,29 @@ +package com.ninecraft.booket.di + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import com.slack.circuit.backstack.NavArgument +import com.slack.circuit.foundation.NavigatorDefaults +import com.slack.circuit.foundation.animation.AnimatedNavDecorator +import com.slack.circuit.foundation.animation.AnimatedNavEvent +import com.slack.circuit.foundation.animation.AnimatedNavState + +data class CrossFadeNavDecoratorFactory(val durationMillis: Int = 300) : + AnimatedNavDecorator.Factory { + override fun create(): AnimatedNavDecorator = + CrossFadeNavDecorator(durationMillis) +} + +class CrossFadeNavDecorator(private val durationMillis: Int) : + AnimatedNavDecorator> by NavigatorDefaults.DefaultDecorator() { + + override fun AnimatedContentTransitionScope.transitionSpec( + animatedNavEvent: AnimatedNavEvent, + ): ContentTransform { + return fadeIn(tween(durationMillis)) togetherWith fadeOut(tween(durationMillis)) + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/initializer/FirebaseCrashlyticsInitializer.kt b/app/src/main/kotlin/com/ninecraft/booket/initializer/FirebaseCrashlyticsInitializer.kt new file mode 100644 index 00000000..132d4051 --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/initializer/FirebaseCrashlyticsInitializer.kt @@ -0,0 +1,16 @@ +package com.ninecraft.booket.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.ninecraft.booket.BuildConfig + +class FirebaseCrashlyticsInitializer : Initializer { + override fun create(context: Context) { + FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = !BuildConfig.DEBUG + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/initializer/KakaoSdkInitializer.kt b/app/src/main/kotlin/com/ninecraft/booket/initializer/KakaoSdkInitializer.kt new file mode 100644 index 00000000..27f99d2c --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/initializer/KakaoSdkInitializer.kt @@ -0,0 +1,17 @@ +package com.ninecraft.booket.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.kakao.sdk.common.KakaoSdk +import com.ninecraft.booket.BuildConfig + +class KakaoSdkInitializer : Initializer { + + override fun create(context: Context) { + KakaoSdk.init(context, BuildConfig.KAKAO_NATIVE_APP_KEY) + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/initializer/LoggerInitializer.kt b/app/src/main/kotlin/com/ninecraft/booket/initializer/LoggerInitializer.kt new file mode 100644 index 00000000..95ef244d --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/initializer/LoggerInitializer.kt @@ -0,0 +1,21 @@ +package com.ninecraft.booket.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.ninecraft.booket.BuildConfig +import com.orhanobut.logger.AndroidLogAdapter +import com.orhanobut.logger.Logger + +class LoggerInitializer : Initializer { + + override fun create(context: Context) { + Logger.addLogAdapter(object : AndroidLogAdapter() { + override fun isLoggable(priority: Int, tag: String?): Boolean { + return BuildConfig.DEBUG + } + }) + } + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9c..00000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78e..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null 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 deleted file mode 100644 index b2dfe3d1..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null 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 deleted file mode 100644 index 62b611da..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null 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 deleted file mode 100644 index 1b9a6956..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null 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 deleted file mode 100644 index 9287f508..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null 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 deleted file mode 100644 index 9126ae37..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55fe981b..83483fc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ - Booket-Android - \ No newline at end of file + Reed + Reed.dev + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index f9c05e74..00000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - + + 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 00000000..90f1c070 --- /dev/null +++ b/core/designsystem/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + 네트워크 연결이 불안해요.\n잠시후 다시 이용해주세요. + 이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요. + 알 수 없는 오류가 발생하였습니다. + 도서 검색 후 내 서재에 담아보세요 + diff --git a/core/designsystem/src/main/res/values/themes.xml b/core/designsystem/src/main/res/values/themes.xml new file mode 100644 index 00000000..67a14ca0 --- /dev/null +++ b/core/designsystem/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +