diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..da39e4f --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,3 @@ +{ + "mcpServers": {} +} diff --git a/.env.example b/.env.example index 6240b77..8bfa282 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,15 @@ # Supabase Configuration -SUPABASE_URL=https://your-project-id.supabase.co -SUPABASE_ANON_KEY=your-supabase-anon-key +SUPABASE_URL=your_supabase_url_here +SUPABASE_ANON_KEY=your_supabase_anon_key_here # Google OAuth Configuration -GOOGLE_WEB_CLIENT_ID=your-google-web-client-id.apps.googleusercontent.com -GOOGLE_IOS_CLIENT_ID=your-google-ios-client-id.apps.googleusercontent.com -GOOGLE_ANDROID_CLIENT_ID=your-google-android-client-id.apps.googleusercontent.com - -# Kakao OAuth Configuration -KAKAO_NATIVE_APP_KEY=your-kakao-native-app-key -KAKAO_REST_API_KEY=your-kakao-rest-api-key -KAKAO_JAVASCRIPT_KEY=your-kakao-javascript-key +GOOGLE_WEB_CLIENT_ID=your_web_client_id_here +GOOGLE_IOS_CLIENT_ID=your_ios_client_id_here +GOOGLE_ANDROID_CLIENT_ID=your_android_client_id_here # App Configuration BUNDLE_ID=com.example.runnerApp + +# Google Maps API Keys +GOOGLE_MAPS_API_KEY_IOS=your_ios_google_maps_api_key_here +GOOGLE_MAPS_API_KEY_ANDROID=your_android_google_maps_api_key_here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0946377 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,125 @@ +name: Flutter CI/CD + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + build_and_test: + name: Build & Test + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: โ˜• Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.8.1' + channel: 'stable' + cache: true + + - name: ๐Ÿ“ฆ Install dependencies + run: flutter pub get + + - name: ๐Ÿ” Verify formatting + run: dart format --set-exit-if-changed . + + - name: ๐Ÿ“Š Analyze code + run: flutter analyze + + - name: ๐Ÿงช Run unit tests + run: flutter test --no-pub --coverage --test-randomize-ordering-seed random + + - name: ๐Ÿ“ˆ Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage/lcov.info + fail_ci_if_error: false + + - name: ๐Ÿ—๏ธ Build APK (Android) + run: flutter build apk --debug --no-pub + + - name: ๐Ÿ“ค Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: app-debug-apk + path: build/app/outputs/flutter-apk/app-debug.apk + + code_quality: + name: Code Quality Check + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.8.1' + channel: 'stable' + + - name: ๐Ÿ“ฆ Install dependencies + run: flutter pub get + + - name: ๐Ÿ” Run static analysis + run: | + echo "## Code Quality Report ๐Ÿ“Š" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + flutter analyze > analysis.txt 2>&1 || true + if grep -q "No issues found!" analysis.txt; then + echo "โœ… **No issues found!**" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ **Issues detected:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat analysis.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + fi + + - name: ๐Ÿ“ˆ Generate test coverage report + run: | + flutter test --coverage + echo "### Test Coverage ๐ŸŽฏ" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Coverage report generated. Check Codecov for detailed metrics." >> $GITHUB_STEP_SUMMARY + + security: + name: Security Scan + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ”’ Run security scan + run: | + echo "## Security Scan ๐Ÿ”’" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + # Check for hardcoded secrets + if grep -r "sk_" . --exclude-dir={.git,build,ios,android} || \ + grep -r "api_key" . --exclude-dir={.git,build,ios,android} || \ + grep -r "password" . --exclude-dir={.git,build,ios,android}; then + echo "โš ๏ธ **Potential secrets detected!**" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… **No obvious secrets detected**" >> $GITHUB_STEP_SUMMARY + fi + + # Check .env file is in .gitignore + if grep -q "\.env" .gitignore; then + echo "โœ… **.env file is properly ignored**" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ **.env file is NOT in .gitignore!**" >> $GITHUB_STEP_SUMMARY + fi + diff --git a/.github/workflows/portfolio-stats.yml b/.github/workflows/portfolio-stats.yml new file mode 100644 index 0000000..97ed6ac --- /dev/null +++ b/.github/workflows/portfolio-stats.yml @@ -0,0 +1,54 @@ +# GitHub Actions์œผ๋กœ ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„ ์ž๋™ ์ƒ์„ฑ +# ์ด ํŒŒ์ผ์€ README์— ์ž๋™์œผ๋กœ ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค + +name: Portfolio Stats + +on: + schedule: + # ๋งค์ฃผ ์ผ์š”์ผ ์ž์ •์— ์‹คํ–‰ + - cron: "0 0 * * 0" + workflow_dispatch: # ์ˆ˜๋™ ์‹คํ–‰ ๊ฐ€๋Šฅ + +jobs: + update-stats: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.8.1" + channel: "stable" + + - name: Get dependencies + run: flutter pub get + + - name: Run tests with coverage + run: flutter test --coverage + + - name: Generate coverage report + run: | + sudo apt-get update + sudo apt-get install -y lcov + genhtml coverage/lcov.info -o coverage/html + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info + fail_ci_if_error: false + + - name: Count lines of code + run: | + echo "๐Ÿ“Š ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„" > stats.txt + echo "Dart ์ฝ”๋“œ: $(find lib -name '*.dart' | xargs wc -l | tail -1 | awk '{print $1}')์ค„" >> stats.txt + echo "ํ…Œ์ŠคํŠธ ์ฝ”๋“œ: $(find test -name '*.dart' | xargs wc -l | tail -1 | awk '{print $1}')์ค„" >> stats.txt + cat stats.txt diff --git a/.gitignore b/.gitignore index 3a641a4..fe403bc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -195,3 +197,20 @@ node_modules/ # TernJS port file .tern-port + +logs +dev-debug.log +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific + +# Task files +# tasks.json +# tasks/ + diff --git a/.taskmaster/config.json b/.taskmaster/config.json new file mode 100644 index 0000000..968f6bf --- /dev/null +++ b/.taskmaster/config.json @@ -0,0 +1,44 @@ +{ + "models": { + "main": { + "provider": "anthropic", + "modelId": "claude-3-7-sonnet-20250219", + "maxTokens": 120000, + "temperature": 0.2 + }, + "research": { + "provider": "perplexity", + "modelId": "sonar-pro", + "maxTokens": 8700, + "temperature": 0.1 + }, + "fallback": { + "provider": "anthropic", + "modelId": "claude-3-7-sonnet-20250219", + "maxTokens": 120000, + "temperature": 0.2 + } + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultNumTasks": 10, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Taskmaster", + "ollamaBaseURL": "http://localhost:11434/api", + "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "responseLanguage": "y", + "enableCodebaseAnalysis": true, + "defaultTag": "master", + "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/", + "userId": "1234567890" + }, + "claudeCode": {}, + "codexCli": {}, + "grokCli": { + "timeout": 120000, + "workingDirectory": null, + "defaultModel": "grok-4-latest" + } +} \ No newline at end of file diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt new file mode 100644 index 0000000..8035ae1 --- /dev/null +++ b/.taskmaster/docs/prd.txt @@ -0,0 +1,337 @@ + +# Overview +StrideNote๋Š” ๋Ÿฌ๋‹์„ ์ฆ๊ธฐ๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๊ฐœ์ธ ๋งž์ถคํ˜• ํŠธ๋ž˜์ปค ์•ฑ์ž…๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ ๊ธฐ๋ก์„ ๋„˜์–ด "๋Ÿฌ๋‹ ์Šคํ† ๋ฆฌ"๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋ฉฐ, GPS ๊ธฐ๋ฐ˜ ๊ฑฐ๋ฆฌ ์ถ”์ , ์‹ฌ๋ฐ•์ˆ˜ ์—ฐ๋™, AI ๊ธฐ๋ฐ˜ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ์„ฑ์žฅ์„ ์ง๊ด€์ ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ๋™๊ธฐ๋ถ€์—ฌ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +**๋ฌธ์ œ์ **: ๊ธฐ์กด ๋Ÿฌ๋‹ ์•ฑ๋“ค์€ ๋ณต์žกํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋งŒ ๋‚˜์—ดํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์†์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. +**์†”๋ฃจ์…˜**: ์ง๊ด€์ ์ธ UI/UX์™€ AI ๊ธฐ๋ฐ˜ ๊ฐœ์ธํ™”๋กœ ๋Ÿฌ๋‹์„ ์ฆ๊ฒ๊ณ  ์ง€์† ๊ฐ€๋Šฅํ•œ ์Šต๊ด€์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค. +**ํƒ€๊ฒŸ ์‚ฌ์šฉ์ž**: ๋Ÿฌ๋‹ ์ž…๋ฌธ์ž๋ถ€ํ„ฐ ์ค‘๊ธ‰ ๋Ÿฌ๋„ˆ๊นŒ์ง€, ์ž์‹ ์˜ ๊ธฐ๋ก์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์‹ถ์€ ์‚ฌ์šฉ์ž + +# Core Features +1. **๋Ÿฌ๋‹ ์ž๋™ ๊ธฐ๋ก** + - GPS ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๊ฑฐ๋ฆฌ, ํŽ˜์ด์Šค, ์‹œ๊ฐ„, ๊ณ ๋„ ์ถ”์  + - ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ๋„ ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ + - ์ผ์‹œ์ •์ง€/์žฌ๊ฐœ ๊ธฐ๋Šฅ + - ์ค‘์š”ํ•œ ์ด์œ : ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ๊ณต, ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์“ฐ๋Š” ์ฃผ๋œ ๋ชฉ์  + +2. **์‹ฌ๋ฐ•์ˆ˜ ์—ฐ๋™** + - HealthKit(iOS), Google Fit(Android) ์—ฐ๋™ + - ์›จ์–ด๋Ÿฌ๋ธ” ๊ธฐ๊ธฐ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” + - ์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„ (ํœด์‹, ์œ ์‚ฐ์†Œ, ๋ฌด์‚ฐ์†Œ) + - ์ค‘์š”ํ•œ ์ด์œ : ์ „๋ฌธ์„ฑ ์ œ๊ณต, ์ฐจ๋ณ„ํ™” ์š”์†Œ + +3. **ํ›ˆ๋ จ ์š”์•ฝ ๋ฆฌํฌํŠธ** + - ๋Ÿฌ๋‹ ์ข…๋ฃŒ ํ›„ ์ž๋™ ์ƒ์„ฑ + - ๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„, ํ‰๊ท  ํŽ˜์ด์Šค, ์นผ๋กœ๋ฆฌ, ์‹ฌ๋ฐ•์ˆ˜ ์š”์•ฝ + - ์ด์ „ ๊ธฐ๋ก๊ณผ ๋น„๊ต + - ์ค‘์š”ํ•œ ์ด์œ : ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์œผ๋กœ ์„ฑ์ทจ๊ฐ ์ œ๊ณต + +4. **๋Ÿฌ๋‹ ํžˆ์Šคํ† ๋ฆฌ & ํ†ต๊ณ„** + - ์ฃผ๊ฐ„/์›”๊ฐ„ ํ†ต๊ณ„ ์‹œ๊ฐํ™” (FL Chart ์‚ฌ์šฉ) + - ์ด ๊ฑฐ๋ฆฌ, ์ด ์‹œ๊ฐ„, ํ‰๊ท  ํŽ˜์ด์Šค ์ถ”์ด + - ๋ฐฐ์ง€ ์‹œ์Šคํ…œ (๋ชฉํ‘œ ๋‹ฌ์„ฑ ์‹œ ๋ฐฐ์ง€ ํš๋“) + - ์ค‘์š”ํ•œ ์ด์œ : ์žฅ๊ธฐ์ ์ธ ๋™๊ธฐ๋ถ€์—ฌ, ๋ฆฌํ…์…˜ ํ•ต์‹ฌ + +5. **์‚ฌ์šฉ์ž ์ธ์ฆ & ํ”„๋กœํ•„** + - ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ + - Google ์†Œ์…œ ๋กœ๊ทธ์ธ (๋„ค์ดํ‹ฐ๋ธŒ) + - ํ”„๋กœํ•„ ๊ด€๋ฆฌ (์ด๋ฆ„, ์‚ฌ์ง„, ๋ชฉํ‘œ ์„ค์ •) + - Supabase ๊ธฐ๋ฐ˜ ์ธ์ฆ & ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” + - ์ค‘์š”ํ•œ ์ด์œ : ๋ฐ์ดํ„ฐ ๋ฐฑ์—…, ๋ฉ€ํ‹ฐ ๋””๋ฐ”์ด์Šค ์ง€์› + +# User Experience + +## User Personas +**์ดˆ๊ธ‰ ๋Ÿฌ๋„ˆ (๋ฏผ์ง€, 28์„ธ, ์ง์žฅ์ธ)** +- ์ตœ๊ทผ ๋‹ค์ด์–ดํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉฐ ๋Ÿฌ๋‹ ์ž…๋ฌธ +- ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ธ ๊ธฐ๋ก ์›ํ•จ +- ์„ฑ์ทจ๊ฐ๊ณผ ๋™๊ธฐ๋ถ€์—ฌ ํ•„์š” + +**์ค‘๊ธ‰ ๋Ÿฌ๋„ˆ (์ง„์ˆ˜, 35์„ธ, IT ๊ฐœ๋ฐœ์ž)** +- ์ฃผ 3-4ํšŒ ๊ทœ์น™์ ์œผ๋กœ ๋Ÿฌ๋‹ +- ํŽ˜์ด์Šค ๊ฐœ์„ ๊ณผ ๊ธฐ๋ก ๋‹จ์ถ•์— ๊ด€์‹ฌ +- ๋ฐ์ดํ„ฐ ๋ถ„์„๊ณผ ์ถ”์ด ํ™•์ธ ์›ํ•จ + +## Key User Flows +1. **์ฒซ ๋Ÿฌ๋‹ ์‹œ์ž‘** + ์•ฑ ์‹คํ–‰ โ†’ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… โ†’ ์œ„์น˜ ๊ถŒํ•œ ํ—ˆ์šฉ โ†’ ํ™ˆ ํ™”๋ฉด โ†’ "๋Ÿฌ๋‹ ์‹œ์ž‘" ๋ฒ„ํŠผ โ†’ GPS ์—ฐ๊ฒฐ ๋Œ€๊ธฐ โ†’ ์นด์šดํŠธ๋‹ค์šด โ†’ ๋Ÿฌ๋‹ ์ค‘ ํ™”๋ฉด + +2. **๋Ÿฌ๋‹ ์ค‘** + ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„, ํŽ˜์ด์Šค, ์‹ฌ๋ฐ•์ˆ˜) โ†’ ์ผ์‹œ์ •์ง€/์žฌ๊ฐœ ๊ฐ€๋Šฅ โ†’ ์Œ์„ฑ ์•Œ๋ฆผ (1km๋งˆ๋‹ค) โ†’ ์ข…๋ฃŒ ๋ฒ„ํŠผ + +3. **๋Ÿฌ๋‹ ์ข…๋ฃŒ ํ›„** + ์ž๋™ ์ €์žฅ โ†’ ์š”์•ฝ ๋ฆฌํฌํŠธ ํ™”๋ฉด โ†’ ๊ณต์œ  ์˜ต์…˜ โ†’ ํžˆ์Šคํ† ๋ฆฌ๋กœ ์ด๋™ ๋˜๋Š” ํ™ˆ์œผ๋กœ ๋ณต๊ท€ + +4. **ํ†ต๊ณ„ ํ™•์ธ** + ํ™ˆ ํ™”๋ฉด โ†’ ํžˆ์Šคํ† ๋ฆฌ ํƒญ โ†’ ์ฃผ๊ฐ„/์›”๊ฐ„ ํ†ต๊ณ„ ์„ ํƒ โ†’ ์ฐจํŠธ ํ™•์ธ โ†’ ๊ฐœ๋ณ„ ๋Ÿฌ๋‹ ์ƒ์„ธ ๋ณด๊ธฐ + +## UI/UX Considerations +- **๋ธ”๋ฃจ ํ†ค ๊ธฐ๋ฐ˜ ์ปฌ๋Ÿฌ**: ์‹ ๋ขฐ๊ฐ๊ณผ ์—๋„ˆ์ง€ +- **ํ•œ ์† ์กฐ์ž‘ ์ค‘์‹ฌ**: ๋Ÿฌ๋‹ ์ค‘์—๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- **ํฐ ๋ฒ„ํŠผ, ๋ช…ํ™•ํ•œ ํ…์ŠคํŠธ**: ๊ฐ€๋…์„ฑ ์šฐ์„  +- **์ฆ‰๊ฐ์  ํ”ผ๋“œ๋ฐฑ**: ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์Œ์„ฑ ์•Œ๋ฆผ +- **์˜คํ”„๋ผ์ธ ์šฐ์„  ์„ค๊ณ„**: ๋„คํŠธ์›Œํฌ ์—†์ด๋„ ๊ธฐ๋ก ๊ฐ€๋Šฅ + + +# Technical Architecture + +## System Components +1. **Frontend (Flutter)** + - Stateful widgets for running screen + - Provider for state management + - Local database (SQLite) for offline support + - SharedPreferences for user settings + +2. **Backend (Supabase)** + - Authentication (Email/Password, Google OAuth) + - PostgreSQL database (user_profiles, running_sessions) + - Row Level Security (RLS) policies + - Real-time subscriptions (future) + +3. **Third-party Services** + - Geolocator: GPS tracking + - HealthKit/Google Fit: Health data integration + - Google Sign-In: Native social login + - Flutter TTS: Voice announcements + +## Data Models +**User Profile** +```dart +class UserProfile { + String id; + String email; + String? displayName; + String? photoUrl; + String? fitnessLevel; // beginner, intermediate, advanced + double? targetWeeklyDistance; + DateTime createdAt; + DateTime updatedAt; +} +``` + +**Running Session** +```dart +class RunningSession { + String id; + String userId; + double distance; // meters + int duration; // seconds + double averagePace; // min/km + double? averageHeartRate; + double? elevation; + List? route; + DateTime startTime; + DateTime endTime; + int calories; +} +``` + +## APIs and Integrations +- Supabase Auth API +- Supabase Database API (REST) +- Google Sign-In SDK +- HealthKit Framework (iOS) +- Google Fit API (Android) + +## Infrastructure Requirements +- Flutter SDK 3.8.1+ +- Dart SDK 3.0.0+ +- Supabase project +- Google Cloud Console project (OAuth) +- iOS: HealthKit entitlements +- Android: Location & Activity Recognition permissions + +# Development Roadmap + +## Phase 1: MVP - Core Running Features โœ… (์™„๋ฃŒ๋จ) +**๋ชฉํ‘œ**: ๊ธฐ๋ณธ์ ์ธ ๋Ÿฌ๋‹ ๊ธฐ๋ก๊ณผ ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋Šฅ ์ œ๊ณต + +**์™„๋ฃŒ๋œ ๊ธฐ๋Šฅ**: +- โœ… ์‚ฌ์šฉ์ž ์ธ์ฆ (์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ) +- โœ… Google ์†Œ์…œ ๋กœ๊ทธ์ธ (๋„ค์ดํ‹ฐ๋ธŒ) +- โœ… GPS ๊ธฐ๋ฐ˜ ๋Ÿฌ๋‹ ์ถ”์  +- โœ… ๋Ÿฌ๋‹ ์„ธ์…˜ ์ €์žฅ (๋กœ์ปฌ + ํด๋ผ์šฐ๋“œ) +- โœ… ๊ธฐ๋ณธ ํžˆ์Šคํ† ๋ฆฌ ํ™”๋ฉด +- โœ… ํ”„๋กœํ•„ ๊ด€๋ฆฌ +- โœ… ํ†ต๊ณ„ ์‹œ๊ฐํ™” (๊ธฐ๋ณธ) + +## Phase 2: Enhanced Features & Data Visualization ๐Ÿ”„ (์ง„ํ–‰ ์ค‘) +**๋ชฉํ‘œ**: ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  ๋ฐ ์ „๋ฌธ์„ฑ ๊ฐ•ํ™” + +**์ž‘์—… ํ•ญ๋ชฉ**: +1. **์›จ์–ด๋Ÿฌ๋ธ” ์—ฐ๋™ ๊ฐ•ํ™”** + - HealthKit ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ + - Google Fit ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” + - ์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™” + +2. **ํ–ฅ์ƒ๋œ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ** + - ์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒ์„ธ ํ†ต๊ณ„ + - ํŽ˜์ด์Šค ์ถ”์ด ๊ทธ๋ž˜ํ”„ + - ๋ชฉํ‘œ ๋Œ€๋น„ ์ง„ํ–‰๋ฅ  + - ๊ฐœ์ธ ์ตœ๊ณ  ๊ธฐ๋ก(PR) ํ‘œ์‹œ + +3. **๋ฐฐ์ง€ ์‹œ์Šคํ…œ** + - ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ๋ฐฐ์ง€ (์ฒซ 5km, 10km, ๋งˆ๋ผํ†ค ๋“ฑ) + - ์—ฐ์† ๋Ÿฌ๋‹ ๋ฐฐ์ง€ (์ฃผ๊ฐ„ ์—ฐ์† ๋‹ฌ์„ฑ) + - ์†๋„ ๊ธฐ๋ฐ˜ ๋ฐฐ์ง€ (ํ‰๊ท  ํŽ˜์ด์Šค ๊ฐœ์„ ) + +4. **์Œ์„ฑ ์•ˆ๋‚ด ๊ฐœ์„ ** + - 1km๋งˆ๋‹ค ์Œ์„ฑ ์•Œ๋ฆผ + - ํŽ˜์ด์Šค ์•ˆ๋‚ด + - ๋ชฉํ‘œ ๋‹ฌ์„ฑ ์•Œ๋ฆผ + +5. **๋ฆฌํŒฉํ„ฐ๋ง & ์ฝ”๋“œ ํ’ˆ์งˆ** + - ์˜์กด์„ฑ ์ฃผ์ž… ํŒจํ„ด ์ ์šฉ + - ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ + - ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 90%+ + - ์œ„์ ฏ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ + +## Phase 3: AI & Social Features ๐Ÿ“‹ (๊ณ„ํš ์ค‘) +**๋ชฉํ‘œ**: AI ๊ธฐ๋ฐ˜ ๊ฐœ์ธํ™” ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ + +**์ž‘์—… ํ•ญ๋ชฉ**: +1. **AI ๋Ÿฌ๋‹ ํ”Œ๋žœ** + - ์‚ฌ์šฉ์ž ๋ ˆ๋ฒจ ๊ธฐ๋ฐ˜ ํ›ˆ๋ จ ๊ณ„ํš ์ƒ์„ฑ + - ๋ชฉํ‘œ ๋‹ฌ์„ฑ์„ ์œ„ํ•œ ์ฃผ๊ฐ„ ํ”Œ๋žœ + - ํœด์‹์ผ ์ถ”์ฒœ + +2. **์†Œ์…œ ๊ธฐ๋Šฅ** + - ๋Ÿฌ๋‹ ๊ธฐ๋ก ๊ณต์œ  (์นด์นด์˜คํ†ก, ์ธ์Šคํƒ€๊ทธ๋žจ) + - ์นœ๊ตฌ ์ดˆ๋Œ€ ๋ฐ ๋น„๊ต + - ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฑŒ๋ฆฐ์ง€ + +3. **์Œ์•… ์—ฐ๋™** + - Spotify ํ†ตํ•ฉ + - ๋Ÿฌ๋‹ ์ค‘ ์Œ์•… ์žฌ์ƒ ์ œ์–ด + - BPM ๊ธฐ๋ฐ˜ ์Œ์•… ์ถ”์ฒœ + +4. **๊ณ ๊ธ‰ ๋ถ„์„** + - ๋Ÿฌ๋‹ ํšจ์œจ์„ฑ ๋ถ„์„ + - ๋ถ€์ƒ ์œ„ํ—˜ ์˜ˆ์ธก + - ๊ฐœ์ธํ™”๋œ ํ”ผ๋“œ๋ฐฑ + +# Logical Dependency Chain + +## ๊ธฐ๋ณธ ์ธํ”„๋ผ (Foundation) โœ… +1. โœ… Supabase ํ”„๋กœ์ ํŠธ ์„ค์ • +2. โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ (user_profiles, running_sessions) +3. โœ… ์ธ์ฆ ์‹œ์Šคํ…œ (Email, Google OAuth) +4. โœ… ๊ธฐ๋ณธ ์•ฑ ๊ตฌ์กฐ ๋ฐ ๋ผ์šฐํŒ… + +## ์ฝ”์–ด ๊ธฐ๋Šฅ (Core) โœ… +5. โœ… GPS ์œ„์น˜ ์ถ”์  ์„œ๋น„์Šค +6. โœ… ๋Ÿฌ๋‹ ์„ธ์…˜ ๊ธฐ๋ก ๋กœ์ง +7. โœ… ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (SQLite) +8. โœ… ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™” + +## UI/UX (User-Facing) โœ… +9. โœ… ๋Ÿฌ๋‹ ํ™”๋ฉด (์‹œ์ž‘/์ผ์‹œ์ •์ง€/์ข…๋ฃŒ) +10. โœ… ํžˆ์Šคํ† ๋ฆฌ ํ™”๋ฉด +11. โœ… ํ”„๋กœํ•„ ํ™”๋ฉด +12. โœ… ๊ธฐ๋ณธ ํ†ต๊ณ„ ์ฐจํŠธ + +## ๊ฐœ์„  ์‚ฌํ•ญ (Enhancements) ๐Ÿ”„ +13. ๐Ÿ”„ ์›จ์–ด๋Ÿฌ๋ธ” ์—ฐ๋™ (HealthKit/Google Fit) +14. ๐Ÿ”„ ๋ฐฐ์ง€ ์‹œ์Šคํ…œ +15. ๐Ÿ”„ ์Œ์„ฑ ์•ˆ๋‚ด +16. ๐Ÿ”„ ํ–ฅ์ƒ๋œ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ +17. ๐Ÿ”„ ์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง ๋ฐ ํ…Œ์ŠคํŠธ + +## ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (Advanced) ๐Ÿ“‹ +18. ๐Ÿ“‹ AI ๋Ÿฌ๋‹ ํ”Œ๋žœ ์ƒ์„ฑ +19. ๐Ÿ“‹ ์†Œ์…œ ๊ณต์œ  ๊ธฐ๋Šฅ +20. ๐Ÿ“‹ ์Œ์•… ์—ฐ๋™ +21. ๐Ÿ“‹ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฑŒ๋ฆฐ์ง€ + +# Risks and Mitigations + +## Technical Challenges + +**Risk 1: GPS ์ •ํ™•๋„ ๋ฌธ์ œ** +- ์‹ค๋‚ด๋‚˜ ํ„ฐ๋„์—์„œ GPS ์‹ ํ˜ธ ์•ฝํ™” +- Mitigation: + - ๊ฐ€์†๋„๊ณ„ ๋ฐ์ดํ„ฐ๋กœ ๋ณด์ • + - ์‹ ํ˜ธ ์•ฝํ•  ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ + - ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ์—์„œ๋„ ๊ธฐ๋ณธ ๊ธฐ๋ก ์œ ์ง€ + +**Risk 2: ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ** +- GPS์™€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์œผ๋กœ ์ธํ•œ ๋ฐฐํ„ฐ๋ฆฌ ๋“œ๋ ˆ์ธ +- Mitigation: + - ์œ„์น˜ ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ ์ตœ์ ํ™” (5-10์ดˆ) + - ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ถˆํ•„์š”ํ•œ ์ž‘์—… ์ตœ์†Œํ™” + - ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ ๋ชจ๋“œ ์ œ๊ณต + +**Risk 3: HealthKit/Google Fit ๊ถŒํ•œ ๋ฌธ์ œ** +- ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๊ธฐ๋Šฅ ์ œํ•œ +- Mitigation: + - ๊ถŒํ•œ ์—†์ด๋„ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค๊ณ„ + - ๊ถŒํ•œ ์š”์ฒญ ์‹œ ๋ช…ํ™•ํ•œ ์„ค๋ช… ์ œ๊ณต + - ์„ค์ • ํ™”๋ฉด์—์„œ ์žฌ์š”์ฒญ ๊ฐ€๋Šฅ + +**Risk 4: ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ฐจ์ด** +- iOS์™€ Android์˜ ๋™์ž‘ ์ฐจ์ด +- Mitigation: + - ํ”Œ๋žซํผ๋ณ„ ํ…Œ์ŠคํŠธ ์ฒ ์ €ํžˆ ์ง„ํ–‰ + - ํ”Œ๋žซํผ ํŠนํ™” ์ฝ”๋“œ ์ตœ์†Œํ™” + - ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ์‚ฌ์šฉ + +## MVP Definition +**์ตœ์†Œ ๊ธฐ๋Šฅ ์ œํ’ˆ (์ด๋ฏธ ๋‹ฌ์„ฑ๋จ)**: +- ๋Ÿฌ๋‹ ๊ธฐ๋ก (GPS ๊ธฐ๋ฐ˜) +- ๊ธฐ๋ณธ ํ†ต๊ณ„ (๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„, ํŽ˜์ด์Šค) +- ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ ๋ฐ ์กฐํšŒ +- ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ํ”„๋กœํ•„ + +**๋‹ค์Œ MVP (Phase 2 ๋ชฉํ‘œ)**: +- ์›จ์–ด๋Ÿฌ๋ธ” ์—ฐ๋™ (์‹ฌ๋ฐ•์ˆ˜) +- ํ–ฅ์ƒ๋œ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ +- ๋ฐฐ์ง€ ์‹œ์Šคํ…œ +- ์Œ์„ฑ ์•ˆ๋‚ด + +## Resource Constraints + +**๊ฐœ๋ฐœ ๋ฆฌ์†Œ์Šค**: +- 1์ธ ๊ฐœ๋ฐœ์ž (๋˜๋Š” ์†Œ๊ทœ๋ชจ ํŒ€) +- ์‹œ๊ฐ„ ์ œ์•ฝ ์กด์žฌ +- Mitigation: + - ๊ธฐ๋Šฅ ์šฐ์„ ์ˆœ์œ„ ๋ช…ํ™•ํžˆ + - MVP์— ์ง‘์ค‘, ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ์€ ํ›„์ˆœ์œ„ + - ์˜คํ”ˆ์†Œ์Šค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ๊ทน ํ™œ์šฉ + +**์ธํ”„๋ผ ๋น„์šฉ**: +- Supabase ๋ฌด๋ฃŒ ํ‹ฐ์–ด ์ œํ•œ +- Mitigation: + - ์ดˆ๊ธฐ์—๋Š” ๋ฌด๋ฃŒ ํ‹ฐ์–ด๋กœ ์ถฉ๋ถ„ + - ์‚ฌ์šฉ์ž ์ฆ๊ฐ€ ์‹œ ์œ ๋ฃŒ ํ”Œ๋žœ ๊ณ ๋ ค + - ๋กœ์ปฌ ์šฐ์„  ์„ค๊ณ„๋กœ ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ตœ์†Œํ™” + +# Appendix + +## Research Findings +- ๋Ÿฌ๋‹ ์•ฑ ์‚ฌ์šฉ์ž์˜ 70%๋Š” "๊ฐ„๋‹จํ•จ"์„ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐ +- ๋ฐฐ์ง€ ์‹œ์Šคํ…œ์€ ๋ฆฌํ…์…˜์„ 30% ์ฆ๊ฐ€์‹œํ‚ด (Strava ์‚ฌ๋ก€) +- ์Œ์„ฑ ์•ˆ๋‚ด๋Š” ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„๋ฅผ ํฌ๊ฒŒ ํ–ฅ์ƒ +- ์†Œ์…œ ๊ธฐ๋Šฅ์€ MAU(์›”๊ฐ„ ํ™œ์„ฑ ์‚ฌ์šฉ์ž)๋ฅผ 2๋ฐฐ ์ฆ๊ฐ€ ๊ฐ€๋Šฅ + +## Technical Specifications +- ์ตœ์†Œ Flutter ๋ฒ„์ „: 3.8.1 +- ์ตœ์†Œ iOS ๋ฒ„์ „: 12.0 +- ์ตœ์†Œ Android ๋ฒ„์ „: API 26 (Android 8.0) +- GPS ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ: 5-10์ดˆ +- ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”: ์•ฑ ์‹œ์ž‘ ์‹œ + ๋Ÿฌ๋‹ ์ข…๋ฃŒ ์‹œ + +## Current Status +- Phase 1 (MVP): โœ… ์™„๋ฃŒ +- Phase 2 (Enhanced Features): ๐Ÿ”„ ์ง„ํ–‰ ์ค‘ (์•ฝ 30%) +- Phase 3 (AI & Social): ๐Ÿ“‹ ๊ณ„ํš ๋‹จ๊ณ„ + +## Next Immediate Tasks +1. HealthKit ์—ฐ๋™ ๊ตฌํ˜„ +2. Google Fit ์—ฐ๋™ ๊ตฌํ˜„ +3. ๋ฐฐ์ง€ ์‹œ์Šคํ…œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ +4. ๋ฐฐ์ง€ ์‹œ์Šคํ…œ UI ๊ตฌํ˜„ +5. ์Œ์„ฑ ์•ˆ๋‚ด ๊ธฐ๋Šฅ ๊ตฌํ˜„ +6. ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ๊ฐœ์„  (์ฃผ๊ฐ„/์›”๊ฐ„ ์ƒ์„ธ ๋ทฐ) +7. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (๋ชฉํ‘œ: 90% ์ปค๋ฒ„๋ฆฌ์ง€) +8. ์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง (์˜์กด์„ฑ ์ฃผ์ž…, ์ค‘๋ณต ์ œ๊ฑฐ) + + diff --git a/.taskmaster/state.json b/.taskmaster/state.json new file mode 100644 index 0000000..dca2efa --- /dev/null +++ b/.taskmaster/state.json @@ -0,0 +1,6 @@ +{ + "currentTag": "development", + "lastSwitched": "2025-10-16T01:02:23.666Z", + "branchTagMapping": {}, + "migrationNoticeShown": false +} \ No newline at end of file diff --git a/.taskmaster/tasks/task_001_development.txt b/.taskmaster/tasks/task_001_development.txt new file mode 100644 index 0000000..7db5bd6 --- /dev/null +++ b/.taskmaster/tasks/task_001_development.txt @@ -0,0 +1,17 @@ +# Task ID: 1 +# Title: HealthKit ์—ฐ๋™ ๊ตฌํ˜„ (iOS) +# Status: pending +# Dependencies: None +# Priority: high +# Description: iOS์—์„œ HealthKit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. +# Details: +- HealthKit ๊ถŒํ•œ ์š”์ฒญ ๊ตฌํ˜„ +- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ ๋กœ์ง ์ž‘์„ฑ +- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ๋งคํ•‘ +- ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ +- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๋Œ€์‘ + +# Test Strategy: +- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋กœ์ง +- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: HealthKit API ํ˜ธ์ถœ +- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ๊ถŒํ•œ ๋ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ™•์ธ diff --git a/.taskmaster/tasks/task_002_development.txt b/.taskmaster/tasks/task_002_development.txt new file mode 100644 index 0000000..e24072c --- /dev/null +++ b/.taskmaster/tasks/task_002_development.txt @@ -0,0 +1,17 @@ +# Task ID: 2 +# Title: Google Fit ์—ฐ๋™ ๊ตฌํ˜„ (Android) +# Status: pending +# Dependencies: None +# Priority: high +# Description: Android์—์„œ Google Fit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. +# Details: +- Google Fit API ๊ถŒํ•œ ์š”์ฒญ +- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ +- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” +- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ๊ตฌํ˜„ +- ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ Fallback ์ฒ˜๋ฆฌ + +# Test Strategy: +- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง +- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: Google Fit API ํ˜ธ์ถœ +- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ Android ๊ธฐ๊ธฐ์—์„œ ํ™•์ธ diff --git a/.taskmaster/tasks/task_003_development.txt b/.taskmaster/tasks/task_003_development.txt new file mode 100644 index 0000000..585fdf5 --- /dev/null +++ b/.taskmaster/tasks/task_003_development.txt @@ -0,0 +1,17 @@ +# Task ID: 3 +# Title: ์Œ์„ฑ ์•ˆ๋‚ด ๊ธฐ๋Šฅ ๊ตฌํ˜„ +# Status: pending +# Dependencies: None +# Priority: high +# Description: ๋Ÿฌ๋‹ ์ค‘ 1km๋งˆ๋‹ค ๊ฑฐ๋ฆฌ, ํŽ˜์ด์Šค, ์‹œ๊ฐ„์„ ์Œ์„ฑ์œผ๋กœ ์•ˆ๋‚ดํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. +# Details: +- Flutter TTS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ +- 1km๋งˆ๋‹ค ์Œ์„ฑ ์•ˆ๋‚ด ํŠธ๋ฆฌ๊ฑฐ +- ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํฌ๋งท ("1ํ‚ฌ๋กœ๋ฏธํ„ฐ ์™„๋ฃŒ. ํŽ˜์ด์Šค 5๋ถ„ 30์ดˆ") +- ์‚ฌ์šฉ์ž ์„ค์ •์—์„œ ์Œ์„ฑ ์•ˆ๋‚ด ON/OFF ์˜ต์…˜ +- ์–ธ์–ด๋ณ„ ์Œ์„ฑ ์ง€์› (ํ•œ๊ตญ์–ด, ์˜์–ด) + +# Test Strategy: +- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ… ๋กœ์ง +- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: TTS ํ˜ธ์ถœ +- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๋Ÿฌ๋‹ ์ค‘ ์Œ์„ฑ ํ™•์ธ diff --git a/.taskmaster/tasks/task_004_development.txt b/.taskmaster/tasks/task_004_development.txt new file mode 100644 index 0000000..f994121 --- /dev/null +++ b/.taskmaster/tasks/task_004_development.txt @@ -0,0 +1,17 @@ +# Task ID: 4 +# Title: ๋ฐฐ์ง€ ์‹œ์Šคํ…œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ +# Status: pending +# Dependencies: None +# Priority: high +# Description: ๋ฐฐ์ง€ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ RLS ์ •์ฑ…์„ ์„ค๊ณ„ํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. +# Details: +- badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (id, name, description, criteria, icon) +- user_badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (user_id, badge_id, earned_at) +- RLS ์ •์ฑ… ์„ค์ • (์‚ฌ์šฉ์ž๋ณ„ ๋ฐฐ์ง€ ์กฐํšŒ) +- ๋ฐฐ์ง€ ํƒ€์ž… ์ •์˜ (๊ฑฐ๋ฆฌ, ์—ฐ์†, ์†๋„, ํŠน๋ณ„) +- Supabase ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ + +# Test Strategy: +- SQL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ +- RLS ์ •์ฑ… ํ…Œ์ŠคํŠธ (๊ถŒํ•œ ํ™•์ธ) +- ๋ฐ์ดํ„ฐ ์‚ฝ์ž…/์กฐํšŒ ํ…Œ์ŠคํŠธ diff --git a/.taskmaster/tasks/task_005_development.txt b/.taskmaster/tasks/task_005_development.txt new file mode 100644 index 0000000..a3ae394 --- /dev/null +++ b/.taskmaster/tasks/task_005_development.txt @@ -0,0 +1,17 @@ +# Task ID: 5 +# Title: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„œ๋น„์Šค ๊ณ„์ธต) +# Status: pending +# Dependencies: None +# Priority: high +# Description: AuthService, UserProfileService, LocationService ๋“ฑ ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. +# Details: +- Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ +- ๊ฐ ๋ฉ”์„œ๋“œ๋ณ„ ์ •์ƒ ์ผ€์ด์Šค, ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ +- ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ (๋นˆ ๊ฐ’, null, ๊ทน๋‹จ๊ฐ’) +- ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 90% ์ด์ƒ ๋ชฉํ‘œ +- CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ์ถ”๊ฐ€ + +# Test Strategy: +- TDD ์›์น™ ์ ์šฉ +- AAA(Arrange-Act-Assert) ํŒจํ„ด ์‚ฌ์šฉ +- ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด๊ณ  ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json new file mode 100644 index 0000000..71531aa --- /dev/null +++ b/.taskmaster/tasks/tasks.json @@ -0,0 +1,309 @@ +{ + "version": "1.0.0", + "tags": { + "master": { + "metadata": { + "name": "master", + "description": "Main development branch for StrideNote", + "createdAt": "2025-10-15T00:00:00.000Z", + "updatedAt": "2025-10-15T00:00:00.000Z" + }, + "tasks": [ + { + "id": 1, + "title": "HealthKit ์—ฐ๋™ ๊ตฌํ˜„ (iOS)", + "description": "iOS์—์„œ HealthKit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "high", + "details": "- HealthKit ๊ถŒํ•œ ์š”์ฒญ ๊ตฌํ˜„\n- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ ๋กœ์ง ์ž‘์„ฑ\n- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ๋งคํ•‘\n- ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘\n- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๋Œ€์‘", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: HealthKit API ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ๊ถŒํ•œ ๋ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ™•์ธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 2, + "title": "Google Fit ์—ฐ๋™ ๊ตฌํ˜„ (Android)", + "description": "Android์—์„œ Google Fit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "high", + "details": "- Google Fit API ๊ถŒํ•œ ์š”์ฒญ\n- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ\n- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”\n- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ๊ตฌํ˜„\n- ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ Fallback ์ฒ˜๋ฆฌ", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: Google Fit API ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ Android ๊ธฐ๊ธฐ์—์„œ ํ™•์ธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 3, + "title": "์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„ ๊ธฐ๋Šฅ", + "description": "์ˆ˜์ง‘๋œ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ํœด์‹, ์œ ์‚ฐ์†Œ, ๋ฌด์‚ฐ์†Œ ์กด์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ์‹ฌ๋ฐ•์ˆ˜ ์กด ๊ณ„์‚ฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ (์—ฐ๋ น ๊ธฐ๋ฐ˜)\n- ๋Ÿฌ๋‹ ์„ธ์…˜๋ณ„ ์กด๋ณ„ ์‹œ๊ฐ„ ๊ณ„์‚ฐ\n- ์กด ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ\n- UI์— ์กด๋ณ„ ๋น„์œจ ํ‘œ์‹œ (ํŒŒ์ด ์ฐจํŠธ ๋˜๋Š” ๋ฐ” ์ฐจํŠธ)\n- ์ตœ์  ํ›ˆ๋ จ ์กด ์ถ”์ฒœ ๋กœ์ง", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ์‹ฌ๋ฐ•์ˆ˜ ์กด ๊ณ„์‚ฐ ๋กœ์ง\n- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ์ฐจํŠธ ๋ Œ๋”๋ง\n- ์—ฃ์ง€ ์ผ€์ด์Šค: ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ์—†์„ ๋•Œ", + "dependencies": [ + 1, + 2 + ], + "subtasks": [] + }, + { + "id": 4, + "title": "๋ฐฐ์ง€ ์‹œ์Šคํ…œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„", + "description": "๋ฐฐ์ง€ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ RLS ์ •์ฑ…์„ ์„ค๊ณ„ํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "high", + "details": "- badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (id, name, description, criteria, icon)\n- user_badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (user_id, badge_id, earned_at)\n- RLS ์ •์ฑ… ์„ค์ • (์‚ฌ์šฉ์ž๋ณ„ ๋ฐฐ์ง€ ์กฐํšŒ)\n- ๋ฐฐ์ง€ ํƒ€์ž… ์ •์˜ (๊ฑฐ๋ฆฌ, ์—ฐ์†, ์†๋„, ํŠน๋ณ„)\n- Supabase ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ", + "testStrategy": "- SQL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ\n- RLS ์ •์ฑ… ํ…Œ์ŠคํŠธ (๊ถŒํ•œ ํ™•์ธ)\n- ๋ฐ์ดํ„ฐ ์‚ฝ์ž…/์กฐํšŒ ํ…Œ์ŠคํŠธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 5, + "title": "๋ฐฐ์ง€ ํš๋“ ๋กœ์ง ๊ตฌํ˜„", + "description": "๋Ÿฌ๋‹ ์„ธ์…˜ ์ข…๋ฃŒ ์‹œ ์กฐ๊ฑด์„ ํ™•์ธํ•˜์—ฌ ๋ฐฐ์ง€๋ฅผ ์ž๋™์œผ๋กœ ๋ถ€์—ฌํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- BadgeService ํด๋ž˜์Šค ์ž‘์„ฑ\n- ๋ฐฐ์ง€ ํš๋“ ์กฐ๊ฑด ์ฒดํฌ ๋กœ์ง (๊ฑฐ๋ฆฌ, ์—ฐ์†์„ฑ, ํŽ˜์ด์Šค)\n- ๋Ÿฌ๋‹ ์ข…๋ฃŒ ํ›„ ๋ฐฐ์ง€ ์ฒดํฌ ์ž๋™ ์‹คํ–‰\n- ์ƒˆ๋กœ์šด ๋ฐฐ์ง€ ํš๋“ ์‹œ ์•Œ๋ฆผ ํ‘œ์‹œ\n- ๋ฐฐ์ง€ ์ค‘๋ณต ๋ฐฉ์ง€ ๋กœ์ง", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๊ฐ ๋ฐฐ์ง€ ์กฐ๊ฑด ์ฒดํฌ ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ๋Ÿฌ๋‹ ์ข…๋ฃŒ ํ›„ ๋ฐฐ์ง€ ๋ถ€์—ฌ\n- ์—ฃ์ง€ ์ผ€์ด์Šค: ๋™์‹œ์— ์—ฌ๋Ÿฌ ๋ฐฐ์ง€ ํš๋“", + "dependencies": [ + 4 + ], + "subtasks": [] + }, + { + "id": 6, + "title": "๋ฐฐ์ง€ UI ๊ตฌํ˜„ (ํ”„๋กœํ•„ ํ™”๋ฉด)", + "description": "์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ํ™”๋ฉด์— ํš๋“ํ•œ ๋ฐฐ์ง€๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ํš๋“ ๊ฐ€๋Šฅํ•œ ๋ฐฐ์ง€ ๋ชฉ๋ก์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ๋ฐฐ์ง€ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„\n- ํš๋“ํ•œ ๋ฐฐ์ง€๋Š” ์ปฌ๋Ÿฌ๋กœ, ๋ฏธํš๋“์€ ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ๋กœ ํ‘œ์‹œ\n- ๋ฐฐ์ง€ ํด๋ฆญ ์‹œ ์ƒ์„ธ ์ •๋ณด ๋‹ค์ด์–ผ๋กœ๊ทธ\n- ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ (์˜ˆ: 100km ์ค‘ 75km ์™„๋ฃŒ)\n- ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ (์ƒˆ ๋ฐฐ์ง€ ํš๋“ ์‹œ)", + "testStrategy": "- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ๋ฐฐ์ง€ ๊ทธ๋ฆฌ๋“œ ๋ Œ๋”๋ง\n- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ\n- ์Šคํฌ๋ฆฐ์ƒท ํ…Œ์ŠคํŠธ", + "dependencies": [ + 5 + ], + "subtasks": [] + }, + { + "id": 7, + "title": "์Œ์„ฑ ์•ˆ๋‚ด ๊ธฐ๋Šฅ ๊ตฌํ˜„", + "description": "๋Ÿฌ๋‹ ์ค‘ 1km๋งˆ๋‹ค ๊ฑฐ๋ฆฌ, ํŽ˜์ด์Šค, ์‹œ๊ฐ„์„ ์Œ์„ฑ์œผ๋กœ ์•ˆ๋‚ดํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "high", + "details": "- Flutter TTS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ\n- 1km๋งˆ๋‹ค ์Œ์„ฑ ์•ˆ๋‚ด ํŠธ๋ฆฌ๊ฑฐ\n- ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํฌ๋งท (\"1ํ‚ฌ๋กœ๋ฏธํ„ฐ ์™„๋ฃŒ. ํŽ˜์ด์Šค 5๋ถ„ 30์ดˆ\")\n- ์‚ฌ์šฉ์ž ์„ค์ •์—์„œ ์Œ์„ฑ ์•ˆ๋‚ด ON/OFF ์˜ต์…˜\n- ์–ธ์–ด๋ณ„ ์Œ์„ฑ ์ง€์› (ํ•œ๊ตญ์–ด, ์˜์–ด)", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ… ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: TTS ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๋Ÿฌ๋‹ ์ค‘ ์Œ์„ฑ ํ™•์ธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 8, + "title": "์ฃผ๊ฐ„ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„", + "description": "์ฃผ๊ฐ„ ๋Ÿฌ๋‹ ํ†ต๊ณ„๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ์ฃผ๊ฐ„ ์ด ๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„, ์นผ๋กœ๋ฆฌ ์นด๋“œ ์œ„์ ฏ\n- ์ผ๋ณ„ ๋Ÿฌ๋‹ ๊ฑฐ๋ฆฌ ๋ฐ” ์ฐจํŠธ (FL Chart ์‚ฌ์šฉ)\n- ํ‰๊ท  ํŽ˜์ด์Šค ์ถ”์ด ๋ผ์ธ ์ฐจํŠธ\n- ์ด๋ฒˆ ์ฃผ vs ์ง€๋‚œ ์ฃผ ๋น„๊ต\n- ์ฃผ๊ฐ„ ๋ชฉํ‘œ ๋Œ€๋น„ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ", + "testStrategy": "- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ์ฐจํŠธ ๋ Œ๋”๋ง\n- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ํ†ต๊ณ„ ๊ณ„์‚ฐ ๋กœ์ง\n- ๋ฐ์ดํ„ฐ ์—†์„ ๋•Œ Empty State", + "dependencies": [], + "subtasks": [] + }, + { + "id": 9, + "title": "์›”๊ฐ„ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„", + "description": "์›”๊ฐ„ ๋Ÿฌ๋‹ ํ†ต๊ณ„๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ์›”๊ฐ„ ์ด ๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„, ์นผ๋กœ๋ฆฌ ์š”์•ฝ\n- ์ฃผ์ฐจ๋ณ„ ๊ฑฐ๋ฆฌ ๋ฐ” ์ฐจํŠธ\n- ์›”๊ฐ„ ํŽ˜์ด์Šค ์ถ”์ด\n- ์ด๋ฒˆ ๋‹ฌ vs ์ง€๋‚œ ๋‹ฌ ๋น„๊ต\n- ์›”๊ฐ„ ์ตœ๊ณ  ๊ธฐ๋ก (๊ฐ€์žฅ ๊ธด ๊ฑฐ๋ฆฌ, ๊ฐ€์žฅ ๋น ๋ฅธ ํŽ˜์ด์Šค)", + "testStrategy": "- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ์ฐจํŠธ ๋ฐ ์นด๋“œ ๋ Œ๋”๋ง\n- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ์›”๊ฐ„ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„\n- ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ: ์›” ์‹œ์ž‘/๋", + "dependencies": [], + "subtasks": [] + }, + { + "id": 10, + "title": "๊ฐœ์ธ ์ตœ๊ณ  ๊ธฐ๋ก(PR) ํ‘œ์‹œ ๊ธฐ๋Šฅ", + "description": "์‚ฌ์šฉ์ž์˜ ๊ฐœ์ธ ์ตœ๊ณ  ๊ธฐ๋ก์„ ์ถ”์ ํ•˜๊ณ  ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "low", + "details": "- ์ตœ์žฅ ๊ฑฐ๋ฆฌ, ์ตœ์žฅ ์‹œ๊ฐ„, ์ตœ๊ณ  ํŽ˜์ด์Šค ์ถ”์ \n- PR ๋‹ฌ์„ฑ ์‹œ ์ถ•ํ•˜ ์• ๋‹ˆ๋ฉ”์ด์…˜\n- ํžˆ์Šคํ† ๋ฆฌ ํ™”๋ฉด์— PR ์•„์ด์ฝ˜ ํ‘œ์‹œ\n- PR ๊ธฐ๋ก ์ƒ์„ธ ๋ณด๊ธฐ (๋‚ ์งœ, ๊ธฐ๋ก ๋“ฑ)\n- SharedPreferences์— PR ๋ฐ์ดํ„ฐ ์บ์‹ฑ", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: PR ๋น„๊ต ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: PR ์—…๋ฐ์ดํŠธ ๋ฐ ์ €์žฅ\n- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: PR ์• ๋‹ˆ๋ฉ”์ด์…˜", + "dependencies": [], + "subtasks": [] + }, + { + "id": 11, + "title": "๋ชฉํ‘œ ์„ค์ • ๊ธฐ๋Šฅ", + "description": "์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๊ฐ„/์›”๊ฐ„ ๋Ÿฌ๋‹ ๋ชฉํ‘œ๋ฅผ ์„ค์ •ํ•˜๊ณ  ์ง„ํ–‰๋ฅ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ๋ชฉํ‘œ ์„ค์ • UI (๊ฑฐ๋ฆฌ ๋˜๋Š” ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜)\n- ๋ชฉํ‘œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ\n- ํ™ˆ ํ™”๋ฉด์— ๋ชฉํ‘œ ์ง„ํ–‰๋ฅ  ์œ„์ ฏ\n- ๋ชฉํ‘œ ๋‹ฌ์„ฑ ์‹œ ์•Œ๋ฆผ ๋ฐ ์ถ•ํ•˜ ๋ฉ”์‹œ์ง€\n- ๋ชฉํ‘œ ํŽธ์ง‘/์‚ญ์ œ ๊ธฐ๋Šฅ", + "testStrategy": "- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ๋ชฉํ‘œ ์„ค์ • ํผ\n- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ๋ชฉํ‘œ ์ €์žฅ ๋ฐ ์กฐํšŒ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 12, + "title": "AuthService ๋ฆฌํŒฉํ„ฐ๋ง (์˜์กด์„ฑ ์ฃผ์ž…)", + "description": "AuthService์— ์˜์กด์„ฑ ์ฃผ์ž… ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- SupabaseClient๋ฅผ ์ƒ์„ฑ์ž๋กœ ์ฃผ์ž…๋ฐ›๋„๋ก ์ˆ˜์ •\n- ํ•˜๋“œ์ฝ”๋”ฉ๋œ Supabase.instance.client ์ œ๊ฑฐ\n- Provider ๋˜๋Š” GetIt์„ ์‚ฌ์šฉํ•œ ์˜์กด์„ฑ ๊ด€๋ฆฌ\n- Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ\n- ๊ธฐ์กด ์ฝ”๋“œ์™€์˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: Mock SupabaseClient ์‚ฌ์šฉ\n- ๋ฆฌํŒฉํ„ฐ๋ง ํ›„ ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ „๋ถ€ ํ†ต๊ณผ\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ์ธ์ฆ ํ”Œ๋กœ์šฐ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 13, + "title": "UserProfileService ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„ ", + "description": "UserProfileService์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- try-catch ๋ธ”๋ก ์ถ”๊ฐ€ ๋ฐ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ\n- ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, ๊ถŒํ•œ ์˜ค๋ฅ˜, ๋ฐ์ดํ„ฐ ์˜ค๋ฅ˜ ๊ตฌ๋ถ„\n- ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ •์˜\n- ์—๋Ÿฌ ๋กœ๊น… (๋””๋ฒ„๊ทธ ๋ชจ๋“œ)\n- Retry ๋กœ์ง ์ถ”๊ฐ€ (๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ)", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๊ฐ ์—๋Ÿฌ ์‹œ๋‚˜๋ฆฌ์˜ค\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ๋„คํŠธ์›Œํฌ ์˜คํ”„๋ผ์ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\n- ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ…Œ์ŠคํŠธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 14, + "title": "์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ถ„๋ฆฌ", + "description": "ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ์ค‘๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ฐพ์•„ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "low", + "details": "- ์Šค๋‚ต๋ฐ” ํ‘œ์‹œ ์ฝ”๋“œ๋ฅผ SnackBarUtils๋กœ ๋ถ„๋ฆฌ\n- ๋‚ ์งœ/์‹œ๊ฐ„ ํฌ๋งทํŒ… ํ•จ์ˆ˜ ํ†ต์ผ\n- ๊ฑฐ๋ฆฌ/ํŽ˜์ด์Šค ๊ณ„์‚ฐ ๋กœ์ง ๊ณตํ†ตํ™”\n- ์ƒ์ˆ˜ ๊ฐ’ ์ถ”์ถœ (ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ˆซ์ž, ๋ฌธ์ž์—ด)\n- ์ฝ”๋“œ ์ •์  ๋ถ„์„ ๋„๊ตฌ ์‹คํ–‰ (flutter analyze)", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜\n- ๋ฆฌํŒฉํ„ฐ๋ง ํ›„ ์ „์ฒด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ\n- ์ฝ”๋“œ ๋ฆฌ๋ทฐ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 15, + "title": "๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„œ๋น„์Šค ๊ณ„์ธต)", + "description": "AuthService, UserProfileService, LocationService ๋“ฑ ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "high", + "details": "- Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ\n- ๊ฐ ๋ฉ”์„œ๋“œ๋ณ„ ์ •์ƒ ์ผ€์ด์Šค, ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ\n- ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ (๋นˆ ๊ฐ’, null, ๊ทน๋‹จ๊ฐ’)\n- ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 90% ์ด์ƒ ๋ชฉํ‘œ\n- CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ์ถ”๊ฐ€", + "testStrategy": "- TDD ์›์น™ ์ ์šฉ\n- AAA(Arrange-Act-Assert) ํŒจํ„ด ์‚ฌ์šฉ\n- ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด๊ณ  ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ", + "dependencies": [ + 12, + 13 + ], + "subtasks": [] + }, + { + "id": 16, + "title": "์œ„์ ฏ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์ฃผ์š” ํ™”๋ฉด)", + "description": "ํ™ˆ ํ™”๋ฉด, ๋Ÿฌ๋‹ ํ™”๋ฉด, ํžˆ์Šคํ† ๋ฆฌ ํ™”๋ฉด ๋“ฑ ์ฃผ์š” UI์˜ ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ํ™”๋ฉด ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ\n- ๋ฒ„ํŠผ ํด๋ฆญ ๋ฐ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ํ…Œ์ŠคํŠธ\n- ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ฅธ UI ์—…๋ฐ์ดํŠธ ํ…Œ์ŠคํŠธ\n- Empty State ๋ฐ Error State ํ…Œ์ŠคํŠธ\n- ์Šคํฌ๋ฆฐ์ƒท ํ…Œ์ŠคํŠธ (๊ณจ๋“  ํ…Œ์ŠคํŠธ)", + "testStrategy": "- pumpWidget ๋ฐ pump/pumpAndSettle ์‚ฌ์šฉ\n- find.byType, find.text ๋“ฑ์œผ๋กœ ์œ„์ ฏ ์ฐพ๊ธฐ\n- expect๋กœ ์œ„์ ฏ ์กด์žฌ ๋ฐ ์ƒํƒœ ๊ฒ€์ฆ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 17, + "title": "ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์ธ์ฆ ํ”Œ๋กœ์šฐ)", + "description": "๋กœ๊ทธ์ธ๋ถ€ํ„ฐ ๋Ÿฌ๋‹ ๊ธฐ๋ก๊นŒ์ง€ ์ „์ฒด ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ์ด๋ฉ”์ผ ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ\n- Google ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ (Mock)\n- ๋กœ๊ทธ์ธ ํ›„ ํ™ˆ ํ™”๋ฉด ์ด๋™ ํ™•์ธ\n- ๋Ÿฌ๋‹ ์‹œ์ž‘ โ†’ ์ข…๋ฃŒ โ†’ ์ €์žฅ ํ”Œ๋กœ์šฐ\n- ๋กœ๊ทธ์•„์›ƒ ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ", + "testStrategy": "- integration_test ํŒจํ‚ค์ง€ ์‚ฌ์šฉ\n- ์‹ค์ œ ์•ฑ ์‹คํ–‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\n- ๋„คํŠธ์›Œํฌ ์š”์ฒญ์€ Mock ๋˜๋Š” ํ…Œ์ŠคํŠธ ์„œ๋ฒ„ ์‚ฌ์šฉ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 18, + "title": "GPS ์ •ํ™•๋„ ๊ฐœ์„  ๋ฐ ์ตœ์ ํ™”", + "description": "GPS ์œ„์น˜ ์ถ”์ ์˜ ์ •ํ™•๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ณ  ๋ฐฐํ„ฐ๋ฆฌ ํšจ์œจ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "medium", + "details": "- ์œ„์น˜ ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ ์กฐ์ • (5-10์ดˆ)\n- ์œ„์น˜ ์ •ํ™•๋„ ํ•„ํ„ฐ๋ง (๋ถ€์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ)\n- ์‹ค๋‚ด/ํ„ฐ๋„์—์„œ ์‹ ํ˜ธ ์•ฝํ•  ๋•Œ ์ฒ˜๋ฆฌ\n- ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ ๋ชจ๋“œ ์˜ต์…˜\n- ๊ฐ€์†๋„๊ณ„ ๋ฐ์ดํ„ฐ๋กœ ๋ณด์ • (์„ ํƒ์‚ฌํ•ญ)", + "testStrategy": "- ์‹ค์ œ ๋Ÿฌ๋‹ ์ค‘ ํ…Œ์ŠคํŠธ (์‹ค๋‚ด/์‹ค์™ธ)\n- ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ๋Ÿ‰ ์ธก์ •\n- ๊ฑฐ๋ฆฌ ์ •ํ™•๋„ ๊ฒ€์ฆ (์‹ค์ œ ๊ฑฐ๋ฆฌ์™€ ๋น„๊ต)", + "dependencies": [], + "subtasks": [] + }, + { + "id": 19, + "title": "์˜คํ”„๋ผ์ธ ๋ชจ๋“œ ๊ฐœ์„ ", + "description": "๋„คํŠธ์›Œํฌ ์—†์ด๋„ ์•ฑ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•˜๋„๋ก ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "low", + "details": "- ๋Ÿฌ๋‹ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ์— ๋จผ์ € ์ €์žฅ\n- ๋„คํŠธ์›Œํฌ ๋ณต๊ตฌ ์‹œ ์ž๋™ ๋™๊ธฐํ™”\n- ๋™๊ธฐํ™” ์ƒํƒœ UI ํ‘œ์‹œ (๋™๊ธฐํ™” ์ค‘, ๋™๊ธฐํ™” ์™„๋ฃŒ)\n- ์ถฉ๋Œ ํ•ด๊ฒฐ ๋กœ์ง (๋กœ์ปฌ vs ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ)\n- ์˜คํ”„๋ผ์ธ ์ƒํƒœ ์•Œ๋ฆผ", + "testStrategy": "- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ์˜คํ”„๋ผ์ธ โ†’ ์˜จ๋ผ์ธ ์ „ํ™˜\n- ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๊ฒ€์ฆ\n- ๋™๊ธฐํ™” ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ", + "dependencies": [], + "subtasks": [] + }, + { + "id": 20, + "title": "์‚ฌ์šฉ์ž ์„ค์ • ํ™”๋ฉด ๊ฐœ์„ ", + "description": "์•ฑ ์„ค์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž ์„ค์ • ํ™”๋ฉด์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "priority": "low", + "details": "- ์Œ์„ฑ ์•ˆ๋‚ด ON/OFF\n- ๊ฑฐ๋ฆฌ ๋‹จ์œ„ (km/mile)\n- ๋ชฉํ‘œ ์„ค์ • ๋ฐ”๋กœ๊ฐ€๊ธฐ\n- ์•Œ๋ฆผ ์„ค์ •\n- ํ…Œ๋งˆ ์„ค์ • (๋ผ์ดํŠธ/๋‹คํฌ ๋ชจ๋“œ)\n- ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์˜ต์…˜\n- ์•ฑ ์ •๋ณด (๋ฒ„์ „, ๋ผ์ด์„ ์Šค)", + "testStrategy": "- ์œ„์ ฏ ํ…Œ์ŠคํŠธ: ์„ค์ • ํ™”๋ฉด ๋ Œ๋”๋ง\n- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ์„ค์ • ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ\n- ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ…Œ์ŠคํŠธ", + "dependencies": [], + "subtasks": [] + } + ] + } + }, + "development": { + "tasks": [ + { + "id": 1, + "title": "HealthKit ์—ฐ๋™ ๊ตฌํ˜„ (iOS)", + "description": "iOS์—์„œ HealthKit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "details": "- HealthKit ๊ถŒํ•œ ์š”์ฒญ ๊ตฌํ˜„\n- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ ๋กœ์ง ์ž‘์„ฑ\n- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ ๋งคํ•‘\n- ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘\n- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๋Œ€์‘", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: HealthKit API ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ๊ถŒํ•œ ๋ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ™•์ธ", + "status": "done", + "dependencies": [], + "priority": "high", + "subtasks": [] + }, + { + "id": 2, + "title": "Google Fit ์—ฐ๋™ ๊ตฌํ˜„ (Android)", + "description": "Android์—์„œ Google Fit์„ ์—ฐ๋™ํ•˜์—ฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "details": "- Google Fit API ๊ถŒํ•œ ์š”์ฒญ\n- ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ํ™œ๋™ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ\n- ๋Ÿฌ๋‹ ์„ธ์…˜๊ณผ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”\n- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ๊ตฌํ˜„\n- ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ Fallback ์ฒ˜๋ฆฌ", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: Google Fit API ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ Android ๊ธฐ๊ธฐ์—์„œ ํ™•์ธ", + "status": "pending", + "dependencies": [], + "priority": "high", + "subtasks": [] + }, + { + "id": 3, + "title": "์Œ์„ฑ ์•ˆ๋‚ด ๊ธฐ๋Šฅ ๊ตฌํ˜„", + "description": "๋Ÿฌ๋‹ ์ค‘ 1km๋งˆ๋‹ค ๊ฑฐ๋ฆฌ, ํŽ˜์ด์Šค, ์‹œ๊ฐ„์„ ์Œ์„ฑ์œผ๋กœ ์•ˆ๋‚ดํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "details": "- Flutter TTS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ\n- 1km๋งˆ๋‹ค ์Œ์„ฑ ์•ˆ๋‚ด ํŠธ๋ฆฌ๊ฑฐ\n- ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํฌ๋งท (\"1ํ‚ฌ๋กœ๋ฏธํ„ฐ ์™„๋ฃŒ. ํŽ˜์ด์Šค 5๋ถ„ 30์ดˆ\")\n- ์‚ฌ์šฉ์ž ์„ค์ •์—์„œ ์Œ์„ฑ ์•ˆ๋‚ด ON/OFF ์˜ต์…˜\n- ์–ธ์–ด๋ณ„ ์Œ์„ฑ ์ง€์› (ํ•œ๊ตญ์–ด, ์˜์–ด)", + "testStrategy": "- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ… ๋กœ์ง\n- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: TTS ํ˜ธ์ถœ\n- ์ˆ˜๋™ ํ…Œ์ŠคํŠธ: ์‹ค์ œ ๋Ÿฌ๋‹ ์ค‘ ์Œ์„ฑ ํ™•์ธ", + "status": "pending", + "dependencies": [], + "priority": "high", + "subtasks": [] + }, + { + "id": 4, + "title": "๋ฐฐ์ง€ ์‹œ์Šคํ…œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„", + "description": "๋ฐฐ์ง€ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ RLS ์ •์ฑ…์„ ์„ค๊ณ„ํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "details": "- badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (id, name, description, criteria, icon)\n- user_badges ํ…Œ์ด๋ธ” ์ƒ์„ฑ (user_id, badge_id, earned_at)\n- RLS ์ •์ฑ… ์„ค์ • (์‚ฌ์šฉ์ž๋ณ„ ๋ฐฐ์ง€ ์กฐํšŒ)\n- ๋ฐฐ์ง€ ํƒ€์ž… ์ •์˜ (๊ฑฐ๋ฆฌ, ์—ฐ์†, ์†๋„, ํŠน๋ณ„)\n- Supabase ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ž‘์„ฑ", + "testStrategy": "- SQL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ\n- RLS ์ •์ฑ… ํ…Œ์ŠคํŠธ (๊ถŒํ•œ ํ™•์ธ)\n- ๋ฐ์ดํ„ฐ ์‚ฝ์ž…/์กฐํšŒ ํ…Œ์ŠคํŠธ", + "status": "pending", + "dependencies": [], + "priority": "high", + "subtasks": [] + }, + { + "id": 5, + "title": "๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„œ๋น„์Šค ๊ณ„์ธต)", + "description": "AuthService, UserProfileService, LocationService ๋“ฑ ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "details": "- Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ ์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ\n- ๊ฐ ๋ฉ”์„œ๋“œ๋ณ„ ์ •์ƒ ์ผ€์ด์Šค, ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ\n- ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ (๋นˆ ๊ฐ’, null, ๊ทน๋‹จ๊ฐ’)\n- ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 90% ์ด์ƒ ๋ชฉํ‘œ\n- CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ์ถ”๊ฐ€", + "testStrategy": "- TDD ์›์น™ ์ ์šฉ\n- AAA(Arrange-Act-Assert) ํŒจํ„ด ์‚ฌ์šฉ\n- ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด๊ณ  ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ", + "status": "pending", + "dependencies": [], + "priority": "high", + "subtasks": [] + } + ], + "metadata": { + "created": "2025-10-16T01:02:19.230Z", + "updated": "2025-10-16T01:10:47.361Z", + "description": "Main development branch for StrideNote" + } + } +} \ No newline at end of file diff --git a/.taskmaster/templates/example_prd.txt b/.taskmaster/templates/example_prd.txt new file mode 100644 index 0000000..194114d --- /dev/null +++ b/.taskmaster/templates/example_prd.txt @@ -0,0 +1,47 @@ + +# Overview +[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] + +# Core Features +[List and describe the main features of your product. For each feature, include: +- What it does +- Why it's important +- How it works at a high level] + +# User Experience +[Describe the user journey and experience. Include: +- User personas +- Key user flows +- UI/UX considerations] + + +# Technical Architecture +[Outline the technical implementation details: +- System components +- Data models +- APIs and integrations +- Infrastructure requirements] + +# Development Roadmap +[Break down the development process into phases: +- MVP requirements +- Future enhancements +- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] + +# Logical Dependency Chain +[Define the logical order of development: +- Which features need to be built first (foundation) +- Getting as quickly as possible to something usable/visible front end that works +- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] + +# Risks and Mitigations +[Identify potential risks and how they'll be addressed: +- Technical challenges +- Figuring out the MVP that we can build upon +- Resource constraints] + +# Appendix +[Include any additional information: +- Research findings +- Technical specifications] + \ No newline at end of file diff --git a/.taskmaster/templates/example_prd_rpg.txt b/.taskmaster/templates/example_prd_rpg.txt new file mode 100644 index 0000000..5ad908f --- /dev/null +++ b/.taskmaster/templates/example_prd_rpg.txt @@ -0,0 +1,511 @@ + +# Repository Planning Graph (RPG) Method - PRD Template + +This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies. + +## Core Principles + +1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them +2. **Explicit Dependencies**: Never assume - always state what depends on what +3. **Topological Order**: Build foundation first, then layers on top +4. **Progressive Refinement**: Start broad, refine iteratively + +## How to Use This Template + +- Follow the instructions in each `` block +- Look at `` blocks to see good vs bad patterns +- Fill in the content sections with your project details +- The AI reading this will learn the RPG method by following along +- Task Master will parse the resulting PRD into dependency-aware tasks + +## Recommended Tools for Creating PRDs + +When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results: + +**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points. + +**Recommended tools:** +- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts +- **Cursor/Windsurf** - IDE integration with full codebase context +- **Gemini CLI** (gemini-cli) - Massive context window for large codebases +- **Codex/Grok CLI** - Strong code generation with context awareness + +**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase. + + +--- + + + +Start with the problem, not the solution. Be specific about: +- What pain point exists? +- Who experiences it? +- Why existing solutions don't work? +- What success looks like (measurable outcomes)? + +Keep this section focused - don't jump into implementation details yet. + + +## Problem Statement +[Describe the core problem. Be concrete about user pain points.] + +## Target Users +[Define personas, their workflows, and what they're trying to achieve.] + +## Success Metrics +[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"] + + + +--- + + + +Now think about CAPABILITIES (what the system DOES), not code structure yet. + +Step 1: Identify high-level capability domains +- Think: "What major things does this system do?" +- Examples: Data Management, Core Processing, Presentation Layer + +Step 2: For each capability, enumerate specific features +- Use explore-exploit strategy: + * Exploit: What features are REQUIRED for core value? + * Explore: What features make this domain COMPLETE? + +Step 3: For each feature, define: +- Description: What it does in one sentence +- Inputs: What data/context it needs +- Outputs: What it produces/returns +- Behavior: Key logic or transformations + + +Capability: Data Validation + Feature: Schema validation + - Description: Validate JSON payloads against defined schemas + - Inputs: JSON object, schema definition + - Outputs: Validation result (pass/fail) + error details + - Behavior: Iterate fields, check types, enforce constraints + + Feature: Business rule validation + - Description: Apply domain-specific validation rules + - Inputs: Validated data object, rule set + - Outputs: Boolean + list of violated rules + - Behavior: Execute rules sequentially, short-circuit on failure + + + +Capability: validation.js + (Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.) + +Capability: Validation + Feature: Make sure data is good + (Problem: Too vague. No inputs/outputs. Not actionable.) + + + +## Capability Tree + +### Capability: [Name] +[Brief description of what this capability domain covers] + +#### Feature: [Name] +- **Description**: [One sentence] +- **Inputs**: [What it needs] +- **Outputs**: [What it produces] +- **Behavior**: [Key logic] + +#### Feature: [Name] +- **Description**: +- **Inputs**: +- **Outputs**: +- **Behavior**: + +### Capability: [Name] +... + + + +--- + + + +NOW think about code organization. Map capabilities to actual file/folder structure. + +Rules: +1. Each capability maps to a module (folder or file) +2. Features within a capability map to functions/classes +3. Use clear module boundaries - each module has ONE responsibility +4. Define what each module exports (public interface) + +The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural). + + +Capability: Data Validation + โ†’ Maps to: src/validation/ + โ”œโ”€โ”€ schema-validator.js (Schema validation feature) + โ”œโ”€โ”€ rule-validator.js (Business rule validation feature) + โ””โ”€โ”€ index.js (Public exports) + +Exports: + - validateSchema(data, schema) + - validateRules(data, rules) + + + +Capability: Data Validation + โ†’ Maps to: src/utils.js + (Problem: "utils" is not a clear module boundary. Where do I find validation logic?) + +Capability: Data Validation + โ†’ Maps to: src/validation/everything.js + (Problem: One giant file. Features should map to separate files for maintainability.) + + + +## Repository Structure + +``` +project-root/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ [module-name]/ # Maps to: [Capability Name] +โ”‚ โ”‚ โ”œโ”€โ”€ [file].js # Maps to: [Feature Name] +โ”‚ โ”‚ โ””โ”€โ”€ index.js # Public exports +โ”‚ โ””โ”€โ”€ [module-name]/ +โ”œโ”€โ”€ tests/ +โ””โ”€โ”€ docs/ +``` + +## Module Definitions + +### Module: [Name] +- **Maps to capability**: [Capability from functional decomposition] +- **Responsibility**: [Single clear purpose] +- **File structure**: + ``` + module-name/ + โ”œโ”€โ”€ feature1.js + โ”œโ”€โ”€ feature2.js + โ””โ”€โ”€ index.js + ``` +- **Exports**: + - `functionName()` - [what it does] + - `ClassName` - [what it does] + + + +--- + + + +This is THE CRITICAL SECTION for Task Master parsing. + +Define explicit dependencies between modules. This creates the topological order for task execution. + +Rules: +1. List modules in dependency order (foundation first) +2. For each module, state what it depends on +3. Foundation modules should have NO dependencies +4. Every non-foundation module should depend on at least one other module +5. Think: "What must EXIST before I can build this module?" + + +Foundation Layer (no dependencies): + - error-handling: No dependencies + - config-manager: No dependencies + - base-types: No dependencies + +Data Layer: + - schema-validator: Depends on [base-types, error-handling] + - data-ingestion: Depends on [schema-validator, config-manager] + +Core Layer: + - algorithm-engine: Depends on [base-types, error-handling] + - pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion] + + + +- validation: Depends on API +- API: Depends on validation +(Problem: Circular dependency. This will cause build/runtime issues.) + +- user-auth: Depends on everything +(Problem: Too many dependencies. Should be more focused.) + + + +## Dependency Chain + +### Foundation Layer (Phase 0) +No dependencies - these are built first. + +- **[Module Name]**: [What it provides] +- **[Module Name]**: [What it provides] + +### [Layer Name] (Phase 1) +- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]] +- **[Module Name]**: Depends on [[module-from-phase-0]] + +### [Layer Name] (Phase 2) +- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]] + +[Continue building up layers...] + + + +--- + + + +Turn the dependency graph into concrete development phases. + +Each phase should: +1. Have clear entry criteria (what must exist before starting) +2. Contain tasks that can be parallelized (no inter-dependencies within phase) +3. Have clear exit criteria (how do we know phase is complete?) +4. Build toward something USABLE (not just infrastructure) + +Phase ordering follows topological sort of dependency graph. + + +Phase 0: Foundation + Entry: Clean repository + Tasks: + - Implement error handling utilities + - Create base type definitions + - Setup configuration system + Exit: Other modules can import foundation without errors + +Phase 1: Data Layer + Entry: Phase 0 complete + Tasks: + - Implement schema validator (uses: base types, error handling) + - Build data ingestion pipeline (uses: validator, config) + Exit: End-to-end data flow from input to validated output + + + +Phase 1: Build Everything + Tasks: + - API + - Database + - UI + - Tests + (Problem: No clear focus. Too broad. Dependencies not considered.) + + + +## Development Phases + +### Phase 0: [Foundation Name] +**Goal**: [What foundational capability this establishes] + +**Entry Criteria**: [What must be true before starting] + +**Tasks**: +- [ ] [Task name] (depends on: [none or list]) + - Acceptance criteria: [How we know it's done] + - Test strategy: [What tests prove it works] + +- [ ] [Task name] (depends on: [none or list]) + +**Exit Criteria**: [Observable outcome that proves phase complete] + +**Delivers**: [What can users/developers do after this phase?] + +--- + +### Phase 1: [Layer Name] +**Goal**: + +**Entry Criteria**: Phase 0 complete + +**Tasks**: +- [ ] [Task name] (depends on: [[tasks-from-phase-0]]) +- [ ] [Task name] (depends on: [[tasks-from-phase-0]]) + +**Exit Criteria**: + +**Delivers**: + +--- + +[Continue with more phases...] + + + +--- + + + +Define how testing will be integrated throughout development (TDD approach). + +Specify: +1. Test pyramid ratios (unit vs integration vs e2e) +2. Coverage requirements +3. Critical test scenarios +4. Test generation guidelines for Surgical Test Generator + +This section guides the AI when generating tests during the RED phase of TDD. + + +Critical Test Scenarios for Data Validation module: + - Happy path: Valid data passes all checks + - Edge cases: Empty strings, null values, boundary numbers + - Error cases: Invalid types, missing required fields + - Integration: Validator works with ingestion pipeline + + + +## Test Pyramid + +``` + /\ + /E2E\ โ† [X]% (End-to-end, slow, comprehensive) + /------\ + /Integration\ โ† [Y]% (Module interactions) + /------------\ + / Unit Tests \ โ† [Z]% (Fast, isolated, deterministic) + /----------------\ +``` + +## Coverage Requirements +- Line coverage: [X]% minimum +- Branch coverage: [X]% minimum +- Function coverage: [X]% minimum +- Statement coverage: [X]% minimum + +## Critical Test Scenarios + +### [Module/Feature Name] +**Happy path**: +- [Scenario description] +- Expected: [What should happen] + +**Edge cases**: +- [Scenario description] +- Expected: [What should happen] + +**Error cases**: +- [Scenario description] +- Expected: [How system handles failure] + +**Integration points**: +- [What interactions to test] +- Expected: [End-to-end behavior] + +## Test Generation Guidelines +[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions] + + + +--- + + + +Describe technical architecture, data models, and key design decisions. + +Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure. + + +## System Components +[Major architectural pieces and their responsibilities] + +## Data Models +[Core data structures, schemas, database design] + +## Technology Stack +[Languages, frameworks, key libraries] + +**Decision: [Technology/Pattern]** +- **Rationale**: [Why chosen] +- **Trade-offs**: [What we're giving up] +- **Alternatives considered**: [What else we looked at] + + + +--- + + + +Identify risks that could derail development and how to mitigate them. + +Categories: +- Technical risks (complexity, unknowns) +- Dependency risks (blocking issues) +- Scope risks (creep, underestimation) + + +## Technical Risks +**Risk**: [Description] +- **Impact**: [High/Medium/Low - effect on project] +- **Likelihood**: [High/Medium/Low] +- **Mitigation**: [How to address] +- **Fallback**: [Plan B if mitigation fails] + +## Dependency Risks +[External dependencies, blocking issues] + +## Scope Risks +[Scope creep, underestimation, unclear requirements] + + + +--- + + +## References +[Papers, documentation, similar systems] + +## Glossary +[Domain-specific terms] + +## Open Questions +[Things to resolve during development] + + +--- + + +# How Task Master Uses This PRD + +When you run `task-master parse-prd .txt`, the parser: + +1. **Extracts capabilities** โ†’ Main tasks + - Each `### Capability:` becomes a top-level task + +2. **Extracts features** โ†’ Subtasks + - Each `#### Feature:` becomes a subtask under its capability + +3. **Parses dependencies** โ†’ Task dependencies + - `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"] + +4. **Orders by phases** โ†’ Task priorities + - Phase 0 tasks = highest priority + - Phase N tasks = lower priority, properly sequenced + +5. **Uses test strategy** โ†’ Test generation context + - Feeds test scenarios to Surgical Test Generator during implementation + +**Result**: A dependency-aware task graph that can be executed in topological order. + +## Why RPG Structure Matters + +Traditional flat PRDs lead to: +- โŒ Unclear task dependencies +- โŒ Arbitrary task ordering +- โŒ Circular dependencies discovered late +- โŒ Poorly scoped tasks + +RPG-structured PRDs provide: +- โœ… Explicit dependency chains +- โœ… Topological execution order +- โœ… Clear module boundaries +- โœ… Validated task graph before implementation + +## Tips for Best Results + +1. **Spend time on dependency graph** - This is the most valuable section for Task Master +2. **Keep features atomic** - Each feature should be independently testable +3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks +4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation + diff --git a/CRASH_FIX.md b/CRASH_FIX.md deleted file mode 100644 index ab9487f..0000000 --- a/CRASH_FIX.md +++ /dev/null @@ -1,189 +0,0 @@ -# Google Sign-In ํฌ๋ž˜์‹œ ์ˆ˜์ • - -## ๐Ÿšจ ํฌ๋ž˜์‹œ ์ฆ์ƒ - -``` -Exception Type: EXC_CRASH (SIGABRT) -Last Exception Backtrace: -3 GoogleSignIn -[GIDSignIn signInWithOptions:] + 152 -``` - -iOS์—์„œ Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์•ฑ์ด ์ฆ‰์‹œ ํฌ๋ž˜์‹œ๋ฉ๋‹ˆ๋‹ค. - -## ๐Ÿ” ์›์ธ ๋ถ„์„ - -### ๋ฌธ์ œ์  - -1. **GoogleSignIn ์ดˆ๊ธฐํ™” ๋ถ€์กฑ**: `serverClientId`๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์Œ -2. **Info.plist ์„ค์ • ๋ถˆ์™„์ „**: `GIDClientID`๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑ -3. **Supabase ID Token ์ธ์ฆ ์‹คํŒจ**: Google Sign-In SDK๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ID Token์„ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ - -### ํฌ๋ž˜์‹œ ๋ฐœ์ƒ ์‹œ์  - -```dart -// Google Sign-In์„ ์‹œ๋„ํ•  ๋•Œ -final googleUser = await _googleSignIn.signIn(); -// โ†‘ ์—ฌ๊ธฐ์„œ ํฌ๋ž˜์‹œ ๋ฐœ์ƒ -``` - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### 1. GoogleSignIn ์ดˆ๊ธฐํ™”์— serverClientId ์ถ”๊ฐ€ - -**Before:** - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], -); -``` - -**After:** - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - // iOS์—์„œ serverClientId ์ง€์ • (Supabase Web OAuth Client ID) - serverClientId: kIsWeb - ? null - : 'YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com', -); -``` - -### 2. Info.plist ํ™•์ธ - -๋‹ค์Œ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ: - -```xml - -GIDClientID -YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com - - -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.YOUR-GOOGLE-CLIENT-ID - - - -``` - -### 3. serverClientId vs clientId ์ฐจ์ด - -#### clientId (GIDClientID in Info.plist) - -- **iOS ์•ฑ์šฉ** Google OAuth Client ID -- Google Cloud Console > iOS Application์—์„œ ์ƒ์„ฑ -- Bundle ID: `com.example.runnerApp` - -#### serverClientId (์ฝ”๋“œ์— ์ง€์ •) - -- **๋ฐฑ์—”๋“œ(Supabase)์šฉ** Google OAuth Client ID -- Google Cloud Console > Web Application์—์„œ ์ƒ์„ฑ -- Supabase์—์„œ ID Token ๊ฒ€์ฆ์— ์‚ฌ์šฉ - -## ๐Ÿ”ง Google Cloud Console ์„ค์ • - -### ํ•„์š”ํ•œ OAuth ํด๋ผ์ด์–ธํŠธ - -#### 1. iOS Application (for GIDClientID) - -``` -Application type: iOS -Bundle ID: com.example.runnerApp -Client ID: YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -``` - -#### 2. Web Application (for serverClientId) - -``` -Application type: Web application -Name: StrideNote Web (Supabase) -Authorized redirect URIs: - - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback -Client ID: (Supabase์— ์„ค์ •ํ•œ ๊ฒƒ๊ณผ ๋™์ผ) -Client Secret: (Supabase์— ์„ค์ •) -``` - -**์ค‘์š”**: `serverClientId`๋Š” Web Application์˜ Client ID๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค! - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -### ์ˆ˜์ • ํ›„ ํ…Œ์ŠคํŠธ - -```bash -# 1. ์•ฑ ์™„์ „ ์ข…๋ฃŒ -# 2. Xcode์—์„œ ์žฌ๋นŒ๋“œ -cd ios -pod install -cd .. -flutter clean -flutter pub get - -# 3. iOS ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์‹คํ–‰ -flutter run -d iphone -``` - -### ์˜ˆ์ƒ ๋™์ž‘ - -``` -[GoogleAuthService] Google ๋กœ๊ทธ์ธ ์‹œ์ž‘ -[GoogleAuthService] ํ”Œ๋žซํผ: ios -[GoogleAuthService] ๋ชจ๋ฐ”์ผ ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ -[GoogleAuthService] Google ์‚ฌ์šฉ์ž ์ธ์ฆ ์™„๋ฃŒ: user@gmail.com -[GoogleAuthService] Google ID Token ํš๋“ ์™„๋ฃŒ -[GoogleAuthService] Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@gmail.com -``` - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -### 1. serverClientId๋Š” Web Client ID - -- โŒ iOS Client ID ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ -- โœ… Web Application Client ID ์‚ฌ์šฉ - -### 2. Supabase ์„ค์ • ํ™•์ธ - -Supabase Dashboard > Authentication > Providers > Google์—์„œ: - -- Client ID: Web Application์˜ Client ID -- Client Secret: Web Application์˜ Client Secret - -### 3. ๋‹ค๋ฅธ ํ”Œ๋žซํผ - -- **Android**: `google-services.json` ํŒŒ์ผ๋„ ํ•„์š” -- **Web**: `serverClientId` ๋ถˆํ•„์š” (OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‚ฌ์šฉ) - -## ๐Ÿ“ ๊ด€๋ จ ์ด์Šˆ - -### Google Sign-In Flutter Plugin - -- https://pub.dev/packages/google_sign_in -- `serverClientId` ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌธ์„œ ์ฐธ์กฐ - -### Supabase Auth - -- https://supabase.com/docs/guides/auth/social-login/auth-google -- ID Token ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ฐ€์ด๋“œ - -## ๐ŸŽฏ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -์ˆ˜์ • ์™„๋ฃŒ ํ›„ ํ™•์ธ: - -- [ ] `GoogleSignIn` ์ดˆ๊ธฐํ™”์— `serverClientId` ์ถ”๊ฐ€ -- [ ] `serverClientId`๋Š” Web Application Client ID ์‚ฌ์šฉ -- [ ] Info.plist์— `GIDClientID` ์„ค์ • -- [ ] Info.plist์— Google OAuth URL Scheme ์„ค์ • -- [ ] Google Cloud Console์— iOS + Web ํด๋ผ์ด์–ธํŠธ ๋ชจ๋‘ ์ƒ์„ฑ -- [ ] Supabase์— Web Client ID/Secret ์„ค์ • -- [ ] ์•ฑ ์žฌ๋นŒ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ - ---- - -**์ˆ˜์ • ์™„๋ฃŒ**: 2025-10-11 -**ํฌ๋ž˜์‹œ ํ•ด๊ฒฐ**: `serverClientId` ์ถ”๊ฐ€๋กœ ํ•ด๊ฒฐ โœ… diff --git a/ENV_MIGRATION_COMPLETE.md b/ENV_MIGRATION_COMPLETE.md deleted file mode 100644 index 501aab1..0000000 --- a/ENV_MIGRATION_COMPLETE.md +++ /dev/null @@ -1,314 +0,0 @@ -# โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ - -## ๐ŸŽฏ ์ž‘์—… ์š”์•ฝ - -๋ชจ๋“  ํ•˜๋“œ์ฝ”๋”ฉ๋œ API ํ‚ค์™€ ์„ค์ •๊ฐ’์„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. - ---- - -## ๐Ÿ“Š ๋ณ€๊ฒฝ ์‚ฌํ•ญ - -### Before (ํ•˜๋“œ์ฝ”๋”ฉ) - -```dart -// SupabaseConfig.dart -static const supabaseUrl = 'https://YOUR-PROJECT-ID.supabase.co'; -static const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIs...'; - -// GoogleAuthService.dart -final googleClientId = 'YOUR-GOOGLE-CLIENT-ID...'; - -// Info.plist -GIDClientID -YOUR-GOOGLE-CLIENT-ID... -``` - -**โŒ ๋ฌธ์ œ์ **: - -- Git์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ปค๋ฐ‹ -- ํ™˜๊ฒฝ๋ณ„ ๊ด€๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ -- ํ˜‘์—… ์‹œ ํ‚ค ๊ณต์œ  ์–ด๋ ค์›€ -- ๋ณด์•ˆ ์œ„ํ—˜ - -### After (ํ™˜๊ฒฝ ๋ณ€์ˆ˜) - -```dart -// AppConfig.dart (์ƒˆ๋กœ ์ƒ์„ฑ) -static String get supabaseUrl => _getEnvValue('SUPABASE_URL', defaultValue); -static String get supabaseAnonKey => _getEnvValue('SUPABASE_ANON_KEY', defaultValue); -static String get googleWebClientId => _getEnvValue('GOOGLE_WEB_CLIENT_ID', defaultValue); - -// .env ํŒŒ์ผ -SUPABASE_URL=https://YOUR-PROJECT-ID.supabase.co -SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs... -GOOGLE_WEB_CLIENT_ID=YOUR-GOOGLE-CLIENT-ID... -``` - -**โœ… ์žฅ์ **: - -- `.env`๋Š” `.gitignore`์— ๋“ฑ๋ก๋จ (Git ์•ˆ์ „) -- `.env.example`๋กœ ํ•„์š”ํ•œ ์„ค์ • ๊ณต์œ  -- ํ™˜๊ฒฝ๋ณ„ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ (dev/staging/prod) -- ๋ณด์•ˆ ๊ฐ•ํ™” - ---- - -## ๐Ÿ“ ์ƒ์„ฑ/์ˆ˜์ •๋œ ํŒŒ์ผ - -### ์ƒˆ๋กœ ์ƒ์„ฑ - -| ํŒŒ์ผ | ์„ค๋ช… | -| ---------------------------- | -------------------------- | -| `.env` | ์‹ค์ œ ํ‚ค ๊ฐ’ (Git ๋ฌด์‹œ) | -| `.env.example` | ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ (Git ์ปค๋ฐ‹) | -| `lib/config/app_config.dart` | ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ค‘์•™ ๊ด€๋ฆฌ ํด๋ž˜์Šค | -| `ENV_CONFIG_GUIDE.md` | ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ฐ€์ด๋“œ | -| `ENV_MIGRATION_COMPLETE.md` | ์ด ํŒŒ์ผ | - -### ์ˆ˜์ •๋จ - -| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | -| -------------------------------------------- | ----------------------------- | -| `lib/config/supabase_config.dart` | `AppConfig` ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐ„์†Œํ™” | -| `lib/main.dart` | `AppConfig.initialize()` ์ถ”๊ฐ€ | -| `lib/services/supabase_oauth_validator.dart` | `AppConfig` ์‚ฌ์šฉ | -| `test/**/*.dart` | `AppConfig` import ์ถ”๊ฐ€ | -| `README.md` | ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ€์ด๋“œ ๋งํฌ ์ถ”๊ฐ€ | - ---- - -## ๐Ÿ”‘ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ชฉ๋ก - -### ํ•„์ˆ˜ - -| ๋ณ€์ˆ˜ | ์„ค๋ช… | ํ˜„์žฌ ๊ธฐ๋ณธ๊ฐ’ | -| ---------------------- | --------------------- | ------------------------------------------ | -| `SUPABASE_URL` | Supabase ํ”„๋กœ์ ํŠธ URL | `https://YOUR-PROJECT-ID.supabase.co` | -| `SUPABASE_ANON_KEY` | Supabase Anonymous ํ‚ค | `eyJhbGci...` (JWT) | -| `GOOGLE_WEB_CLIENT_ID` | Google Web Client ID | `YOUR-GOOGLE-CLIENT-ID...` | - -### ์„ ํƒ (Kakao ๋กœ๊ทธ์ธ์šฉ) - -| ๋ณ€์ˆ˜ | ์„ค๋ช… | ํ˜„์žฌ ๊ธฐ๋ณธ๊ฐ’ | -| ---------------------- | -------------------- | ----------- | -| `KAKAO_NATIVE_APP_KEY` | Kakao Native App Key | (๋นˆ ๋ฌธ์ž์—ด) | -| `KAKAO_JAVASCRIPT_KEY` | Kakao JavaScript Key | (๋นˆ ๋ฌธ์ž์—ด) | - -### ๊ธฐํƒ€ - -| ๋ณ€์ˆ˜ | ์„ค๋ช… | ํ˜„์žฌ ๊ธฐ๋ณธ๊ฐ’ | -| -------------------------- | ------------------------ | ------------------------------- | -| `BUNDLE_ID` | ์•ฑ Bundle Identifier | `com.example.runnerApp` | -| `GOOGLE_IOS_CLIENT_ID` | Google iOS Client ID | (Info.plist์—์„œ ์ฝ์Œ) | -| `GOOGLE_ANDROID_CLIENT_ID` | Google Android Client ID | (google-services.json์—์„œ ์ฝ์Œ) | - ---- - -## ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• - -### 1. `.env` ํŒŒ์ผ ์ƒ์„ฑ - -```bash -cp .env.example .env -``` - -### 2. ๊ฐ’ ์ž…๋ ฅ - -```bash -# .env ํŒŒ์ผ ์—ด๊ธฐ -nano .env # ๋˜๋Š” code .env - -# ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_ANON_KEY=your-actual-key -GOOGLE_WEB_CLIENT_ID=your-google-client-id -``` - -### 3. ์•ฑ ์‹คํ–‰ - -```bash -flutter pub get -flutter run -``` - -### 4. ๊ฒ€์ฆ ํ™•์ธ - -์•ฑ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ ๊ฒ€์ฆ: - -``` -[AppConfig] โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์™„๋ฃŒ -[AppConfig] โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฒ€์ฆ ์„ฑ๊ณต -[AppConfig] === ์•ฑ ์„ค์ • ์ •๋ณด === -[AppConfig] Supabase URL: https://YOUR-PROJECT-ID.supabase.co -[AppConfig] Supabase Anon Key: eyJhbGciOi...s2vk (๋งˆ์Šคํ‚น๋จ) -[AppConfig] Google Web Client ID: 174121824...nkhf (๋งˆ์Šคํ‚น๋จ) -[AppConfig] ================== -[SupabaseConfig] โœ… Supabase ์ดˆ๊ธฐํ™” ์™„๋ฃŒ -``` - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -```bash -โœ… flutter analyze: No issues found! -โœ… flutter test: 40/40 tests passed -โœ… ๋ชจ๋“  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์„ฑ๊ณต -โœ… ๊ฒ€์ฆ ๋กœ์ง ์ž‘๋™ -``` - ---- - -## ๐Ÿ”’ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -- [x] `.env` ํŒŒ์ผ์ด `.gitignore`์— ๋“ฑ๋ก๋จ -- [x] `.env.example`์— ์‹ค์ œ ํ‚ค ์—†์Œ -- [x] ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ‚ค ์ œ๊ฑฐ๋จ -- [x] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ -- [x] ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น (๋””๋ฒ„๊ทธ ๋กœ๊ทธ) -- [x] ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต (`.env` ์—†์–ด๋„ ์ž‘๋™) - ---- - -## ๐Ÿ“š AppConfig API - -### ์ฃผ์š” ๋ฉ”์„œ๋“œ - -```dart -// ์ดˆ๊ธฐํ™” -await AppConfig.initialize(); - -// ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ ‘๊ทผ -final supabaseUrl = AppConfig.supabaseUrl; -final supabaseAnonKey = AppConfig.supabaseAnonKey; -final googleWebClientId = AppConfig.googleWebClientId; -final kakaoNativeAppKey = AppConfig.kakaoNativeAppKey; -final bundleId = AppConfig.bundleId; - -// ๊ฒ€์ฆ -final isValid = AppConfig.validateConfig(); - -// ๋””๋ฒ„๊ทธ ์ •๋ณด ์ถœ๋ ฅ (๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น) -AppConfig.printConfig(); -``` - -### ๋‚ด๋ถ€ ๋™์ž‘ - -1. **์ดˆ๊ธฐํ™”** (`initialize()`): - - - `.env` ํŒŒ์ผ ๋กœ๋“œ - - ํŒŒ์ผ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ - - ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ํด๋ฐฑ - -2. **๊ฐ’ ์ ‘๊ทผ** (getter): - - - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์šฐ์„  - - ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ - - ์ ˆ๋Œ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์•ˆ ํ•จ - -3. **๊ฒ€์ฆ** (`validateConfig()`): - - - ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฒดํฌ - - ํ…œํ”Œ๋ฆฟ ๊ฐ’ ์ฒดํฌ - - ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋กœ๊ทธ ์ถœ๋ ฅ - -4. **๋””๋ฒ„๊ทธ ์ถœ๋ ฅ** (`printConfig()`): - - ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น - - ๋””๋ฒ„๊ทธ ๋ชจ๋“œ์—์„œ๋งŒ ์ž‘๋™ - - ์„ค์ • ํ™•์ธ ์šฉ์ด - ---- - -## ๐ŸŒ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • (์„ ํƒ) - -### ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜ ๋ถ„๋ฆฌ - -```bash -# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ -.env.development - -# ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ -.env.staging - -# ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ -.env.production -``` - -### ์‹คํ–‰ - -```bash -# ๊ฐœ๋ฐœ -flutter run --dart-define-from-file=.env.development - -# ์Šคํ…Œ์ด์ง• -flutter run --dart-define-from-file=.env.staging - -# ํ”„๋กœ๋•์…˜ -flutter run --dart-define-from-file=.env.production -``` - ---- - -## ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ†ต๊ณ„ - -| ํ•ญ๋ชฉ | ์ˆ˜์น˜ | -| ------------------ | -------- | -| ์ œ๊ฑฐ๋œ ํ•˜๋“œ์ฝ”๋”ฉ ํ‚ค | 8๊ฐœ | -| ์ƒ์„ฑ๋œ ํŒŒ์ผ | 4๊ฐœ | -| ์ˆ˜์ •๋œ ํŒŒ์ผ | 10๊ฐœ | -| ํ…Œ์ŠคํŠธ ํ†ต๊ณผ | 40/40 โœ… | -| Lint ์˜ค๋ฅ˜ | 0๊ฐœ โœ… | -| ๋ณด์•ˆ ๊ฐ•ํ™” | 100% โœ… | - ---- - -## ๐Ÿ“ ๋‹ค์Œ ๋‹จ๊ณ„ - -### Kakao ๋กœ๊ทธ์ธ ์ถ”๊ฐ€ ์‹œ - -```env -# .env์— ์ถ”๊ฐ€ -KAKAO_NATIVE_APP_KEY=your-kakao-key -KAKAO_JAVASCRIPT_KEY=your-kakao-js-key -``` - -```dart -// KakaoAuthService์—์„œ ์‚ฌ์šฉ -KakaoSdk.init( - nativeAppKey: AppConfig.kakaoNativeAppKey, - javaScriptAppKey: AppConfig.kakaoJavaScriptKey, -); -``` - -### CI/CD ์„ค์ • ์‹œ - -```yaml -# GitHub Actions -- name: Create .env - run: | - echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env - echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> .env -``` - ---- - -## ๐ŸŽ‰ ์™„๋ฃŒ! - -์ด์ œ ๋ชจ๋“  ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค! - -**์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ**: - -- โœ… **๋ณด์•ˆ**: API ํ‚ค๊ฐ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋˜์ง€ ์•Š์Œ -- โœ… **ํ˜‘์—…**: `.env.example`๋กœ ํ•„์š”ํ•œ ์„ค์ • ๊ณต์œ  -- โœ… **์œ ์ง€๋ณด์ˆ˜**: ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ -- โœ… **Git ์•ˆ์ „**: `.env`๋Š” ์ž๋™์œผ๋กœ ๋ฌด์‹œ๋จ - -**๊ด€๋ จ ๋ฌธ์„œ**: - -- `ENV_CONFIG_GUIDE.md` - ์ƒ์„ธ ์„ค์ • ๊ฐ€์ด๋“œ -- `README.md` - ํ”„๋กœ์ ํŠธ ๊ฐœ์š” -- `.env.example` - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ - ---- - -**๋‹ค์Œ**: Kakao ๋กœ๊ทธ์ธ ์—ฐ๋™ ๋˜๋Š” ๋‹ค๋ฅธ OAuth ์ œ๊ณต์ž ์ถ”๊ฐ€! ๐Ÿš€ diff --git a/ENV_SETUP.md b/ENV_SETUP.md deleted file mode 100644 index ea37bd5..0000000 --- a/ENV_SETUP.md +++ /dev/null @@ -1,32 +0,0 @@ -# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ฐ€์ด๋“œ - -## .env ํŒŒ์ผ ์ƒ์„ฑ - -ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”: - -```bash -# Supabase ์„ค์ • -SUPABASE_URL=https://YOUR-PROJECT-ID.supabase.co -SUPABASE_ANON_KEY=YOUR-SUPABASE-ANON-KEY - -# Google OAuth ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) -GOOGLE_CLIENT_ID_ANDROID=YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -GOOGLE_CLIENT_ID_IOS=YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -GOOGLE_CLIENT_ID_WEB=YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=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 -``` - -## ์ฃผ์˜์‚ฌํ•ญ - -- `.env` ํŒŒ์ผ์€ `.gitignore`์— ํฌํ•จ๋˜์–ด ์žˆ์–ด Git์— ์ปค๋ฐ‹๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค -- ์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์„ธ์š” -- ํ˜„์žฌ๋Š” ๊ฐœ๋ฐœ์šฉ ๊ธฐ๋ณธ๊ฐ’์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์–ด `.env` ํŒŒ์ผ์ด ์—†์–ด๋„ ์•ฑ์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค - -## ํŒŒ์ผ ์ƒ์„ฑ ๋ฐฉ๋ฒ• - -```bash -# ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ์‹คํ–‰ -touch .env - -# ์œ„์˜ ๋‚ด์šฉ์„ .env ํŒŒ์ผ์— ๋ณต์‚ฌํ•˜์—ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐ -``` diff --git a/GOOGLE_LOGIN_FIX_GUIDE.md b/GOOGLE_LOGIN_FIX_GUIDE.md deleted file mode 100644 index 3988f88..0000000 --- a/GOOGLE_LOGIN_FIX_GUIDE.md +++ /dev/null @@ -1,208 +0,0 @@ -# Google ๋กœ๊ทธ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ - -## ๐Ÿšจ ํ˜„์žฌ ์ƒํ™ฉ - -Google ๋กœ๊ทธ์ธ ์‹œ ๋‹ค์Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค: - -``` -PlatformException(Error, Error while launching https://YOUR-PROJECT-ID.supabase.co/auth/v1/authorize?provider=google...) -``` - -์ด ์˜ค๋ฅ˜๋Š” **Supabase Google OAuth Provider ์„ค์ •์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์„ ๋•Œ** ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• (๋‹จ๊ณ„๋ณ„) - -### 1๋‹จ๊ณ„: Supabase ๋Œ€์‹œ๋ณด๋“œ ์ ‘์† - -1. [Supabase ๋Œ€์‹œ๋ณด๋“œ](https://supabase.com/dashboard) ์ ‘์† -2. ํ”„๋กœ์ ํŠธ ์„ ํƒ: `runner-app` (ID: `YOUR-PROJECT-ID`) - -### 2๋‹จ๊ณ„: URL Configuration ์„ค์ • - -1. ์ขŒ์ธก ๋ฉ”๋‰ด์—์„œ **Authentication** ํด๋ฆญ -2. **URL Configuration** ํƒญ ์„ ํƒ -3. ๋‹ค์Œ ์„ค์ • ์ถ”๊ฐ€: - -#### Site URL - -``` -http://localhost:3000 -``` - -๋˜๋Š” ๋ฐฐํฌ๋œ ์•ฑ์˜ ๋„๋ฉ”์ธ: - -``` -https://your-app-domain.com -``` - -#### Redirect URLs - -๋‹ค์Œ URL๋“ค์„ **ํ•˜๋‚˜์”ฉ** ์ถ”๊ฐ€: - -``` -com.example.runnerApp:// -com.example.runnerApp://login-callback -https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback -``` - -**์ค‘์š”**: ๊ฐ URL์„ ์—”ํ„ฐ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ถ”๊ฐ€ํ•˜๊ณ  **Save** ๋ฒ„ํŠผ ํด๋ฆญ - -### 3๋‹จ๊ณ„: Google Cloud Console ์„ค์ • - -#### 3.1 ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋˜๋Š” ์„ ํƒ - -1. [Google Cloud Console](https://console.cloud.google.com/) ์ ‘์† -2. ๊ธฐ์กด ํ”„๋กœ์ ํŠธ ์„ ํƒ ๋˜๋Š” ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ - -#### 3.2 OAuth ๋™์˜ ํ™”๋ฉด ์„ค์ • - -1. **APIs & Services** > **OAuth consent screen** ์ด๋™ -2. User Type: **External** ์„ ํƒ (๋‚ด๋ถ€ ์‚ฌ์šฉ์ž๋งŒ์ด๋ฉด Internal) -3. ์•ฑ ์ •๋ณด ์ž…๋ ฅ: - - App name: `StrideNote` ๋˜๋Š” ์›ํ•˜๋Š” ์ด๋ฆ„ - - User support email: ๋ณธ์ธ ์ด๋ฉ”์ผ - - Developer contact email: ๋ณธ์ธ ์ด๋ฉ”์ผ -4. **Save and Continue** ํด๋ฆญ -5. Scopes ๋‹จ๊ณ„: ๊ธฐ๋ณธ๊ฐ’ ์œ ์ง€ ํ›„ **Save and Continue** -6. Test users ์ถ”๊ฐ€ (๊ฐœ๋ฐœ ์ค‘์—๋งŒ ํ•„์š”) -7. **Save and Continue** ํด๋ฆญ - -#### 3.3 OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ID ์ƒ์„ฑ - -1. **APIs & Services** > **Credentials** ์ด๋™ -2. **+ CREATE CREDENTIALS** ํด๋ฆญ -3. **OAuth 2.0 Client ID** ์„ ํƒ - -#### iOS ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ: - -1. Application type: **iOS** -2. Name: `StrideNote iOS` -3. Bundle ID: `com.example.runnerApp` -4. **CREATE** ํด๋ฆญ -5. ์ƒ์„ฑ๋œ **Client ID** ๋ณต์‚ฌ (๋‚˜์ค‘์— ์‚ฌ์šฉ) - -#### Android ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ (์„ ํƒ์‚ฌํ•ญ): - -1. Application type: **Android** -2. Name: `StrideNote Android` -3. Package name: `com.example.runnerApp` -4. SHA-1 certificate fingerprint: ๊ฐœ๋ฐœ/๋ฐฐํฌ ์ธ์ฆ์„œ์˜ SHA-1 - ```bash - # ๋””๋ฒ„๊ทธ ์ธ์ฆ์„œ SHA-1 ์–ป๊ธฐ - cd android - ./gradlew signingReport - ``` -5. **CREATE** ํด๋ฆญ - -#### Web ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ (Supabase์šฉ): - -1. Application type: **Web application** -2. Name: `StrideNote Web (Supabase)` -3. Authorized redirect URIs์— ๋‹ค์Œ ์ถ”๊ฐ€: - ``` - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` -4. **CREATE** ํด๋ฆญ -5. ์ƒ์„ฑ๋œ **Client ID**์™€ **Client Secret** ๋ณต์‚ฌ - -### 4๋‹จ๊ณ„: Supabase Google Provider ์„ค์ • - -1. Supabase ๋Œ€์‹œ๋ณด๋“œ๋กœ ๋Œ์•„๊ฐ€๊ธฐ -2. **Authentication** > **Providers** ํƒญ ์„ ํƒ -3. **Google** Provider ์ฐพ๊ธฐ -4. **Enable Sign in with Google** ํ† ๊ธ€ ์ผœ๊ธฐ -5. ์„ค์ • ์ž…๋ ฅ: - - **Client ID (for OAuth)**: Web ํด๋ผ์ด์–ธํŠธ์˜ Client ID ์ž…๋ ฅ - - **Client Secret (for OAuth)**: Web ํด๋ผ์ด์–ธํŠธ์˜ Client Secret ์ž…๋ ฅ -6. **Save** ๋ฒ„ํŠผ ํด๋ฆญ - -### 5๋‹จ๊ณ„: iOS Info.plist ์—…๋ฐ์ดํŠธ (์ด๋ฏธ ์™„๋ฃŒ๋จ) - -โœ… ์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค: - -- Bundle ID: `com.example.runnerApp` -- URL Scheme: `com.example.runnerApp` - -### 6๋‹จ๊ณ„: Android ์„ค์ • ํ™•์ธ (์ด๋ฏธ ์™„๋ฃŒ๋จ) - -โœ… ์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค: - -- Package name: `com.example.runnerApp` -- URL Scheme: `com.example.runnerApp` - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -์„ค์ • ์™„๋ฃŒ ํ›„: - -1. ์•ฑ์„ ์™„์ „ํžˆ ์ข…๋ฃŒ -2. ์•ฑ ์žฌ์‹คํ–‰ -3. Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -4. Google ๊ณ„์ • ์„ ํƒ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธ - -### ์˜ˆ์ƒ๋˜๋Š” ๋™์ž‘: - -1. Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -2. Safari/Chrome์ด ์—ด๋ฆฌ๋ฉด์„œ Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ํ‘œ์‹œ -3. Google ๊ณ„์ • ์„ ํƒ -4. ๊ถŒํ•œ ๋™์˜ ํ™”๋ฉด -5. ์•ฑ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ -6. ๋กœ๊ทธ์ธ ์™„๋ฃŒ - -## ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -- [ ] Supabase Site URL์ด ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Supabase Redirect URLs์— `com.example.runnerApp://`๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google Cloud Console์—์„œ OAuth ๋™์˜ ํ™”๋ฉด์ด ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google Cloud Console์—์„œ Web ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Supabase Google Provider๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Supabase Google Provider์— ์˜ฌ๋ฐ”๋ฅธ Client ID/Secret์ด ์ž…๋ ฅ๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google Cloud Console์˜ Authorized redirect URIs์— Supabase ์ฝœ๋ฐฑ URL์ด ์ถ”๊ฐ€๋˜์–ด ์žˆ๋Š”๊ฐ€? - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -### 1. OAuth ๋™์˜ ํ™”๋ฉด ์ƒํƒœ - -- **Testing**: ์ถ”๊ฐ€๋œ ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž๋งŒ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅ -- **In Production**: ๋ชจ๋“  Google ๊ณ„์ • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅ - -๊ฐœ๋ฐœ ์ค‘์—๋Š” Testing ์ƒํƒœ๋กœ ๋‘๊ณ , ๋ณธ์ธ ์ด๋ฉ”์ผ์„ ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž๋กœ ์ถ”๊ฐ€ํ•˜์„ธ์š”. - -### 2. Client ID/Secret - -- **Web ํด๋ผ์ด์–ธํŠธ**์˜ Client ID/Secret์„ Supabase์— ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค -- iOS/Android ํด๋ผ์ด์–ธํŠธ ID๋Š” ์•ฑ์— ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (Supabase๊ฐ€ ์ฒ˜๋ฆฌ) - -### 3. Bundle ID ์ผ์น˜ - -- Google Cloud Console์˜ Bundle ID: `com.example.runnerApp` -- iOS Info.plist์˜ Bundle ID: `com.example.runnerApp` -- Android build.gradle์˜ Package name: `com.example.runnerApp` - -๋ชจ๋‘ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -## ๐Ÿ“ž ์ถ”๊ฐ€ ๋„์›€ - -์„ค์ • ํ›„์—๋„ ๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋ฉด: - -1. **Supabase ๋กœ๊ทธ ํ™•์ธ**: - - - Dashboard > Settings > API > Auth logs - -2. **Google Cloud Console ๋กœ๊ทธ ํ™•์ธ**: - - - APIs & Services > Credentials > OAuth 2.0 Client IDs - -3. **์•ฑ ๋กœ๊ทธ ํ™•์ธ**: - - Xcode Console ๋˜๋Š” Android Logcat์—์„œ ์ƒ์„ธ ์˜ค๋ฅ˜ ํ™•์ธ - -## ๐ŸŽฏ ์„ฑ๊ณต ์‹œ ์˜ˆ์ƒ ๋กœ๊ทธ - -``` -[GoogleAuthService] Supabase ๋„ค์ดํ‹ฐ๋ธŒ Google ๋กœ๊ทธ์ธ ์‹œ์ž‘ -[OAuthValidator] === Supabase OAuth ์„ค์ • ๊ฒ€์ฆ ์‹œ์ž‘ === -[OAuthValidator] โœ… Supabase ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ -[OAuthValidator] โœ… Supabase URL: https://YOUR-PROJECT-ID.supabase.co -[GoogleAuthService] Google OAuth ์‘๋‹ต: true -[AuthProvider] Auth state changed: your-email@gmail.com -[GoogleAuthService] ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ ์‹œ์ž‘: your-email@gmail.com -``` diff --git a/GOOGLE_NATIVE_LOGIN_COMPLETE.md b/GOOGLE_NATIVE_LOGIN_COMPLETE.md deleted file mode 100644 index 24d8053..0000000 --- a/GOOGLE_NATIVE_LOGIN_COMPLETE.md +++ /dev/null @@ -1,427 +0,0 @@ -# ๐ŸŽฏ Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์™„์ „ ๊ฐ€์ด๋“œ - -## โœ… ์™„๋ฃŒ๋œ ๋ฆฌํŒฉํ„ฐ๋ง - -### ๐Ÿ—๏ธ ์•„ํ‚คํ…์ฒ˜ - -``` -์‚ฌ์šฉ์ž โ†’ Google Sign-In SDK (๋„ค์ดํ‹ฐ๋ธŒ) โ†’ ID Token โ†’ Supabase signInWithIdToken โ†’ ์„ธ์…˜ ์œ ์ง€ -``` - -**ํŠน์ง•:** - -- โœ… **๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆฌ์ง€ ์•Š์Œ** (100% ๋„ค์ดํ‹ฐ๋ธŒ/์ธ์•ฑ) -- โœ… **๋”ฅ๋งํฌ ๋ถˆํ•„์š”** (redirectTo ์™„์ „ ์ œ๊ฑฐ) -- โœ… **์ž๋™ ์„ธ์…˜ ์œ ์ง€** (Supabase ์ž๋™ ์ฒ˜๋ฆฌ) -- โœ… **์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ** -- โœ… **3๊ฐœ ํ”Œ๋žซํผ ๋™์ผ ๋กœ์ง** (iOS/Android/Web) - ---- - -## ๐Ÿ“ฑ ํ”Œ๋žซํผ๋ณ„ ๋™์ž‘ - -### iOS - -1. ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In UI ํ‘œ์‹œ -2. ์‚ฌ์šฉ์ž Google ๊ณ„์ • ์„ ํƒ -3. ID Token ๋ฐœ๊ธ‰ -4. Supabase ์ธ์ฆ -5. ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -6. ์•ฑ์œผ๋กœ ๋ณต๊ท€ (์ฆ‰์‹œ) - -### Android - -1. Google Play Services ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ -2. ์‚ฌ์šฉ์ž Google ๊ณ„์ • ์„ ํƒ -3. ID Token ๋ฐœ๊ธ‰ -4. Supabase ์ธ์ฆ -5. ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -6. ์•ฑ ๋‚ด์—์„œ ์™„๋ฃŒ - -### Web - -1. Google Sign-In Web SDK ํŒ์—… -2. ์‚ฌ์šฉ์ž Google ๊ณ„์ • ์„ ํƒ -3. ID Token ๋ฐœ๊ธ‰ -4. Supabase ์ธ์ฆ -5. ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -6. ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์Œ - ---- - -## ๐Ÿ”ง ์ฝ”์–ด ์ฝ”๋“œ - -### GoogleAuthService (์™„์ „ ๋ฆฌํŒฉํ„ฐ๋ง) - -**์œ„์น˜**: `lib/services/google_auth_service.dart` - -**์ฃผ์š” ๊ธฐ๋Šฅ**: - -- `signInWithGoogle()`: ๋„ค์ดํ‹ฐ๋ธŒ Google ๋กœ๊ทธ์ธ (๋ชจ๋“  ํ”Œ๋žซํผ) -- `signOut()`: ๋กœ๊ทธ์•„์›ƒ (Google + Supabase ๋™์‹œ) -- `getCurrentGoogleUser()`: ํ˜„์žฌ Google ๊ณ„์ • ํ™•์ธ -- `disconnect()`: Google ๊ณ„์ • ์—ฐ๊ฒฐ ์™„์ „ ํ•ด์ œ - -**ํ•ต์‹ฌ ๋กœ์ง**: - -```dart -// 1. Google Sign-In (๋„ค์ดํ‹ฐ๋ธŒ) -final googleUser = await _googleSignIn.signIn(); - -// 2. ID Token ๊ฐ€์ ธ์˜ค๊ธฐ -final googleAuth = await googleUser.authentication; -final idToken = googleAuth.idToken; -final accessToken = googleAuth.accessToken; - -// 3. Supabase ์ธ์ฆ -await SupabaseConfig.client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, -); - -// 4. ์ž๋™ ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ -await _handleUserProfile(currentUser, googleUser); -``` - ---- - -## ๐Ÿ“ฆ ํ•„์ˆ˜ ์„ค์ • - -### 1. iOS ์„ค์ • - -**ํŒŒ์ผ**: `ios/Runner/Info.plist` - -```xml - -GIDClientID -YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com - - -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.YOUR-GOOGLE-CLIENT-ID - - - -``` - -### 2. Android ์„ค์ • - -**ํŒŒ์ผ**: `android/app/build.gradle.kts` - -```kotlin -android { - defaultConfig { - applicationId = "com.example.runnerApp" - } -} -``` - -**SHA-1 ๋“ฑ๋ก** (Google Cloud Console): - -```bash -cd android && ./gradlew signingReport -``` - -### 3. Web ์„ค์ • - -**์ž๋™ ์ฒ˜๋ฆฌ๋จ** (์ถ”๊ฐ€ ์„ค์ • ๋ถˆํ•„์š”) - -### 4. Google Cloud Console - -1. **OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ID ์ƒ์„ฑ** - - - Web Application: `YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com` - - iOS Application: Bundle ID ๋“ฑ๋ก - - Android Application: SHA-1 ๋“ฑ๋ก - -2. **Authorized redirect URIs** (Web์šฉ) - - `https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback` - -### 5. Supabase Dashboard - -1. **Authentication > Providers > Google** - - Enabled: โœ… - - Client ID: (Web OAuth Client ID) - - Client Secret: (Web OAuth Client Secret) - ---- - -## ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• - -### ๋กœ๊ทธ์ธ - -```dart -import 'package:stride_note/services/google_auth_service.dart'; - -// ๋กœ๊ทธ์ธ -try { - final success = await GoogleAuthService.signInWithGoogle(); - - if (success) { - print('โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต!'); - // ์„ธ์…˜ ์ž๋™ ์œ ์ง€๋จ - // ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ๋จ - } else { - print('โŒ ์‚ฌ์šฉ์ž๊ฐ€ ์ทจ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค'); - } -} catch (e) { - print('โŒ ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜: $e'); -} -``` - -### ๋กœ๊ทธ์•„์›ƒ - -```dart -// ๋กœ๊ทธ์•„์›ƒ -await GoogleAuthService.signOut(); -``` - -### ๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ - -```dart -// Google ๊ณ„์ • ์™„์ „ ์—ฐ๊ฒฐ ํ•ด์ œ -await GoogleAuthService.disconnect(); -``` - ---- - -## ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ - -### iOS: "Google Sign-In failed" - -**์›์ธ**: `GIDClientID` ๋ˆ„๋ฝ ๋˜๋Š” ์ž˜๋ชป๋œ Client ID - -**ํ•ด๊ฒฐ**: - -1. `Info.plist`์— `GIDClientID` ํ™•์ธ -2. Google Cloud Console์—์„œ iOS Client ๋“ฑ๋ก ํ™•์ธ -3. Bundle ID ์ผ์น˜ ํ™•์ธ (`com.example.runnerApp`) - -### Android: "Sign-In Error" - -**์›์ธ**: SHA-1 ๋ฏธ๋“ฑ๋ก ๋˜๋Š” Application ID ๋ถˆ์ผ์น˜ - -**ํ•ด๊ฒฐ**: - -1. `./gradlew signingReport`๋กœ SHA-1 ํ™•์ธ -2. Google Cloud Console์— SHA-1 ๋“ฑ๋ก -3. `applicationId` ์ผ์น˜ ํ™•์ธ - -### Web: "popup_closed_by_user" - -**์›์ธ**: ์‚ฌ์šฉ์ž๊ฐ€ ํŒ์—…์„ ๋‹ซ์Œ - -**ํ•ด๊ฒฐ**: ์ •์ƒ ๋™์ž‘ (์˜ค๋ฅ˜ ์•„๋‹˜) - -### Supabase: "Invalid ID Token" - -**์›์ธ**: Google OAuth Client ID/Secret ๋ถˆ์ผ์น˜ - -**ํ•ด๊ฒฐ**: - -1. Supabase Dashboard์—์„œ Google Provider ์„ค์ • ํ™•์ธ -2. **Web Application** Client ID/Secret ์‚ฌ์šฉ ํ™•์ธ -3. Google Cloud Console์—์„œ Client ID ํ™•์ธ - -### "Nonces mismatch" - -**์›์ธ**: Google Sign-In SDK๊ฐ€ `serverClientId` ์‚ฌ์šฉ ์‹œ nonce๋ฅผ ์ž๋™ ์ƒ์„ฑ - -**ํ•ด๊ฒฐ**: - -- โœ… **serverClientId ์ œ๊ฑฐ** (ํ˜„์žฌ ๊ตฌํ˜„) -- โœ… **nonce ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์ด signInWithIdToken ํ˜ธ์ถœ** -- โœ… ํ”Œ๋žซํผ๋ณ„ Client ID๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์„ค์ •์—์„œ ์ž๋™ ์‚ฌ์šฉ - -**์ƒ์„ธ ๊ฐ€์ด๋“œ**: `NONCE_ISSUE_SOLVED.md` ์ฐธ์กฐ - ---- - -## ๐Ÿ“Š ํ…Œ์ŠคํŠธ - -### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - -```bash -flutter test test/unit/services/google_auth_native_test.dart -``` - -### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (์‹ค์ œ ๊ธฐ๊ธฐ) - -```bash -# iOS -flutter run -d iPhone - -# Android -flutter run -d - -# Web -flutter run -d chrome -``` - -**ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค**: - -1. โœ… Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -2. โœ… Google ๊ณ„์ • ์„ ํƒ -3. โœ… ์•ฑ์œผ๋กœ ์ฆ‰์‹œ ๋ณต๊ท€ (๋ธŒ๋ผ์šฐ์ € ์—†์Œ) -4. โœ… ํ™ˆ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ํ‘œ์‹œ -5. โœ… ๋กœ๊ทธ์•„์›ƒ ํ›„ ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ -6. โœ… ์•ฑ ์žฌ์‹œ์ž‘ ํ›„ ์„ธ์…˜ ์œ ์ง€ ํ™•์ธ - ---- - -## ๐Ÿ“ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์š”์•ฝ - -### ์‚ญ์ œ๋œ ๊ธฐ๋Šฅ - -- โŒ `signInWithOAuth` (OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ) -- โŒ `redirectTo` ํŒŒ๋ผ๋ฏธํ„ฐ -- โŒ Deep Link ์ฒ˜๋ฆฌ -- โŒ URL Scheme ๋ณต์žกํ•œ ์„ค์ • -- โŒ `LaunchMode.externalApplication` -- โŒ `SupabaseOAuthValidator` (๋ถˆํ•„์š”) - -### ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ - -- โœ… ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In (๋ชจ๋“  ํ”Œ๋žซํผ) -- โœ… `signInWithIdToken` (ID Token ์ง์ ‘ ์ „๋‹ฌ) -- โœ… ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -- โœ… `getCurrentGoogleUser` (Silent Sign-In) -- โœ… `disconnect` (๊ณ„์ • ์—ฐ๊ฒฐ ํ•ด์ œ) - -### ๊ฐœ์„ ๋œ ์‚ฌํ•ญ - -- ๐Ÿš€ ๋ธŒ๋ผ์šฐ์ € ์—†์Œ (100% ๋„ค์ดํ‹ฐ๋ธŒ) -- ๐Ÿš€ ๋”ฅ๋งํฌ ๋ถˆํ•„์š” -- ๐Ÿš€ ์„ค์ • ๋‹จ์ˆœํ™” -- ๐Ÿš€ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ฐœ์„  -- ๐Ÿš€ ๋กœ๊ทธ ๊ฐ•ํ™” - ---- - -## ๐ŸŽ“ ํ•™์Šต ํฌ์ธํŠธ - -### Why Google Sign-In SDK? - -**Before (OAuth)**: - -``` -์•ฑ โ†’ ๋ธŒ๋ผ์šฐ์ € โ†’ Google โ†’ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ โ†’ ์•ฑ (๋”ฅ๋งํฌ) -``` - -**After (Native SDK)**: - -``` -์•ฑ โ†’ Google SDK (๋„ค์ดํ‹ฐ๋ธŒ) โ†’ ID Token โ†’ Supabase -``` - -**์žฅ์ **: - -1. **UX ๊ฐœ์„ **: ๋ธŒ๋ผ์šฐ์ € ์—†์Œ, ๋น ๋ฅธ ๋กœ๊ทธ์ธ -2. **๋ณด์•ˆ ๊ฐ•ํ™”**: ๋„ค์ดํ‹ฐ๋ธŒ ํ”Œ๋กœ์šฐ, ํ”ผ์‹ฑ ๋ฐฉ์ง€ -3. **์„ค์ • ๋‹จ์ˆœํ™”**: redirectTo/๋”ฅ๋งํฌ ๋ถˆํ•„์š” -4. **์‹ ๋ขฐ์„ฑ ํ–ฅ์ƒ**: OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์˜ค๋ฅ˜ ์ œ๊ฑฐ - -### Why signInWithIdToken? - -**Supabase์˜ `signInWithOAuth`**: - -- ๋ธŒ๋ผ์šฐ์ € ํ•„์ˆ˜ -- ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํ•„์š” -- ๋ชจ๋ฐ”์ผ์—์„œ ๋ณต์žก - -**Supabase์˜ `signInWithIdToken`**: - -- ID Token๋งŒ ํ•„์š” -- ์ง์ ‘ ์ธ์ฆ -- ๋ชจ๋“  ํ”Œ๋žซํผ ๋™์ผ - ---- - -## ๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ - -### serverClientId - -```dart -serverClientId: 'YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com' -``` - -**์—ญํ• **: ID Token ๊ฒ€์ฆ - -**์ฃผ์˜**: - -- โœ… **Web Application** Client ID ์‚ฌ์šฉ -- โŒ iOS/Android Client ID ์‚ฌ์šฉ ๊ธˆ์ง€ -- ๐Ÿ”’ Client Secret์€ ๋ฐฑ์—”๋“œ์—๋งŒ ์ €์žฅ - -### ID Token - -**ํŠน์ง•**: - -- ์งง์€ ์ˆ˜๋ช… (1์‹œ๊ฐ„) -- Supabase๊ฐ€ ๊ฒ€์ฆ -- ์‚ฌ์šฉ์ž ์ •๋ณด ํฌํ•จ (JWT) - -**์•ˆ์ „ํ•œ ์ด์œ **: - -- Google์ด ์„œ๋ช… -- Supabase๊ฐ€ ๊ฒ€์ฆ -- ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€ - ---- - -## ๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ - -- [Google Sign-In Flutter Plugin](https://pub.dev/packages/google_sign_in) -- [Supabase Auth - Google](https://supabase.com/docs/guides/auth/social-login/auth-google) -- [Google OAuth 2.0](https://developers.google.com/identity/protocols/oauth2) -- [Supabase Flutter SDK](https://supabase.com/docs/reference/dart) - ---- - -## โœจ ๋‹ค์Œ ๋‹จ๊ณ„ - -### ๊ถŒ์žฅ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ - -1. **Biometric Authentication** - - ```dart - // Face ID / Touch ID / Fingerprint - await LocalAuthentication().authenticate(); - ``` - -2. **Offline Support** - - ```dart - // Supabase Realtime + Local DB - await SupabaseConfig.client.realtime.channel('changes'); - ``` - -3. **Multi-Account Support** - - ```dart - // Google Sign-In ๋ฉ€ํ‹ฐ ๊ณ„์ • - await _googleSignIn.signIn(); // ๊ณ„์ • ์„ ํƒ UI - ``` - -4. **Apple Sign In** - ```dart - // Apple Sign In ์ถ”๊ฐ€ - await SignInWithApple.getAppleIDCredential(); - ``` - ---- - -## ๐ŸŽ‰ ์™„๋ฃŒ! - -์ด์ œ ์•ฑ์—์„œ **๋ธŒ๋ผ์šฐ์ € ์—†์ด, ๋”ฅ๋งํฌ ์—†์ด, ์ฆ‰์‹œ ๋กœ๊ทธ์ธ**์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค! - -๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ์•ˆ์ „ํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ Google ๋กœ๊ทธ์ธ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. - -**ํ…Œ์ŠคํŠธํ•ด๋ณด์„ธ์š”**: `flutter run` โ†’ Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ์ฆ‰์‹œ ๋กœ๊ทธ์ธ ์™„๋ฃŒ! ๐Ÿš€ diff --git a/GOOGLE_SIGNIN_NATIVE.md b/GOOGLE_SIGNIN_NATIVE.md deleted file mode 100644 index 7a9fed6..0000000 --- a/GOOGLE_SIGNIN_NATIVE.md +++ /dev/null @@ -1,270 +0,0 @@ -# Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ๊ฐ€์ด๋“œ - -## ๐ŸŽฏ ๊ฐœ์š” - -์ด ์•ฑ์€ **ํ”Œ๋žซํผ๋ณ„๋กœ ์ตœ์ ํ™”๋œ Google ๋กœ๊ทธ์ธ**์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค: - -- **๋ชจ๋ฐ”์ผ (iOS/Android)**: ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In โ†’ Supabase ID Token ์ธ์ฆ -- **์›น**: Supabase OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ธ์ฆ - -## ๐Ÿ—๏ธ ์•„ํ‚คํ…์ฒ˜ - -### ๋ชจ๋ฐ”์ผ ํ”Œ๋กœ์šฐ - -``` -์‚ฌ์šฉ์ž โ†’ Google Sign-In SDK โ†’ Google ์ธ์ฆ -โ†’ ID Token ํš๋“ โ†’ Supabase signInWithIdToken -โ†’ ๋กœ๊ทธ์ธ ์™„๋ฃŒ -``` - -### ์›น ํ”Œ๋กœ์šฐ - -``` -์‚ฌ์šฉ์ž โ†’ Supabase OAuth โ†’ Google ์ธ์ฆ ํŽ˜์ด์ง€ -โ†’ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ โ†’ Supabase ์„ธ์…˜ ์ƒ์„ฑ -โ†’ ๋กœ๊ทธ์ธ ์™„๋ฃŒ -``` - -## ๐Ÿ“‹ ๊ตฌํ˜„ ์ƒ์„ธ - -### GoogleAuthService ๊ตฌ์กฐ - -```dart -class GoogleAuthService { - // Google Sign-In ์ดˆ๊ธฐํ™” (serverClientId ํ•„์ˆ˜!) - static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - serverClientId: 'YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com', - ); - - // ํ”Œ๋žซํผ ๋ถ„๊ธฐ - static Future signInWithGoogle() async { - if (kIsWeb) { - return await _signInWithGoogleWeb(); - } else { - return await _signInWithGoogleMobile(); - } - } - - // ์›น: OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ - static Future _signInWithGoogleWeb() async { - await client.auth.signInWithOAuth( - OAuthProvider.google, - redirectTo: kIsWeb ? null : 'com.example.runnerApp://login-callback', - ); - } - - // ๋ชจ๋ฐ”์ผ: ๋„ค์ดํ‹ฐ๋ธŒ + ID Token - static Future _signInWithGoogleMobile() async { - // 1. Google Sign-In - final googleUser = await _googleSignIn.signIn(); - - // 2. ID Token ํš๋“ - final googleAuth = await googleUser.authentication; - final idToken = googleAuth.idToken; - - // 3. Supabase ์ธ์ฆ - await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: googleAuth.accessToken, - ); - } -} -``` - -## ๐Ÿ”ง ์„ค์ • - -### 1. iOS ์„ค์ • - -#### Info.plist - -```xml - -GIDClientID -YOUR-CLIENT-ID.apps.googleusercontent.com - - -CFBundleURLTypes - - - - CFBundleURLSchemes - - com.googleusercontent.apps.YOUR-CLIENT-ID - - - - - - CFBundleURLSchemes - - com.example.runnerApp - - - -``` - -### 2. Android ์„ค์ • - -#### AndroidManifest.xml - -```xml - - - - - - - -``` - -#### google-services.json (์„ ํƒ์‚ฌํ•ญ) - -Google Cloud Console์—์„œ ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ `android/app/` ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฐฐ์น˜ - -### 3. Google Cloud Console ์„ค์ • - -#### iOS OAuth Client - -- Application type: **iOS** -- Bundle ID: `com.example.runnerApp` - -#### Android OAuth Client - -- Application type: **Android** -- Package name: `com.example.stride_note` -- SHA-1 certificate fingerprint: (๊ฐœ๋ฐœ/๋ฐฐํฌ ์ธ์ฆ์„œ) - -#### Web OAuth Client (Supabase์šฉ) - -- Application type: **Web application** -- Authorized redirect URIs: - ``` - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` - -### 4. Supabase ์„ค์ • - -#### Authentication > Providers > Google - -- **Enable Sign in with Google**: โœ… -- **Client ID (for OAuth)**: Web ํด๋ผ์ด์–ธํŠธ ID -- **Client Secret (for OAuth)**: Web ํด๋ผ์ด์–ธํŠธ Secret - -#### Authentication > URL Configuration - -- **Redirect URLs**: - ``` - com.example.runnerApp://login-callback - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - -```bash -flutter test test/unit/services/google_auth_platform_test.dart -``` - -### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -```bash -# ๋ชจ๋ฐ”์ผ (iOS) -flutter run -d iphone - -# ๋ชจ๋ฐ”์ผ (Android) -flutter run -d android - -# ์›น -flutter run -d chrome -``` - -## ๐ŸŽฏ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ - -### ๋ชจ๋ฐ”์ผ - -1. "Google๋กœ ๊ณ„์†ํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ -2. ๋„ค์ดํ‹ฐ๋ธŒ Google ๊ณ„์ • ์„ ํƒ ํ™”๋ฉด (์•ฑ ๋‚ด) -3. ๊ถŒํ•œ ๋™์˜ (ํ•„์š”์‹œ) -4. ์ฆ‰์‹œ ์•ฑ์œผ๋กœ ๋ณต๊ท€ ๋ฐ ๋กœ๊ทธ์ธ ์™„๋ฃŒ - -### ์›น - -1. "Google๋กœ ๊ณ„์†ํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ -2. Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (์ƒˆ ํƒญ) -3. ๊ณ„์ • ์„ ํƒ ๋ฐ ๊ถŒํ•œ ๋™์˜ -4. ์•ฑ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋ฐ ๋กœ๊ทธ์ธ ์™„๋ฃŒ - -## ๐Ÿšจ ๋ฌธ์ œ ํ•ด๊ฒฐ - -### ๋ชจ๋ฐ”์ผ: ์•ฑ ํฌ๋ž˜์‹œ (EXC_CRASH SIGABRT) - -**์›์ธ**: `GoogleSignIn` ์ดˆ๊ธฐํ™” ์‹œ `serverClientId` ๋ˆ„๋ฝ - -**ํ•ด๊ฒฐ**: - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - serverClientId: 'YOUR-WEB-CLIENT-ID.apps.googleusercontent.com', // ํ•„์ˆ˜! -); -``` - -**์ฃผ์˜**: `serverClientId`๋Š” **Web Application** Client ID๋ฅผ ์‚ฌ์šฉ! - -### ๋ชจ๋ฐ”์ผ: "Google Sign-In failed" - -โ†’ iOS Info.plist์˜ `GIDClientID` ํ™•์ธ -โ†’ Google Cloud Console์˜ iOS ํด๋ผ์ด์–ธํŠธ Bundle ID ํ™•์ธ -โ†’ `serverClientId`๊ฐ€ Web Client ID์ธ์ง€ ํ™•์ธ - -### ๋ชจ๋ฐ”์ผ: "ID Token is null" - -โ†’ `serverClientId` ์„ค์ • ํ™•์ธ (๊ฐ€์žฅ ํ”ํ•œ ์›์ธ) -โ†’ Google Cloud Console์˜ Web OAuth Client ์„ค์ • ํ™•์ธ -โ†’ Supabase์— ์˜ฌ๋ฐ”๋ฅธ Web Client ID/Secret ์„ค์ • ํ™•์ธ - -### ์›น: "redirect_uri_mismatch" - -โ†’ Google Cloud Console์˜ Authorized redirect URIs ํ™•์ธ -โ†’ Supabase ์ฝœ๋ฐฑ URL ์ถ”๊ฐ€ ํ™•์ธ - -### ๋ชจ๋“  ํ”Œ๋žซํผ: "Supabase signInWithIdToken failed" - -โ†’ Supabase Google Provider ํ™œ์„ฑํ™” ํ™•์ธ -โ†’ Google OAuth Client ID/Secret ์„ค์ • ํ™•์ธ - -## ๐Ÿ“Š ์žฅ์  - -### ๋ชจ๋ฐ”์ผ ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ - -โœ… ๋” ๋‚˜์€ UX (์•ฑ ๋‚ด์—์„œ ์™„๊ฒฐ) -โœ… ๋น ๋ฅธ ์ธ์ฆ (๋ธŒ๋ผ์šฐ์ € ์ „ํ™˜ ๋ถˆํ•„์š”) -โœ… ๋” ์•ˆ์ •์  (URL Scheme ์ด์Šˆ ์—†์Œ) -โœ… ๋” ์•ˆ์ „ํ•จ (ID Token ์ง์ ‘ ๊ฒ€์ฆ) - -### ์›น OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ - -โœ… ํ‘œ์ค€ OAuth ํ”Œ๋กœ์šฐ -โœ… ์ถ”๊ฐ€ SDK ๋ถˆํ•„์š” -โœ… Supabase ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์› -โœ… ๊ฐ„๋‹จํ•œ ์„ค์ • - -## ๐Ÿ”„ ๋กœ๊ทธ์•„์›ƒ - -```dart -await GoogleAuthService.signOut(); -``` - -- ๋ชจ๋ฐ”์ผ: Google Sign-In SDK ๋กœ๊ทธ์•„์›ƒ + Supabase ๋กœ๊ทธ์•„์›ƒ -- ์›น: Supabase ๋กœ๊ทธ์•„์›ƒ๋งŒ ์ˆ˜ํ–‰ - -## ๐Ÿ“ ์ฐธ๊ณ  ๋ฌธ์„œ - -- [Google Sign-In for Flutter](https://pub.dev/packages/google_sign_in) -- [Supabase Auth with Google](https://supabase.com/docs/guides/auth/social-login/auth-google) -- [Google OAuth 2.0](https://developers.google.com/identity/protocols/oauth2) diff --git a/NONCE_FINAL_FIX.md b/NONCE_FINAL_FIX.md deleted file mode 100644 index 7450745..0000000 --- a/NONCE_FINAL_FIX.md +++ /dev/null @@ -1,293 +0,0 @@ -# ๐Ÿ”ง Nonce ๋ฌธ์ œ ์ตœ์ข… ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ - -## โŒ ๋ฌธ์ œ ์ƒํ™ฉ - -``` -AuthApiException(message: Passed nonce and nonce in id_token should either both exist or not., statusCode: 400, code: null) -``` - -## ๐Ÿ” ๊ทผ๋ณธ ์›์ธ - -Google Sign-In SDK๊ฐ€ **GIDClientID๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ nonce๋ฅผ ์ƒ์„ฑ**ํ•˜์ง€๋งŒ: - -1. **์šฐ๋ฆฌ๋Š” ์›๋ณธ raw nonce์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Œ** -2. ID Token์—๋Š” **ํ•ด์‹œ๋œ nonce**๊ฐ€ ํฌํ•จ๋จ -3. Supabase๋Š” **raw nonce**๋ฅผ ๊ธฐ๋Œ€ํ•จ -4. **nonce ๋ถˆ์ผ์น˜**๋กœ ์ธ์ฆ ์‹คํŒจ - -## โœ… ์ตœ์ข… ํ•ด๊ฒฐ์ฑ… - -### 1. iOS Info.plist์—์„œ GIDClientID ์ œ๊ฑฐ - -**ํŒŒ์ผ**: `ios/Runner/Info.plist` - -```xml - - - -``` - -**Before**: - -```xml -GIDClientID -YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -``` - -**After**: ์ œ๊ฑฐ๋จ - -### 2. GoogleAuthService์—์„œ serverClientId ์ œ๊ฑฐ - -**ํŒŒ์ผ**: `lib/services/google_auth_service.dart` - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - // โš ๏ธ serverClientId๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด nonce๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ - // ํ•˜์ง€๋งŒ ID Token์€ ์—ฌ์ „ํžˆ ๋ฐœ๊ธ‰๋จ (ํ”Œ๋žซํผ๋ณ„ Client ID ์‚ฌ์šฉ) -); -``` - -### 3. Supabase signInWithIdToken์— nonce ์ „๋‹ฌํ•˜์ง€ ์•Š์Œ - -```dart -await SupabaseConfig.client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, - // nonce ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ -); -``` - ---- - -## ๐Ÿ“ฑ ํ”Œ๋žซํผ๋ณ„ ์„ค์ • - -### iOS - -**Info.plist**: GIDClientID ์ œ๊ฑฐ โœ… - -**Google Cloud Console**: - -- iOS OAuth Client ์ƒ์„ฑ -- Bundle ID: `com.example.runnerApp` -- **โš ๏ธ Client ID๋ฅผ ์•ฑ์— ์„ค์ •ํ•˜์ง€ ์•Š์Œ** (์ž๋™ ์ธ์‹) - -### Android - -**AndroidManifest.xml**: ๋ณ€๊ฒฝ ์—†์Œ - -**Google Cloud Console**: - -- Android OAuth Client ์ƒ์„ฑ -- Package name: `com.example.runnerApp` -- SHA-1 ๋“ฑ๋ก - -### Web - -**์ž๋™ ์ฒ˜๋ฆฌ๋จ** - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -### ์˜ˆ์ƒ ๋กœ๊ทธ - -``` -[GoogleAuthService] === Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ === -[GoogleAuthService] ํ”Œ๋žซํผ: ios -[GoogleAuthService] โœ… Google ์ธ์ฆ ์™„๋ฃŒ: user@example.com -[GoogleAuthService] โœ… Google ID Token ํš๋“ -[GoogleAuthService] ๐Ÿ” Supabase ์ธ์ฆ ์‹œ์ž‘... -[GoogleAuthService] โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@example.com -``` - -### โœ… ์„ฑ๊ณต ๊ธฐ์ค€ - -- โŒ "Nonces mismatch" ์˜ค๋ฅ˜ ์—†์Œ -- โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ -- โœ… ์‚ฌ์šฉ์ž ์ •๋ณด ํ‘œ์‹œ -- โœ… ์„ธ์…˜ ์œ ์ง€ - ---- - -## ๐Ÿ” ๋ณด์•ˆ ๊ฒ€์ฆ - -### Q: GIDClientID ์—†์ด ์•ˆ์ „ํ•œ๊ฐ€? - -**A: ์˜ˆ, ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.** - -1. **ID Token์€ ์—ฌ์ „ํžˆ ๋ฐœ๊ธ‰๋จ** - - - Google์ด ์„œ๋ช…ํ•œ JWT - - ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํฌํ•จ (1์‹œ๊ฐ„) - - ์žฌ์ƒ ๊ณต๊ฒฉ ๋ฐฉ์ง€ - -2. **Supabase๊ฐ€ ID Token ๊ฒ€์ฆ** - - - Google ๊ณต๊ฐœํ‚ค๋กœ ์„œ๋ช… ๊ฒ€์ฆ - - Issuer/Audience ๊ฒ€์ฆ - - ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๊ฒ€์ฆ - -3. **ํ”Œ๋žซํผ๋ณ„ OAuth Client ์‚ฌ์šฉ** - - iOS: Bundle ID๋กœ ์ž๋™ ์ธ์‹ - - Android: SHA-1๋กœ ๊ฒ€์ฆ - - Web: ์ž๋™ ์ฒ˜๋ฆฌ - -### Q: nonce ์—†์ด๋„ ์•ˆ์ „ํ•œ๊ฐ€? - -**A: ์˜ˆ, ID Token ์ž์ฒด๊ฐ€ ์ถฉ๋ถ„ํžˆ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.** - -- **Google์ด ์„œ๋ช…**: ์œ„์กฐ ๋ถˆ๊ฐ€ -- **๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํฌํ•จ**: ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (1์‹œ๊ฐ„) -- **Audience ๊ฒ€์ฆ**: ํŠน์ • ์•ฑ์—๋งŒ ์œ ํšจ -- **HTTPS ์ „์†ก**: ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ ๋ฐฉ์ง€ - ---- - -## ๐Ÿ“Š Before vs After - -### Before (๋ฌธ์ œ ๋ฐœ์ƒ) - -```dart -// iOS Info.plist -GIDClientID -WEB-CLIENT-ID // โŒ nonce ์ƒ์„ฑ - -// GoogleSignIn -serverClientId: 'WEB-CLIENT-ID' // โŒ nonce ์ƒ์„ฑ - -// Supabase -signInWithIdToken( - nonce: extractedNonce, // โŒ raw nonce ์ ‘๊ทผ ๋ถˆ๊ฐ€ -) -``` - -**๊ฒฐ๊ณผ**: `Nonces mismatch` ์˜ค๋ฅ˜ - -### After (ํ•ด๊ฒฐ) - -```dart -// iOS Info.plist - // โœ… nonce ์ƒ์„ฑ ์•ˆ ํ•จ - -// GoogleSignIn -// serverClientId ์—†์Œ // โœ… nonce ์ƒ์„ฑ ์•ˆ ํ•จ - -// Supabase -signInWithIdToken( - // nonce ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ // โœ… ๊ฒ€์ฆ ์Šคํ‚ต -) -``` - -**๊ฒฐ๊ณผ**: ๋กœ๊ทธ์ธ ์„ฑ๊ณต! ๐ŸŽ‰ - ---- - -## ๐Ÿšจ ๋ฌธ์ œ ํ•ด๊ฒฐ - -### ID Token์„ ๋ฐ›์ง€ ๋ชปํ•จ - -**์›์ธ**: Google Cloud Console ์„ค์ • ์˜ค๋ฅ˜ - -**ํ•ด๊ฒฐ**: - -1. iOS OAuth Client๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ -2. Bundle ID๊ฐ€ ์ •ํ™•ํ•œ์ง€ ํ™•์ธ (`com.example.runnerApp`) -3. `CFBundleURLSchemes`์— reverse Client ID ์ถ”๊ฐ€: - ```xml - com.googleusercontent.apps.YOUR-IOS-CLIENT-ID - ``` - -### "Google Sign-In failed" - -**์›์ธ**: OAuth Client ๋ฏธ๋“ฑ๋ก - -**ํ•ด๊ฒฐ**: - -1. Google Cloud Console์—์„œ iOS OAuth Client ์ƒ์„ฑ -2. Bundle ID ๋“ฑ๋ก -3. Reverse Client ID๋ฅผ Info.plist์— ์ถ”๊ฐ€ - -### ์—ฌ์ „ํžˆ nonce ์˜ค๋ฅ˜ ๋ฐœ์ƒ - -**์›์ธ**: ์ด์ „ ๋นŒ๋“œ ์บ์‹œ - -**ํ•ด๊ฒฐ**: - -```bash -# iOS ํด๋ฆฐ ๋นŒ๋“œ -cd ios && rm -rf Pods Podfile.lock && pod install && cd .. -flutter clean -flutter pub get -flutter run -``` - ---- - -## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ - -- `GOOGLE_NATIVE_LOGIN_COMPLETE.md` - ์™„์ „ํ•œ ๊ฐ€์ด๋“œ -- `NONCE_ISSUE_SOLVED.md` - ์ดˆ๊ธฐ nonce ํ•ด๊ฒฐ ์‹œ๋„ -- `REFACTORING_COMPLETE.md` - ์ „์ฒด ๋ฆฌํŒฉํ„ฐ๋ง ์š”์•ฝ - ---- - -## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -### iOS ์„ค์ • - -- [ ] `Info.plist`์—์„œ `GIDClientID` ์ œ๊ฑฐ -- [ ] `CFBundleURLSchemes`์— reverse Client ID ์ถ”๊ฐ€ -- [ ] Google Cloud Console์— iOS OAuth Client ๋“ฑ๋ก -- [ ] Bundle ID ํ™•์ธ: `com.example.runnerApp` - -### Android ์„ค์ • - -- [ ] Google Cloud Console์— Android OAuth Client ๋“ฑ๋ก -- [ ] Package name: `com.example.runnerApp` -- [ ] SHA-1 ๋“ฑ๋ก -- [ ] `AndroidManifest.xml`์— Intent Filter ์„ค์ • - -### ์ฝ”๋“œ ์„ค์ • - -- [ ] `GoogleSignIn`์— `serverClientId` ์—†์Œ -- [ ] `signInWithIdToken`์— `nonce` ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ -- [ ] ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ™•์ธ - -### Google Cloud Console - -- [ ] iOS OAuth Client ์ƒ์„ฑ -- [ ] Android OAuth Client ์ƒ์„ฑ -- [ ] Web OAuth Client ์ƒ์„ฑ (Supabase์šฉ) -- [ ] Supabase Dashboard์— Web Client ID/Secret ์„ค์ • - ---- - -## ๐ŸŽ‰ ์™„๋ฃŒ! - -์ด์ œ **nonce ์˜ค๋ฅ˜ ์—†์ด, ๋ธŒ๋ผ์šฐ์ € ์—†์ด, ๋น ๋ฅด๊ฒŒ** Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! - -**ํ…Œ์ŠคํŠธ**: - -```bash -flutter run -d iPhone -``` - -๋กœ๊ทธ์ธ โ†’ Google ๊ณ„์ • ์„ ํƒ โ†’ ์ฆ‰์‹œ ์™„๋ฃŒ! ๐Ÿš€ - ---- - -## ๐Ÿ”„ ๋กค๋ฐฑ ๋ฐฉ๋ฒ• - -๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋กœ ๋กค๋ฐฑ ๊ฐ€๋Šฅ: - -```dart -await client.auth.signInWithOAuth( - OAuthProvider.google, - redirectTo: 'com.example.runnerApp://login-callback', -); -``` - -**๋‹จ์ **: ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆผ, ๋А๋ฆผ -**์žฅ์ **: nonce ๋ฌธ์ œ ์—†์Œ, 100% ์ž‘๋™ ๋ณด์žฅ diff --git a/NONCE_FIX.md b/NONCE_FIX.md deleted file mode 100644 index f8f5100..0000000 --- a/NONCE_FIX.md +++ /dev/null @@ -1,217 +0,0 @@ -# Google Sign-In Nonce ์˜ค๋ฅ˜ ์ˆ˜์ • - -## ๐Ÿšจ ์˜ค๋ฅ˜ ์ฆ์ƒ - -``` -AuthApiException( - message: Passed nonce and nonce in id_token should either both exist or not., - statusCode: 400, - code: null -) -``` - -Google ID Token์„ ์„ฑ๊ณต์ ์œผ๋กœ ํš๋“ํ–ˆ์ง€๋งŒ, Supabase `signInWithIdToken` ํ˜ธ์ถœ ์‹œ nonce ๊ด€๋ จ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. - -## ๐Ÿ” ์›์ธ ๋ถ„์„ - -### ๋ฌธ์ œ์˜ ํ•ต์‹ฌ - -Supabase๋Š” **nonce ์ผ๊ด€์„ฑ**์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค: - -- ID Token์— `nonce` ํด๋ ˆ์ž„์ด **์žˆ์œผ๋ฉด** โ†’ `signInWithIdToken`์—๋„ `nonce` ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์ˆ˜ -- ID Token์— `nonce` ํด๋ ˆ์ž„์ด **์—†์œผ๋ฉด** โ†’ `signInWithIdToken`์— `nonce` ํŒŒ๋ผ๋ฏธํ„ฐ ์ƒ๋žต - -### Google Sign-In์˜ nonce ์ฒ˜๋ฆฌ - -Google Sign-In SDK๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ: - -1. **nonce๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ** (๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ) -2. ์ผ๋ถ€ ์„ค์ •์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ nonce๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Œ -3. ID Token์— nonce๊ฐ€ ํฌํ•จ๋˜๋ฉด Supabase์—๋„ ์ „๋‹ฌํ•ด์•ผ ํ•จ - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### 1. Supabase ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ - -```yaml -# pubspec.yaml -dependencies: - supabase_flutter: ^2.10.0 # ์ด์ „: ^2.8.0 -``` - -### 2. ID Token์—์„œ nonce ์ž๋™ ์ถ”์ถœ - -```dart -/// ๋ชจ๋ฐ”์ผ์šฉ Google ๋กœ๊ทธ์ธ (๋„ค์ดํ‹ฐ๋ธŒ + ID Token) -static Future _signInWithGoogleMobile() async { - // 1. Google Sign-In - final googleUser = await _googleSignIn.signIn(); - final googleAuth = await googleUser.authentication; - final idToken = googleAuth.idToken; - - // 2. ID Token์—์„œ nonce ์ถ”์ถœ - String? nonce; - try { - final parts = idToken.split('.'); - if (parts.length == 3) { - final payload = parts[1]; - final normalized = base64.normalize(payload); - final decoded = utf8.decode(base64.decode(normalized)); - final json = jsonDecode(decoded) as Map; - nonce = json['nonce'] as String?; - } - } catch (e) { - developer.log('ID Token ๋””์ฝ”๋”ฉ ์‹คํŒจ: $e'); - } - - // 3. nonce ์œ ๋ฌด์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ํ˜ธ์ถœ - final response = nonce != null - ? await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, - nonce: nonce, // nonce ํฌํ•จ - ) - : await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, // nonce ์ƒ๋žต - ); -} -``` - -## ๐Ÿ”ง ์ „์ฒด ์ฝ”๋“œ ๊ตฌ์กฐ - -### imports ์ถ”๊ฐ€ - -```dart -import 'dart:math'; -import 'dart:convert'; -``` - -### ํ•ต์‹ฌ ๋กœ์ง - -1. **ID Token ๋””์ฝ”๋”ฉ**: JWT ํ† ํฐ์„ Base64 ๋””์ฝ”๋”ฉํ•˜์—ฌ payload ์ถ”์ถœ -2. **nonce ํด๋ ˆ์ž„ ํ™•์ธ**: `json['nonce']` ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ -3. **์กฐ๊ฑด๋ถ€ ํ˜ธ์ถœ**: nonce ์œ ๋ฌด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ `signInWithIdToken` ํ˜ธ์ถœ - -## ๐Ÿ“Š ๋™์ž‘ ํ๋ฆ„ - -### Case 1: nonce๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๋Œ€๋ถ€๋ถ„) - -``` -Google Sign-In โ†’ ID Token (nonce ์—†์Œ) -โ†’ Supabase signInWithIdToken(nonce ์ƒ๋žต) -โ†’ โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต -``` - -### Case 2: nonce๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ - -``` -Google Sign-In โ†’ ID Token (nonce ํฌํ•จ) -โ†’ ID Token ๋””์ฝ”๋”ฉ โ†’ nonce ์ถ”์ถœ -โ†’ Supabase signInWithIdToken(nonce ํฌํ•จ) -โ†’ โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต -``` - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -```bash -flutter test test/unit/services/google_auth_platform_test.dart -``` - -**๊ฒฐ๊ณผ**: โœ… 3/3 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ - -## ๐Ÿ” ๋””๋ฒ„๊น… ๋กœ๊ทธ - -### ์„ฑ๊ณต์ ์ธ ๋กœ๊ทธ์ธ - -``` -[GoogleAuthService] ๋ชจ๋ฐ”์ผ ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ -[GoogleAuthService] Google ์‚ฌ์šฉ์ž ์ธ์ฆ ์™„๋ฃŒ: user@gmail.com -[GoogleAuthService] Google ID Token ํš๋“ ์™„๋ฃŒ -[GoogleAuthService] ID Token์— nonce ์—†์Œ -[GoogleAuthService] Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@gmail.com -``` - -๋˜๋Š” - -``` -[GoogleAuthService] ๋ชจ๋ฐ”์ผ ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ -[GoogleAuthService] Google ์‚ฌ์šฉ์ž ์ธ์ฆ ์™„๋ฃŒ: user@gmail.com -[GoogleAuthService] Google ID Token ํš๋“ ์™„๋ฃŒ -[GoogleAuthService] ID Token์—์„œ nonce ๋ฐœ๊ฒฌ: abc123def4... -[GoogleAuthService] Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@gmail.com -``` - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -### 1. base64 ํŒจํ‚ค์ง€ ์‚ฌ์šฉ - -`base64.normalize()`์„ ์‚ฌ์šฉํ•˜์—ฌ Base64 URL-safe ๋””์ฝ”๋”ฉ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. - -### 2. ์—๋Ÿฌ ์ฒ˜๋ฆฌ - -ID Token ๋””์ฝ”๋”ฉ ์‹คํŒจ ์‹œ์—๋„ ๋กœ๊ทธ์ธ์„ ๊ณ„์† ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค (nonce ์—†๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ). - -### 3. Supabase ๋ฒ„์ „ - -`supabase_flutter: ^2.10.0` ์ด์ƒ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. - -## ๐ŸŽฏ ์™œ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋‚˜? - -### Apple Sign In๊ณผ์˜ ์ฐจ์ด - -- **Apple Sign In**: nonce๊ฐ€ **ํ•„์ˆ˜** - - ```dart - final rawNonce = generateNonce(); - // Apple Sign In with rawNonce - // ID Token์— nonce ํฌํ•จ๋จ - await client.auth.signInWithIdToken( - provider: OAuthProvider.apple, - idToken: idToken, - nonce: rawNonce, // ํ•„์ˆ˜! - ); - ``` - -- **Google Sign In**: nonce๊ฐ€ **์„ ํƒ์ ** - ```dart - // Google Sign In (nonce ์ž๋™ ์ƒ์„ฑ ์•ˆ ํ•จ) - // ID Token์— nonce ์—†์Œ - await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - // nonce ์ƒ๋žต ๊ฐ€๋Šฅ - ); - ``` - -### Supabase์˜ ๊ฒ€์ฆ ๋กœ์ง - -Supabase๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด nonce ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค: - -```python -# Supabase ๋ฐฑ์—”๋“œ ์˜์‚ฌ ์ฝ”๋“œ -if id_token.has_nonce() and not request.has_nonce(): - raise AuthApiException("nonce mismatch") -if not id_token.has_nonce() and request.has_nonce(): - raise AuthApiException("nonce mismatch") -``` - -## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ - -- [Supabase Auth - Google Sign In](https://supabase.com/docs/guides/auth/social-login/auth-google) -- [JWT.io - Token Inspector](https://jwt.io/) -- [Google Sign-In Flutter Plugin](https://pub.dev/packages/google_sign_in) - -## ๐ŸŽ“ ํ•™์Šต ํฌ์ธํŠธ - -1. **JWT ๊ตฌ์กฐ ์ดํ•ด**: Header.Payload.Signature ํ˜•์‹ -2. **Base64 URL-safe ๋””์ฝ”๋”ฉ**: ํŒจ๋”ฉ ์ฒ˜๋ฆฌ์˜ ์ค‘์š”์„ฑ -3. **OAuth ๋ณด์•ˆ**: nonce์˜ ์—ญํ•  (replay attack ๋ฐฉ์ง€) -4. **ํ”Œ๋žซํผ๋ณ„ ์ฐจ์ด**: Apple vs Google Sign-In์˜ nonce ์ฒ˜๋ฆฌ ์ฐจ์ด - ---- - -**์ˆ˜์ • ์™„๋ฃŒ**: 2025-10-11 -**์˜ค๋ฅ˜ ํ•ด๊ฒฐ**: ID Token์—์„œ nonce ์ž๋™ ์ถ”์ถœ ํ›„ ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ โœ… diff --git a/NONCE_ISSUE_SOLVED.md b/NONCE_ISSUE_SOLVED.md deleted file mode 100644 index a5fa631..0000000 --- a/NONCE_ISSUE_SOLVED.md +++ /dev/null @@ -1,206 +0,0 @@ -# ๐Ÿ”ง Google Sign-In Nonce ๋ฌธ์ œ ํ•ด๊ฒฐ - -## ๐Ÿ”ด ๋ฌธ์ œ ์ƒํ™ฉ - -``` -AuthApiException(message: Passed nonce and nonce in id_token should either both exist or not., statusCode: 400, code: null) -``` - -### ์›์ธ - -Google Sign-In Flutter SDK๊ฐ€ `serverClientId`๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ: - -1. **์ž๋™์œผ๋กœ nonce๋ฅผ ์ƒ์„ฑ**ํ•˜์—ฌ ID Token์— ํฌํ•จ -2. ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” **์›๋ณธ nonce์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Œ** -3. Supabase๋Š” **์šฐ๋ฆฌ๊ฐ€ ์ „๋‹ฌํ•œ nonce**์™€ **ID Token์˜ nonce**๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•จ -4. ๋ถˆ์ผ์น˜๋กœ ์ธํ•ด ์˜ค๋ฅ˜ ๋ฐœ์ƒ - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### ๋ฐฉ๋ฒ• 1: serverClientId ์ œ๊ฑฐ (์„ ํƒ๋œ ํ•ด๊ฒฐ์ฑ…) - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - // serverClientId ์—†์ด ์‚ฌ์šฉ (nonce ๋ฌธ์ œ ๋ฐฉ์ง€) - // ํ”Œ๋žซํผ๋ณ„ Google OAuth Client ID๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์„ค์ •์—์„œ ์ž๋™ ์‚ฌ์šฉ -); -``` - -**์žฅ์ **: - -- โœ… Nonce ๋ฌธ์ œ ์™„์ „ ํ•ด๊ฒฐ -- โœ… ์„ค์ • ๋‹จ์ˆœํ™” -- โœ… ID Token ์—ฌ์ „ํžˆ ๋ฐœ๊ธ‰๋จ (ํ”Œ๋žซํผ๋ณ„ Client ID ์‚ฌ์šฉ) - -**๋‹จ์ **: - -- โš ๏ธ ID Token ๊ฒ€์ฆ์ด ํ”Œ๋žซํผ๋ณ„ Client ID๋กœ๋งŒ ๊ฐ€๋Šฅ - -### ๋ฐฉ๋ฒ• 2: Supabase์— nonce ์ „๋‹ฌํ•˜์ง€ ์•Š๊ธฐ - -```dart -await SupabaseConfig.client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, - // nonce ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ - Google Sign-In์ด ์ž๋™ ์ƒ์„ฑํ•˜๋Š” nonce์™€ ์ถฉ๋Œ ๋ฐฉ์ง€ -); -``` - -**์žฅ์ **: - -- โœ… Supabase๊ฐ€ nonce ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Œ -- โœ… serverClientId ์‚ฌ์šฉ ๊ฐ€๋Šฅ - -**๋‹จ์ **: - -- โš ๏ธ Nonce ๊ฒ€์ฆ์ด ์Šคํ‚ต๋จ (๋ณด์•ˆ์ƒ ์•ฝ๊ฐ„ ์•ฝํ™”) - -## ๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ - -### Nonce์˜ ์—ญํ•  - -Nonce๋Š” **์žฌ์ƒ ๊ณต๊ฒฉ(Replay Attack)** ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค: - -1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ณ ์œ ํ•œ nonce ์ƒ์„ฑ -2. Google์ด nonce๋ฅผ ID Token์— ํฌํ•จ -3. ์„œ๋ฒ„๊ฐ€ nonce๋ฅผ ๊ฒ€์ฆํ•˜์—ฌ Token์˜ ์‹ ์„ ๋„ ํ™•์ธ - -### serverClientId ์—†์ด ์‚ฌ์šฉํ•ด๋„ ์•ˆ์ „ํ•œ ์ด์œ  - -1. **ID Token ์ž์ฒด๊ฐ€ JWT** - - - Google์ด ์„œ๋ช… - - ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํฌํ•จ (1์‹œ๊ฐ„) - - ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€ - -2. **Supabase๊ฐ€ ID Token ๊ฒ€์ฆ** - - - Google ๊ณต๊ฐœํ‚ค๋กœ ์„œ๋ช… ๊ฒ€์ฆ - - Issuer, Audience ๊ฒ€์ฆ - - ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๊ฒ€์ฆ - -3. **ํ”Œ๋žซํผ๋ณ„ Client ID ์‚ฌ์šฉ** - - iOS: `Info.plist`์˜ `GIDClientID` - - Android: Google Cloud Console์˜ SHA-1 - - Web: ์ž๋™ ์ฒ˜๋ฆฌ - -## ๐Ÿ“ฑ ํ”Œ๋žซํผ๋ณ„ ์„ค์ • - -### iOS (Info.plist) - -```xml - -GIDClientID -YOUR-IOS-CLIENT-ID.apps.googleusercontent.com - - -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.YOUR-IOS-CLIENT-ID - - - -``` - -### Android (Google Cloud Console) - -1. **Android OAuth Client ์ƒ์„ฑ** - - Package name: `com.example.runnerApp` - - SHA-1 ๋“ฑ๋ก: `./gradlew signingReport` - -### Web - -**์ž๋™ ์ฒ˜๋ฆฌ๋จ** - ์ถ”๊ฐ€ ์„ค์ • ๋ถˆํ•„์š” - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -### ๋กœ๊ทธ ํ™•์ธ - -```dart -[GoogleAuthService] === Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ === -[GoogleAuthService] ํ”Œ๋žซํผ: ios -[GoogleAuthService] Google ์‚ฌ์šฉ์ž ์ธ์ฆ ์™„๋ฃŒ: user@example.com -[GoogleAuthService] โœ… Google ID Token ํš๋“ -[GoogleAuthService] ๐Ÿ” Supabase ์ธ์ฆ ์‹œ์ž‘... -[GoogleAuthService] โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@example.com -[GoogleAuthService] === Google ๋กœ๊ทธ์ธ ์™„๋ฃŒ === -``` - -### ์„ฑ๊ณต ๊ธฐ์ค€ - -- โœ… "Nonce mismatch" ์˜ค๋ฅ˜ ์—†์Œ -- โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ -- โœ… ์‚ฌ์šฉ์ž ์ •๋ณด ์ •์ƒ ํ‘œ์‹œ -- โœ… ์„ธ์…˜ ์œ ์ง€ ํ™•์ธ - -## ๐ŸŽฏ ์š”์•ฝ - -### Before (๋ฌธ์ œ ๋ฐœ์ƒ) - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - serverClientId: 'WEB-CLIENT-ID.apps.googleusercontent.com', // โŒ nonce ์ž๋™ ์ƒ์„ฑ -); - -await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, - nonce: extractedNonce, // โŒ ์›๋ณธ nonce์— ์ ‘๊ทผ ๋ถˆ๊ฐ€ -); -``` - -**๊ฒฐ๊ณผ**: `Nonces mismatch` ์˜ค๋ฅ˜ - -### After (ํ•ด๊ฒฐ) - -```dart -static final GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: ['email', 'profile'], - // serverClientId ์—†์Œ โœ… nonce ์ƒ์„ฑ ์•ˆ ํ•จ -); - -await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: accessToken, - // nonce ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ โœ… ๊ฒ€์ฆ ์Šคํ‚ต -); -``` - -**๊ฒฐ๊ณผ**: ๋กœ๊ทธ์ธ ์„ฑ๊ณต! ๐ŸŽ‰ - -## ๐Ÿ“š ๊ด€๋ จ ์ด์Šˆ - -- [Supabase Auth - Google Sign-In with Flutter](https://github.com/supabase/supabase-flutter/issues/xxx) -- [google_sign_in - Nonce Support](https://github.com/flutter/packages/issues/xxx) - -## ๐Ÿ”„ ๋Œ€์•ˆ: OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ - -Nonce ๋ฌธ์ œ๊ฐ€ ๊ณ„์†๋˜๋ฉด OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ ๊ฐ€๋Šฅ: - -```dart -await client.auth.signInWithOAuth( - OAuthProvider.google, - redirectTo: 'com.example.runnerApp://login-callback', -); -``` - -**๋‹จ์ **: ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆผ, ๋”ฅ๋งํฌ ํ•„์š” - -## โœ… ์ตœ์ข… ๊ถŒ์žฅ์‚ฌํ•ญ - -1. **serverClientId ์—†์ด ์‚ฌ์šฉ** (ํ˜„์žฌ ๊ตฌํ˜„) -2. **ํ”Œ๋žซํผ๋ณ„ Client ID ์„ค์ •** (Info.plist, SHA-1) -3. **Supabase์— nonce ์ „๋‹ฌํ•˜์ง€ ์•Š๊ธฐ** -4. **ID Token ๊ฒ€์ฆ์€ Supabase์— ์œ„์ž„** - -์ด ๋ฐฉ์‹์œผ๋กœ **๋ธŒ๋ผ์šฐ์ € ์—†์ด, ์•ˆ์ „ํ•˜๊ฒŒ, ๋น ๋ฅด๊ฒŒ** Google ๋กœ๊ทธ์ธ์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! ๐Ÿš€ diff --git a/PROFILE_NULL_FIX.md b/PROFILE_NULL_FIX.md deleted file mode 100644 index 993c2f3..0000000 --- a/PROFILE_NULL_FIX.md +++ /dev/null @@ -1,370 +0,0 @@ -# ๐Ÿ”ง ํ”„๋กœํ•„ Null ์˜ค๋ฅ˜ ๋ฐ ์ค‘๋ณต ์ƒ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ - -## โŒ ๋ฐœ์ƒํ–ˆ๋˜ ๋ฌธ์ œ - -``` -[UserProfileService] ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: type 'Null' is not a subtype of type 'String' in type cast -[GoogleAuthService] โœจ ์‹ ๊ทœ ์‚ฌ์šฉ์ž, ํ”„๋กœํ•„ ์ƒ์„ฑ -[UserProfileService] ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ƒ์„ฑ ์˜ค๋ฅ˜: PostgrestException(message: duplicate key value violates unique constraint "user_profiles_pkey", code: 23505, details: Conflict, hint: null) -``` - ---- - -## ๐Ÿ” ์›์ธ ๋ถ„์„ - -### 1. **Null ํƒ€์ž… ์บ์ŠคํŒ… ์˜ค๋ฅ˜** - -**๋ฌธ์ œ**: - -- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ”„๋กœํ•„์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ `email` ํ•„๋“œ๊ฐ€ null -- `UserProfile.fromJson`์ด ํ•„์ˆ˜ ํ•„๋“œ๋ฅผ String์œผ๋กœ ์บ์ŠคํŒ… ์‹œ๋„ -- **ํƒ€์ž… ์˜ค๋ฅ˜ ๋ฐœ์ƒ** - -**๊ทผ๋ณธ ์›์ธ**: - -- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ์™€ ๋ชจ๋ธ ๋ถˆ์ผ์น˜ -- ๋˜๋Š” ๋ฐ์ดํ„ฐ ์†์ƒ - -### 2. **์ค‘๋ณต ํ‚ค ์ œ์•ฝ ์œ„๋ฐ˜ (23505)** - -**๋ฌธ์ œ**: - -- ํ”„๋กœํ•„ ์กฐํšŒ ์‹คํŒจ โ†’ ์‹ ๊ทœ ์‚ฌ์šฉ์ž๋กœ ํŒ๋‹จ -- ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹œ๋„ โ†’ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ID๋กœ ์ค‘๋ณต ์ƒ์„ฑ ์‹œ๋„ -- **PostgreSQL unique constraint ์œ„๋ฐ˜** - -**๊ทผ๋ณธ ์›์ธ**: - -- ํ”„๋กœํ•„ ์กฐํšŒ ์‹œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ (null ์บ์ŠคํŒ…) -- ์˜ค๋ฅ˜๋ฅผ null๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ "ํ”„๋กœํ•„ ์—†์Œ"์œผ๋กœ ํŒ๋‹จ -- ์‹ค์ œ๋กœ๋Š” ํ”„๋กœํ•„ ์กด์žฌ โ†’ ์ค‘๋ณต ์ƒ์„ฑ ์‹œ๋„ - ---- - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### 1. **UserProfileService - Null ์•ˆ์ „ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€** - -**ํŒŒ์ผ**: `lib/services/user_profile_service.dart` - -```dart -/// ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ํ”„๋กœํ•„ ๊ฐ€์ ธ์˜ค๊ธฐ -static Future getCurrentUserProfile() async { - try { - final user = _supabase.auth.currentUser; - if (user == null) { - developer.log('ํ˜„์žฌ ์‚ฌ์šฉ์ž ์—†์Œ', name: 'UserProfileService'); - return null; - } - - final response = await _supabase - .from('user_profiles') - .select() - .eq('id', user.id) - .maybeSingle(); - - if (response == null) { - developer.log('์‚ฌ์šฉ์ž ํ”„๋กœํ•„์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค', name: 'UserProfileService'); - return null; - } - - // โœ… null ์•ˆ์ „ ๊ฒ€์ฆ: ํ•„์ˆ˜ ํ•„๋“œ ํ™•์ธ - if (response['id'] == null || response['email'] == null) { - developer.log( - 'โš ๏ธ ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ ๋ถˆ์™„์ „: id=${response['id']}, email=${response['email']}', - name: 'UserProfileService', - ); - return null; - } - - return UserProfile.fromJson(response); - } catch (e, stackTrace) { - developer.log( - '์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: $e', - name: 'UserProfileService', - error: e, - stackTrace: stackTrace, - ); - return null; - } -} -``` - -**๊ฐœ์„  ์‚ฌํ•ญ**: - -1. **ํ•„์ˆ˜ ํ•„๋“œ ์‚ฌ์ „ ๊ฒ€์ฆ**: `fromJson` ํ˜ธ์ถœ ์ „ null ์ฒดํฌ -2. **์ƒ์„ธ ๋กœ๊น…**: ์–ด๋–ค ํ•„๋“œ๊ฐ€ null์ธ์ง€ ๋ช…ํ™•ํžˆ ์ถœ๋ ฅ -3. **stackTrace ํฌํ•จ**: ๋””๋ฒ„๊น… ์šฉ์ด - ---- - -### 2. **GoogleAuthService - ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€** - -**ํŒŒ์ผ**: `lib/services/google_auth_service.dart` - -```dart -/// ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -static Future _handleUserProfile( - User supabaseUser, - GoogleSignInAccount googleUser, -) async { - try { - developer.log('๐Ÿ“ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ ์ค‘...', name: 'GoogleAuthService'); - - // โœ… ๊ธฐ์กด ํ”„๋กœํ•„ ํ™•์ธ (null ์•ˆ์ „ ์ฒ˜๋ฆฌ) - UserProfile? existingProfile; - try { - existingProfile = await UserProfileService.getCurrentUserProfile(); - } catch (e) { - developer.log('โš ๏ธ ํ”„๋กœํ•„ ์กฐํšŒ ์˜ค๋ฅ˜ (๋ฌด์‹œ): $e', name: 'GoogleAuthService'); - existingProfile = null; - } - - if (existingProfile == null) { - // ์‹ ๊ทœ ์‚ฌ์šฉ์ž: ํ”„๋กœํ•„ ์ƒ์„ฑ - developer.log('โœจ ์‹ ๊ทœ ์‚ฌ์šฉ์ž, ํ”„๋กœํ•„ ์ƒ์„ฑ', name: 'GoogleAuthService'); - - try { - await UserProfileService.createUserProfile( - email: supabaseUser.email ?? googleUser.email, - displayName: googleUser.displayName, - avatarUrl: googleUser.photoUrl, - ); - - developer.log('โœ… ํ”„๋กœํ•„ ์ƒ์„ฑ ์™„๋ฃŒ', name: 'GoogleAuthService'); - } on PostgrestException catch (e) { - // โœ… ์ค‘๋ณต ํ‚ค ์˜ค๋ฅ˜๋Š” ๋ฌด์‹œ (์ด๋ฏธ ํ”„๋กœํ•„ ์กด์žฌ) - if (e.code == '23505') { - developer.log('โ„น๏ธ ํ”„๋กœํ•„์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค (์ค‘๋ณต ์ƒ์„ฑ ์Šคํ‚ต)', name: 'GoogleAuthService'); - } else { - rethrow; - } - } - } else { - // ๊ธฐ์กด ์‚ฌ์šฉ์ž: ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ (ํ•„์š”์‹œ) - // ... ๊ธฐ์กด ๋กœ์ง - } - } catch (e) { - developer.log('โŒ ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: $e', name: 'GoogleAuthService'); - rethrow; - } -} -``` - -**๊ฐœ์„  ์‚ฌํ•ญ**: - -1. **ํ”„๋กœํ•„ ์กฐํšŒ ์˜ค๋ฅ˜ ๊ฒฉ๋ฆฌ**: try-catch๋กœ ๊ฐ์‹ธ์„œ ์˜ค๋ฅ˜ ๋ฌด์‹œ -2. **PostgrestException ์ฒ˜๋ฆฌ**: ์ค‘๋ณต ํ‚ค ์˜ค๋ฅ˜(23505) ๋ช…์‹œ์  ์ฒ˜๋ฆฌ -3. **์ƒ์„ธ ๋กœ๊น…**: ๊ฐ ๋‹จ๊ณ„๋ณ„ ๋ช…ํ™•ํ•œ ๋กœ๊ทธ - ---- - -## ๐Ÿ“Š Before vs After - -### Before (๋ฌธ์ œ ๋ฐœ์ƒ) - -``` -1. ํ”„๋กœํ•„ ์กฐํšŒ - โ†’ Null ์บ์ŠคํŒ… ์˜ค๋ฅ˜ ๋ฐœ์ƒ - โ†’ catch์—์„œ null ๋ฐ˜ํ™˜ - -2. existingProfile == null - โ†’ "์‹ ๊ทœ ์‚ฌ์šฉ์ž"๋กœ ํŒ๋‹จ - -3. ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹œ๋„ - โ†’ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ID - โ†’ duplicate key ์˜ค๋ฅ˜ ๋ฐœ์ƒ - -4. ์•ฑ ํฌ๋ž˜์‹œ ๋˜๋Š” ๋กœ๊ทธ์ธ ์‹คํŒจ -``` - -### After (ํ•ด๊ฒฐ) - -``` -1. ํ”„๋กœํ•„ ์กฐํšŒ - โ†’ ํ•„์ˆ˜ ํ•„๋“œ ์‚ฌ์ „ ๊ฒ€์ฆ - โ†’ null์ด๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ null ๋ฐ˜ํ™˜ - -2. existingProfile == null - โ†’ ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹œ๋„ - -3. PostgrestException (23505) - โ†’ "ํ”„๋กœํ•„ ์ด๋ฏธ ์กด์žฌ" ๋กœ๊ทธ - โ†’ ์˜ค๋ฅ˜ ๋ฌด์‹œํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰ - -4. ๋กœ๊ทธ์ธ ์„ฑ๊ณต! โœ… -``` - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -### โœ… ์„ฑ๊ณต ๋กœ๊ทธ - -``` -[GoogleAuthService] === Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์‹œ์ž‘ === -[GoogleAuthService] ํ”Œ๋žซํผ: ios -[GoogleAuthService] โœ… Google ์ธ์ฆ ์™„๋ฃŒ: user@example.com -[GoogleAuthService] โœ… Google ID Token ํš๋“ -[GoogleAuthService] ๐Ÿ” Supabase ์ธ์ฆ ์‹œ์ž‘... -[GoogleAuthService] โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ: user@example.com -[GoogleAuthService] ๐Ÿ“ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ ์ค‘... -[UserProfileService] โš ๏ธ ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ ๋ถˆ์™„์ „: id=xxx, email=null -[GoogleAuthService] โš ๏ธ ํ”„๋กœํ•„ ์กฐํšŒ ์˜ค๋ฅ˜ (๋ฌด์‹œ): ... -[GoogleAuthService] โœจ ์‹ ๊ทœ ์‚ฌ์šฉ์ž, ํ”„๋กœํ•„ ์ƒ์„ฑ -[GoogleAuthService] โ„น๏ธ ํ”„๋กœํ•„์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค (์ค‘๋ณต ์ƒ์„ฑ ์Šคํ‚ต) -[GoogleAuthService] === Google ๋กœ๊ทธ์ธ ์™„๋ฃŒ === -``` - -**โœ… ์˜ค๋ฅ˜ ์—†์ด ๋กœ๊ทธ์ธ ์„ฑ๊ณต!** - ---- - -## ๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํ™•์ธ - -### ํ•„์ˆ˜ ์ฒดํฌ์‚ฌํ•ญ - -```sql --- user_profiles ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ํ™•์ธ -SELECT column_name, data_type, is_nullable -FROM information_schema.columns -WHERE table_name = 'user_profiles'; - --- email ์ปฌ๋Ÿผ์ด NOT NULL์ธ์ง€ ํ™•์ธ -ALTER TABLE user_profiles -ALTER COLUMN email SET NOT NULL; - --- ์†์ƒ๋œ ๋ฐ์ดํ„ฐ ํ™•์ธ -SELECT id, email, created_at -FROM user_profiles -WHERE email IS NULL; -``` - -### ๊ถŒ์žฅ ์Šคํ‚ค๋งˆ - -```sql -CREATE TABLE user_profiles ( - id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, - email TEXT NOT NULL, -- โœ… NOT NULL ํ•„์ˆ˜ - display_name TEXT, - avatar_url TEXT, - birth_date DATE, - gender TEXT, - height INTEGER, - weight NUMERIC(5,2), - fitness_level TEXT, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() -); - --- RLS ์ •์ฑ… ์„ค์ • -ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view own profile" - ON user_profiles FOR SELECT - USING (auth.uid() = id); - -CREATE POLICY "Users can update own profile" - ON user_profiles FOR UPDATE - USING (auth.uid() = id); - -CREATE POLICY "Users can insert own profile" - ON user_profiles FOR INSERT - WITH CHECK (auth.uid() = id); -``` - ---- - -## ๐Ÿšจ ์ถ”๊ฐ€ ๊ฐœ์„  ์‚ฌํ•ญ - -### 1. **๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜** - -์†์ƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋‹ค๋ฉด: - -```sql --- email์ด null์ธ ๋ ˆ์ฝ”๋“œ์— auth.users์˜ email ๋ณต์‚ฌ -UPDATE user_profiles -SET email = ( - SELECT email FROM auth.users - WHERE auth.users.id = user_profiles.id -) -WHERE email IS NULL; - --- ์—ฌ์ „ํžˆ null์ด๋ฉด ์‚ญ์ œ -DELETE FROM user_profiles WHERE email IS NULL; -``` - -### 2. **ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ** - -```sql --- ์‚ฌ์šฉ์ž ๊ฐ€์ž… ์‹œ ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ -CREATE OR REPLACE FUNCTION public.handle_new_user() -RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO public.user_profiles (id, email, created_at, updated_at) - VALUES ( - NEW.id, - NEW.email, - NOW(), - NOW() - ); - RETURN NEW; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE TRIGGER on_auth_user_created - AFTER INSERT ON auth.users - FOR EACH ROW - EXECUTE FUNCTION public.handle_new_user(); -``` - -์ด๋ ‡๊ฒŒ ํ•˜๋ฉด **ํ”„๋กœํ•„ ์ค‘๋ณต ์ƒ์„ฑ ๋ฌธ์ œ๊ฐ€ ์›์ฒœ์ ์œผ๋กœ ํ•ด๊ฒฐ**๋ฉ๋‹ˆ๋‹ค! - ---- - -## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -### ์ฝ”๋“œ ์ˆ˜์ • - -- [x] `UserProfileService.getCurrentUserProfile()` null ์•ˆ์ „ ์ฒ˜๋ฆฌ -- [x] `GoogleAuthService._handleUserProfile()` ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€ -- [x] PostgrestException 23505 ๋ช…์‹œ์  ์ฒ˜๋ฆฌ -- [x] ์ƒ์„ธ ๋กœ๊น… ์ถ”๊ฐ€ -- [x] import ์ถ”๊ฐ€ (`user_profile.dart`) - -### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์ธ - -- [ ] `user_profiles.email` ์ปฌ๋Ÿผ์ด NOT NULL์ธ์ง€ ํ™•์ธ -- [ ] ์†์ƒ๋œ ๋ฐ์ดํ„ฐ (email = null) ์ •๋ฆฌ -- [ ] RLS ์ •์ฑ… ์„ค์ • ํ™•์ธ -- [ ] ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ ์„ค์ • (์„ ํƒ) - -### ํ…Œ์ŠคํŠธ - -- [x] `flutter analyze` ํ†ต๊ณผ -- [x] 40/40 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ - ---- - -## ๐ŸŽ‰ ์™„๋ฃŒ! - -์ด์ œ **ํ”„๋กœํ•„ ์ค‘๋ณต ์ƒ์„ฑ ์˜ค๋ฅ˜** ๋ฐ **Null ํƒ€์ž… ์บ์ŠคํŒ… ์˜ค๋ฅ˜**๊ฐ€ ๋ชจ๋‘ ํ•ด๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! - -**์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ ํ๋ฆ„**: - -1. โœ… Google ์ธ์ฆ ์™„๋ฃŒ -2. โœ… Supabase ๋กœ๊ทธ์ธ ์™„๋ฃŒ -3. โœ… ํ”„๋กœํ•„ ์กฐํšŒ (์•ˆ์ „ํ•˜๊ฒŒ) -4. โœ… ํ”„๋กœํ•„ ์—†์œผ๋ฉด ์ƒ์„ฑ (์ค‘๋ณต ๋ฐฉ์ง€) -5. โœ… ํ”„๋กœํ•„ ์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ (ํ•„์š”์‹œ) -6. โœ… ๋กœ๊ทธ์ธ ์™„๋ฃŒ! ๐Ÿš€ - ---- - -## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ - -- `NONCE_FINAL_FIX.md` - Nonce ๋ฌธ์ œ ํ•ด๊ฒฐ -- `GOOGLE_NATIVE_LOGIN_COMPLETE.md` - ์ „์ฒด ๊ฐ€์ด๋“œ -- `DATABASE_SETUP.md` - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • -- `README.md` - ํ”„๋กœ์ ํŠธ ๊ฐœ์š” diff --git a/README.md b/README.md index bcb6ae3..88d8791 100644 --- a/README.md +++ b/README.md @@ -1,225 +1,1056 @@ -# ๐Ÿƒโ€โ™€๏ธ StrideNote - ๋Ÿฌ๋‹ ํŠธ๋ž˜์ปค ์•ฑ +
-StrideNote๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ฌ๋ฆฌ๊ธฐ๋ฅผ ํ•  ๋•Œ ๊ฑฐ๋ฆฌ, ์†๋„, ์‹ฌ๋ฐ•์ˆ˜, ๋Ÿฌ๋‹ ํŒจํ„ด์„ ์ž๋™ ๊ธฐ๋กํ•˜๊ณ , ๊ฐœ์ธ์˜ ์„ฑ์žฅ๊ณผ ํ”ผ๋“œ๋ฐฑ์„ ์ง๊ด€์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ์•ฑ์ž…๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ ๊ธฐ๋ก์ด ์•„๋‹Œ "๋Ÿฌ๋‹ ์Šคํ† ๋ฆฌ"๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฐœ์ธ ๋งž์ถคํ˜• ํŠธ๋ž˜์ปค์ž…๋‹ˆ๋‹ค. +# ๐Ÿƒโ€โ™€๏ธ StrideNote -## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ +### GPS ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๋Ÿฌ๋‹ ์ถ”์  ๋ฐ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ์•ฑ -### ๐ŸŽฏ ์ฝ”์–ด ๊ธฐ๋Šฅ +[![Flutter](https://img.shields.io/badge/Flutter-3.8.1-02569B?style=for-the-badge&logo=flutter&logoColor=white)](https://flutter.dev) +[![Dart](https://img.shields.io/badge/Dart-3.0+-0175C2?style=for-the-badge&logo=dart&logoColor=white)](https://dart.dev) +[![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?style=for-the-badge&logo=supabase&logoColor=white)](https://supabase.com) +[![Cursor AI](https://img.shields.io/badge/Cursor_AI-000000?style=for-the-badge&logo=cursor&logoColor=white)](https://cursor.sh) +[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](LICENSE) -- **๋Ÿฌ๋‹ ์ž๋™ ๊ธฐ๋ก**: GPS ๊ธฐ๋ฐ˜ ๊ฑฐ๋ฆฌ, ํŽ˜์ด์Šค, ์‹œ๊ฐ„, ๊ณ ๋„ ์ถ”์  -- **์‹ฌ๋ฐ•์ˆ˜ ์—ฐ๋™**: ์›จ์–ด๋Ÿฌ๋ธ” ๊ธฐ๊ธฐ ์—ฐ๋™ (HealthKit, Google Fit) -- **ํ›ˆ๋ จ ์š”์•ฝ ๋ฆฌํฌํŠธ**: ๋‹ฌ๋ฆฌ๊ธฐ ํ›„ ์ž๋™ ์ƒ์„ฑ -- **๋Ÿฌ๋‹ ํžˆ์Šคํ† ๋ฆฌ**: ์ฃผ/์›”๊ฐ„ ํ†ต๊ณ„ ์‹œ๊ฐํ™” + ๋ฐฐ์ง€ ์‹œ์Šคํ…œ +**๊ฐœ๋ฐœ ๊ธฐ๊ฐ„**: 2024.09 ~ 2025.10 (2๊ฐœ์›”) | **๊ฐœ๋ฐœ ์ธ์›**: 1์ธ (Full-Stack) | **๊ฐœ๋ฐœ ๋ฐฉ์‹**: AI Pair Programming -### ๐Ÿš€ ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ +[๐Ÿ“ฑ ์ฃผ์š” ํ™”๋ฉด](#-์ฃผ์š”-ํ™”๋ฉด) โ€ข [โœจ ํ•ต์‹ฌ ์„ฑ๊ณผ](#-ํ•ต์‹ฌ-์„ฑ๊ณผ--๊ฐœ์„ -์‚ฌํ•ญ) โ€ข [๐ŸŽฏ ๊ธฐ์ˆ ์  ๋„์ „](#-๊ธฐ์ˆ ์ -๋„์ „๊ณผ์ œ) โ€ข [๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ](#-๊ธฐ์ˆ -์Šคํƒ) โ€ข [๐Ÿ“š ๋ฌธ์„œ](#-๋ฌธ์„œ) -- ๋Ÿฌ๋‹ ํ”Œ๋žœ ์ถ”์ฒœ -- ์†Œ์…œ ๊ณต์œ  ๊ธฐ๋Šฅ -- ์Œ์•… ์—ฐ๋™ -- AI ๊ธฐ๋ฐ˜ ๊ฐœ์ธํ™”๋œ ํ”ผ๋“œ๋ฐฑ +
-## ๐ŸŽจ ๋””์ž์ธ ํŠน์ง• +--- + +## ๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +**StrideNote**๋Š” ๋Ÿฌ๋„ˆ๋“ค์„ ์œ„ํ•œ ์Šค๋งˆํŠธ ํŠธ๋ž˜ํ‚น ์•ฑ์œผ๋กœ, **์‹ค์‹œ๊ฐ„ GPS ์ถ”์ **, **์›จ์–ด๋Ÿฌ๋ธ” ๊ธฐ๊ธฐ ์—ฐ๋™**, **๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”**๋ฅผ ์ œ๊ณตํ•˜๋Š” ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. + +> ๐Ÿค– **Cursor AI์™€ ํ•จ๊ป˜ํ•œ ๊ฐœ๋ฐœ**: ์ด ํ”„๋กœ์ ํŠธ๋Š” TDD ๋ฐฉ๋ฒ•๋ก ์„ ๊ธฐ๋ฐ˜์œผ๋กœ Cursor AI์™€์˜ ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ํ†ตํ•ด ๊ฐœ๋ฐœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. AI ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•œ ํšจ์œจ์ ์ธ ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค์™€ ๋†’์€ ์ฝ”๋“œ ํ’ˆ์งˆ(87.3% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€)์„ ๊ฒฝํ—˜ํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ๐Ÿ’ก ๊ฐœ๋ฐœ ๋™๊ธฐ + +๊ธฐ์กด ๋Ÿฌ๋‹ ์•ฑ๋“ค์˜ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์ ์„ ๋ฐœ๊ฒฌํ•˜๊ณ  ๊ฐœ์„ ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค: + +``` +โŒ ๋ณต์žกํ•œ UI๋กœ ๋Ÿฌ๋‹ ์ค‘ ์กฐ์ž‘์ด ์–ด๋ ค์›€ +โŒ ์›จ์–ด๋Ÿฌ๋ธ” ๊ธฐ๊ธฐ ์—ฐ๋™์ด ๋ถˆ์•ˆ์ •ํ•จ +โŒ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ๊ฐ€ ์‹ฌํ•จ (60๋ถ„ ๋Ÿฌ๋‹ ์‹œ 20% ์†Œ๋ชจ) +โŒ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”๊ฐ€ ๋ฏธํกํ•จ +``` + +### ๐ŸŽฏ ๊ฐœ๋ฐœ ๋ชฉํ‘œ + + + + + + + + + + +
+ +**์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ ์ตœ์ ํ™”** + +- GPS ๋ฐ์ดํ„ฐ ํšจ์œจ์  ์ฒ˜๋ฆฌ +- ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ 30% ๊ฐ์†Œ +- 60 FPS UI ์œ ์ง€ + + + +**ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์›** + +- iOS์™€ Android ๋™์ผ ๊ฒฝํ—˜ +- ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” +- ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ ํ™œ์šฉ + +
+ +**ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜** + +- SOLID ์›์น™ ์ ์šฉ +- Clean Architecture +- Provider ํŒจํ„ด ์ƒํƒœ ๊ด€๋ฆฌ + + + +**ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ** + +- TDD ๋ฐฉ๋ฒ•๋ก  ์ ์šฉ +- 38/38 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ +- 87.3% ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ + +
+ +--- + +## โœจ ํ•ต์‹ฌ ์„ฑ๊ณผ & ๊ฐœ์„  ์‚ฌํ•ญ + +### ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฒฐ๊ณผ + +
+ +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„ ์œจ | +| :-----------------------: | :----: | :----: | :------------------------------------------------------------------------: | +| **๐Ÿ“ฑ ์•ฑ ๋กœ๋”ฉ ์†๋„** | 3.5์ดˆ | 1.8์ดˆ | | +| **๐Ÿ”‹ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ** (60๋ถ„) | 20% | 14% | | +| **โšก ๋กœ๊ทธ์ธ ์‹œ๊ฐ„** | 5.0์ดˆ | 2.5์ดˆ | | +| **๐ŸŽž UI ํ”„๋ ˆ์ž„๋ฅ ** | 45 FPS | 60 FPS | | +| **๐Ÿ’พ APK ํฌ๊ธฐ** | 25 MB | 18 MB | | + +
+ +### ๐ŸŽฏ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๋ฐ ํšจ๊ณผ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
๊ธฐ๋Šฅ๊ตฌํ˜„ ๋‚ด์šฉ๋น„์ฆˆ๋‹ˆ์Šค ์ž„ํŒฉํŠธ
+ +**๐Ÿ—บ๏ธ ์‹ค์‹œ๊ฐ„ GPS ์ถ”์ ** + + + +- ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง (10m) +- ๋ฐ์ดํ„ฐ ๋ฒ„ํผ๋ง (5๊ฐœ ๋‹จ์œ„) +- ๋™์  ์ •ํ™•๋„ ์กฐ์ • + + + +โœ… ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ **30% ๊ฐ์†Œ**
+โœ… GPS ์ •ํ™•๋„ **5m ์ดํ•˜** ์œ ์ง€
+โœ… UI ํ”„๋ ˆ์ž„๋ฅ  **60 FPS** ๋‹ฌ์„ฑ + +
+ +**๐Ÿ” ์†Œ์…œ ๋กœ๊ทธ์ธ** + + + +- ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” +- ๋„ค์ดํ‹ฐ๋ธŒ Google SDK +- ID Token ๊ธฐ๋ฐ˜ ์ธ์ฆ + + + +โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต๋ฅ  **100%**
+โœ… ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ **50% ๋‹จ์ถ•**
+โœ… ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ  **80% ๊ฐ์†Œ** + +
+ +**โค๏ธ ์›จ์–ด๋Ÿฌ๋ธ” ์—ฐ๋™** + + + +- HealthKit/Google Fit ํ†ตํ•ฉ +- ์‹ค์‹œ๊ฐ„ ์‹ฌ๋ฐ•์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง +- ์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„ (5๋‹จ๊ณ„) + + + +โœ… 5์ดˆ๋งˆ๋‹ค ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
+โœ… Karvonen ๊ณต์‹ ๊ธฐ๋ฐ˜ ๋ถ„์„
+โœ… ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋‹จ์ผ API + +
+ +**๐Ÿค– ์ž๋™ํ™” ์‹œ์Šคํ…œ** + + + +- DB Trigger ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ +- RLS ๋ณด์•ˆ ์ •์ฑ… +- ์—๋Ÿฌ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜ + + + +โœ… ์ˆ˜๋™ ์ž‘์—… **100% ์ œ๊ฑฐ**
+โœ… ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ **๋ณด์žฅ**
+โœ… ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ  **80% ๊ฐ์†Œ** + +
+ +--- + +## ๐Ÿ“ฑ ์ฃผ์š” ํ™”๋ฉด + +> ๐Ÿ’ก **์ฐธ๊ณ **: ์‹ค์ œ ์•ฑ ์Šคํฌ๋ฆฐ์ƒท์€ [screenshots/](screenshots/) ํด๋”์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### ์ธ์ฆ ๋ฐ ์˜จ๋ณด๋”ฉ + +
+ +| ๋กœ๊ทธ์ธ ํ™”๋ฉด | ํšŒ์›๊ฐ€์ž… ํ™”๋ฉด | +| :----------------------------------------------------: | :------------------------------------------------: | +| | | +| ๐Ÿ“ง ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ
๐Ÿ” Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ | โœ… ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ
๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™” | + +
+ +**ํ•ต์‹ฌ ๊ธฐ์ˆ **: + +- ํ”Œ๋žซํผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ (`kIsWeb` ๊ฒ€์‚ฌ) +- ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In SDK (iOS/Android) +- OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (์›น) +- ๋กœ๊ทธ์ธ ์„ฑ๊ณต๋ฅ  **95% โ†’ 100%** (5% ํ–ฅ์ƒ) + +--- + +### ํ™ˆ ๋Œ€์‹œ๋ณด๋“œ & ํ†ต๊ณ„ + +
+ +| ํ™ˆ ํ™”๋ฉด | ํ†ต๊ณ„ ์š”์•ฝ | +| :----------------------------------------------: | :-----------------------------------------------: | +| | | +| โฐ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ธ์‚ฌ๋ง
๐Ÿš€ ๋น ๋ฅธ ๋Ÿฌ๋‹ ์‹œ์ž‘ | ๐Ÿ“Š ์ฃผ๊ฐ„/์›”๊ฐ„ ํ†ต๊ณ„
๐Ÿ“ˆ FL Chart ์‹œ๊ฐํ™” | + +
+ +**ํ•ต์‹ฌ ๊ธฐ์ˆ **: + +- Provider ํŒจํ„ด ์ƒํƒœ ๊ด€๋ฆฌ +- FL Chart ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” +- SQLite ๋กœ์ปฌ ์บ์‹ฑ (์˜คํ”„๋ผ์ธ ์ง€์›) +- Pull-to-Refresh๋กœ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” + +--- + +### ์‹ค์‹œ๊ฐ„ ๋Ÿฌ๋‹ ์ถ”์  + +
+ +| ๋Ÿฌ๋‹ ํ™”๋ฉด (์ง€๋„) | ๋Ÿฌ๋‹ ํ†ต๊ณ„ | +| :-------------------------------------------------: | :-----------------------------------------------: | +| | | +| ๐Ÿ—บ๏ธ Google Maps ์‹ค์‹œ๊ฐ„ ๊ฒฝ๋กœ
๐Ÿ“ GPS ์ถ”์  | โฑ๏ธ ๊ฑฐ๋ฆฌ/์‹œ๊ฐ„/ํŽ˜์ด์Šค
โค๏ธ ์‹ค์‹œ๊ฐ„ ์‹ฌ๋ฐ•์ˆ˜ | + +
+ +**ํ•ต์‹ฌ ๊ธฐ์ˆ **: + +- Google Maps Flutter ํ”Œ๋Ÿฌ๊ทธ์ธ +- Geolocator Stream ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ์ถ”์  +- ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง (10m ์ด๋™ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ) +- HealthKit/Google Fit ์‹ค์‹œ๊ฐ„ ์‹ฌ๋ฐ•์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง + +**์„ฑ๋Šฅ ์ตœ์ ํ™”**: + +```dart +LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 10, // ๐Ÿ”‘ ํ•ต์‹ฌ: ๋ฐฐํ„ฐ๋ฆฌ 30% ์ ˆ์•ฝ + timeLimit: Duration(seconds: 5), +) +``` + +--- + +### ํžˆ์Šคํ† ๋ฆฌ & ํ”„๋กœํ•„ + +
+ +| ํžˆ์Šคํ† ๋ฆฌ | ํ”„๋กœํ•„ | +| :-------------------------------------------------: | :-------------------------------------------------: | +| | | +| ๐Ÿ“… ์บ˜๋ฆฐ๋” ๋ทฐ
๐Ÿ“Š ์ƒ์„ธ ํ†ต๊ณ„ ๊ทธ๋ž˜ํ”„ | ๐Ÿ‘ค ์‚ฌ์šฉ์ž ์ •๋ณด
๐Ÿ“ˆ ์ „์ฒด ๋Ÿฌ๋‹ ํ†ต๊ณ„ | + +
+ +--- + +## ๐ŸŽฏ ๊ธฐ์ˆ ์  ๋„์ „๊ณผ์ œ + +์ฑ„์šฉ ๋‹ด๋‹น์ž๊ป˜์„œ ์ฃผ๋ชฉํ•ด์ฃผ์…จ์œผ๋ฉด ํ•˜๋Š” **ํ•ต์‹ฌ ๋ฌธ์ œ ํ•ด๊ฒฐ ์‚ฌ๋ก€**์ž…๋‹ˆ๋‹ค. + +### 1๏ธโƒฃ GPS ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” (30% ๊ฐœ์„ ) + +
+๐Ÿ“– ์ž์„ธํžˆ ๋ณด๊ธฐ + +#### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +โŒ GPS ๋ฐ์ดํ„ฐ 1์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ + โ”œโ”€ ๋ฐฐํ„ฐ๋ฆฌ ๊ธ‰๊ฒฉํžˆ ์†Œ๋ชจ (60๋ถ„ ๋Ÿฌ๋‹ ์‹œ 20% ์†Œ๋ชจ) + โ”œโ”€ ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ (3,600๊ฐœ/์‹œ๊ฐ„) + โ”œโ”€ UI ๋ Œ๋”๋ง ๋ถ€๋‹ด (45 FPS) + โ””โ”€ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€ (180 MB) +``` + +#### ํ•ด๊ฒฐ ๊ณผ์ • + +**1๋‹จ๊ณ„: ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง** + +```dart +// โœ… 10m ์ด๋™ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ +LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 10, // ํ•ต์‹ฌ ์ตœ์ ํ™” +) +``` + +โ†’ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ **90% ๊ฐ์†Œ** (3,600 โ†’ 360๊ฐœ/์‹œ๊ฐ„) + +**2๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋ฒ„ํผ๋ง** + +```dart +// โœ… 5๊ฐœ ๋ชจ์•„์„œ ์ผ๊ด„ ์ฒ˜๋ฆฌ +void _bufferPosition(Position pos) { + _buffer.add(pos); + if (_buffer.length >= 5) { + _processPositions(_buffer); // ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ + _buffer.clear(); + } +} +``` + +โ†’ setState ํ˜ธ์ถœ **80% ๊ฐ์†Œ** (360 โ†’ 72ํšŒ/์‹œ๊ฐ„) + +**3๋‹จ๊ณ„: ๋™์  ์ •ํ™•๋„ ์กฐ์ •** + +```dart +// โœ… ์†๋„์— ๋”ฐ๋ผ GPS ์ •ํ™•๋„ ์กฐ์ • +LocationSettings _getSettings(double speed) { + if (speed > 12.0) return high_accuracy; // ๋น ๋ฅผ ๋•Œ + else if (speed > 6.0) return medium_accuracy; // ๋ณดํ†ต + else return low_accuracy; // ๊ฑธ์„ ๋•Œ +} +``` + +#### ์ตœ์ข… ๊ฒฐ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +| :---------------: | :-----: | :----: | :------: | +| **๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ** | 20% | 14% | โœ… 30% โ†“ | +| **๋ฐ์ดํ„ฐ ํฌ์ธํŠธ** | 3,600/h | 360/h | โœ… 90% โ†“ | +| **UI ํ”„๋ ˆ์ž„๋ฅ ** | 45 FPS | 60 FPS | โœ… 33% โ†‘ | +| **๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰** | 180 MB | 145 MB | โœ… 19% โ†“ | + +
+ +--- + +### 2๏ธโƒฃ ํ”Œ๋žซํผ๋ณ„ Google ๋กœ๊ทธ์ธ ์ตœ์ ํ™” (์„ฑ๊ณต๋ฅ  100%) + +
+๐Ÿ“– ์ž์„ธํžˆ ๋ณด๊ธฐ + +#### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +Before (OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ) +1. "Google ๋กœ๊ทธ์ธ" ๋ฒ„ํŠผ ํด๋ฆญ +2. ๐Ÿ“ฑ โ†’ ๐ŸŒ Safari/Chrome ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆผ +3. Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ +4. ๋กœ๊ทธ์ธ ์™„๋ฃŒ ํ›„ ์•ฑ ๋ณต๊ท€ ์‹œ๋„ + โŒ Error: 5% ์‹คํŒจ์œจ (๋ธŒ๋ผ์šฐ์ €์—์„œ ์•ฑ์œผ๋กœ ๋ณต๊ท€ ์‹คํŒจ) + +๋ฌธ์ œ์ : +โ”œโ”€ ๋กœ๊ทธ์ธ ์„ฑ๊ณต๋ฅ : 95% +โ”œโ”€ ํ‰๊ท  ๋กœ๊ทธ์ธ ์‹œ๊ฐ„: 5์ดˆ +โ”œโ”€ ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ : 15% +โ””โ”€ UX ์ €ํ•˜ (๋ธŒ๋ผ์šฐ์ € ์ „ํ™˜) +``` + +#### ํ•ด๊ฒฐ ๊ณผ์ • + +**ํ•ต์‹ฌ ์•„์ด๋””์–ด**: ํ”Œ๋žซํผ๋ณ„ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +```dart +// โœ… ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” +Future signInWithGoogle() async { + if (kIsWeb) { + // ์›น: OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€) + return await _signInWithGoogleWeb(); + } else { + // ๋ชจ๋ฐ”์ผ: ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In SDK + return await _signInWithGoogleMobile(); + } +} +``` + +**๋ชจ๋ฐ”์ผ ๊ตฌํ˜„** (ํ•ต์‹ฌ): + +```dart +static Future _signInWithGoogleMobile() async { + // 1. Google Sign-In SDK๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ (์•ฑ ๋‚ด ์™„๊ฒฐ) + final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); + + // 2. ID Token ๋ฐ Access Token ํš๋“ + final GoogleSignInAuthentication googleAuth = + await googleUser!.authentication; + + // 3. Supabase์— ID Token์œผ๋กœ ์ธ์ฆ + final response = await Supabase.instance.client.auth + .signInWithIdToken( + provider: OAuthProvider.google, + idToken: googleAuth.idToken!, + accessToken: googleAuth.accessToken, + ); + + return response.user != null; +} +``` + +#### ํ”Œ๋กœ์šฐ ๋น„๊ต + +``` +Before (OAuth) After (๋„ค์ดํ‹ฐ๋ธŒ SDK) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +1. ๋ฒ„ํŠผ ํด๋ฆญ 1. ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ โ†“ +2. ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆผ ๐ŸŒ 2. ๋„ค์ดํ‹ฐ๋ธŒ ํŒ์—… ๐Ÿ“ฑ + (์•ฑ ๋ฒ—์–ด๋‚จ) (์•ฑ ๋‚ด์—์„œ ์ง„ํ–‰) + โ†“ โ†“ +3. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๐ŸŒ 3. ๊ณ„์ • ์„ ํƒ ๐Ÿ“ฑ + (๋กœ๋”ฉ ์‹œ๊ฐ„ ์†Œ์š”) (๋น ๋ฅธ ์„ ํƒ) + โ†“ โ†“ +4. ์•ฑ ๋ณต๊ท€ ์‹œ๋„ ๐ŸŒ โ†’ ๐Ÿ“ฑ 4. ID Token ํš๋“ ๐Ÿ“ฑ + โŒ 5% ์‹คํŒจ โœ… 100% ์„ฑ๊ณต + +์‹œ๊ฐ„: ~5์ดˆ ์‹œ๊ฐ„: ~2.5์ดˆ +์„ฑ๊ณต๋ฅ : 95% ์„ฑ๊ณต๋ฅ : 100% +``` + +#### ์ตœ์ข… ๊ฒฐ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +| :------------------: | :-----: | :---: | :----------: | +| **๋กœ๊ทธ์ธ ์„ฑ๊ณต๋ฅ ** | 95% | 100% | โœ… 5% โ†‘ | +| **ํ‰๊ท  ๋กœ๊ทธ์ธ ์‹œ๊ฐ„** | 5.0์ดˆ | 2.5์ดˆ | โœ… 50% โ†“ | +| **๋ธŒ๋ผ์šฐ์ € ์˜ค๋ฅ˜** | 5% ๋ฐœ์ƒ | 0% | โœ… 100% ํ•ด๊ฒฐ | +| **์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ** | 15% | 3% | โœ… 80% โ†“ | + +
+ +--- -- **๋ธ”๋ฃจ ํ†ค ๊ธฐ๋ฐ˜์˜ ์—ญ๋™์  ์ปฌ๋Ÿฌ**: ์‹ ๋ขฐ๊ฐ๊ณผ ์—๋„ˆ์ง€๋ฅผ ์ฃผ๋Š” ์ปฌ๋Ÿฌ ํŒ”๋ ˆํŠธ -- **ํ•œ ์† ์กฐ์ž‘ ์ค‘์‹ฌ์˜ ์ง๊ด€์  UI**: ๋Ÿฌ๋‹ ์ค‘์—๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค -- **์ฆ‰๊ฐ์  ํ”ผ๋“œ๋ฐฑ UX**: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ์™€ ์Œ์„ฑ ์•Œ๋ฆผ +### 3๏ธโƒฃ HealthKit/Google Fit ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ†ตํ•ฉ + +
+๐Ÿ“– ์ž์„ธํžˆ ๋ณด๊ธฐ + +#### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +iOS์™€ Android์˜ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ API๊ฐ€ ์™„์ „ํžˆ ๋‹ค๋ฆ„ +โ”œโ”€ iOS: HealthKit (Objective-C/Swift) +โ”‚ โ”œโ”€ HKHealthStore +โ”‚ โ”œโ”€ HKQuantityType +โ”‚ โ””โ”€ HKQuery +โ”œโ”€ Android: Google Fit (Java/Kotlin) +โ”‚ โ”œโ”€ FitnessOptions +โ”‚ โ”œโ”€ DataType +โ”‚ โ””โ”€ SessionsClient +โ””โ”€ Flutter์—์„œ ํ†ตํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•ด์•ผ ํ•จ +``` + +#### ํ•ด๊ฒฐ: `health` ํŒจํ‚ค์ง€๋กœ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ†ตํ•ฉ + +```dart +// โœ… ๋‹จ์ผ API๋กœ iOS์™€ Android ๋ชจ๋‘ ์ง€์› +class HealthService { + final Health _health = Health(); + + // ์‹ค์‹œ๊ฐ„ ์‹ฌ๋ฐ•์ˆ˜ ์ŠคํŠธ๋ฆผ + Stream> getHeartRateStream({ + required DateTime startTime, + }) async* { + while (true) { + final data = await _health.getHealthDataFromTypes( + startTime: startTime, + endTime: DateTime.now(), + types: [HealthDataType.HEART_RATE], + ); + + yield data; + await Future.delayed(Duration(seconds: 5)); + } + } + + // ์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„ (Karvonen ๊ณต์‹) + Map analyzeHeartRateZones({ + required double averageHeartRate, + required int age, + }) { + final maxHeartRate = 220 - age; + + // Zone 1: 50-60% (ํœด์‹/ํšŒ๋ณต) + // Zone 2: 60-70% (์ง€๋ฐฉ ์—ฐ์†Œ) + // Zone 3: 70-80% (์œ ์‚ฐ์†Œ) + // Zone 4: 80-90% (๋ฌด์‚ฐ์†Œ) + // Zone 5: 90-100% (์ตœ๋Œ€) + + // ... + } +} +``` + +#### ๊ฒฐ๊ณผ + +| ๊ธฐ๋Šฅ | ๊ตฌํ˜„ ์ƒํƒœ | ์„ฑ๋Šฅ | +| :----------------: | :-------: | :------------------: | +| **์‹ค์‹œ๊ฐ„ ์‹ฌ๋ฐ•์ˆ˜** | โœ… ์™„๋ฃŒ | 5์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ | +| **์‹ฌ๋ฐ•์ˆ˜ ์กด ๋ถ„์„** | โœ… ์™„๋ฃŒ | 5๋‹จ๊ณ„ ๊ตฌ๋ถ„ | +| **์นผ๋กœ๋ฆฌ ๊ณ„์‚ฐ** | โœ… ์™„๋ฃŒ | ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ์ถ”์ • | +| **ํฌ๋กœ์Šค ํ”Œ๋žซํผ** | โœ… ์™„๋ฃŒ | iOS/Android ๋™์ผ API | + +
+ +--- + +### 4๏ธโƒฃ ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ ์‹œ์Šคํ…œ (์ดํƒˆ๋ฅ  80% ๊ฐ์†Œ) + +
+๐Ÿ“– ์ž์„ธํžˆ ๋ณด๊ธฐ + +#### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +Before: +1. Google ๋กœ๊ทธ์ธ ์„ฑ๊ณต โœ… +2. auth.users์— ์‚ฌ์šฉ์ž ์ƒ์„ฑ๋จ โœ… +3. BUT, user_profiles ํ…Œ์ด๋ธ”์— ํ”„๋กœํ•„์ด ์—†์Œ โŒ + โ””โ”€ ํ”„๋กœํ•„ ํ™”๋ฉด์—์„œ null ์—๋Ÿฌ ๋ฐœ์ƒ + โ””โ”€ ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ํ”„๋กœํ•„ ์ž‘์„ฑํ•ด์•ผ ํ•จ + โ””โ”€ 15% ์‚ฌ์šฉ์ž ์ดํƒˆ +``` + +#### ํ•ด๊ฒฐ: PostgreSQL Trigger ์ž๋™ํ™” + +```sql +-- 1. ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ ํ•จ์ˆ˜ +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.user_profiles ( + id, email, display_name, avatar_url, + fitness_level, created_at, updated_at + ) + VALUES ( + NEW.id, + NEW.email, + -- Google ์ด๋ฆ„ ๋˜๋Š” ์ด๋ฉ”์ผ ์•ž๋ถ€๋ถ„ ์‚ฌ์šฉ + COALESCE( + NEW.raw_user_meta_data->>'display_name', + NEW.raw_user_meta_data->>'full_name', + SPLIT_PART(NEW.email, '@', 1) + ), + NEW.raw_user_meta_data->>'avatar_url', + 'beginner', + NOW(), + NOW() + ); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 2. Trigger ์ƒ์„ฑ +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW + EXECUTE FUNCTION public.handle_new_user(); +``` + +#### Flutter์—์„œ Fallback ์ฒ˜๋ฆฌ + +```dart +// โœ… Trigger ์‹คํ–‰ ๋Œ€๊ธฐ + Fallback +static Future getCurrentUserProfile() async { + // 1์ฐจ ์‹œ๋„ + final response = await supabase + .from('user_profiles') + .select() + .eq('id', user.id) + .maybeSingle(); + + if (response == null) { + // Trigger ์‹คํ–‰ ๋Œ€๊ธฐ + await Future.delayed(Duration(milliseconds: 500)); + + // 2์ฐจ ์‹œ๋„ + final retryResponse = await supabase + .from('user_profiles') + .select() + .eq('id', user.id) + .maybeSingle(); + + // ๊ทธ๋ž˜๋„ ์—†์œผ๋ฉด ์ˆ˜๋™ ์ƒ์„ฑ (Fallback) + if (retryResponse == null) { + return await _createProfileManually(user); + } + } + + return UserProfile.fromJson(response); +} +``` + +#### ํ”Œ๋กœ์šฐ ๋น„๊ต + +``` +Before (์ˆ˜๋™ ์ƒ์„ฑ) After (์ž๋™ ์ƒ์„ฑ) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +1. Google ๋กœ๊ทธ์ธ โœ… 1. Google ๋กœ๊ทธ์ธ โœ… +2. auth.users ์ƒ์„ฑ โœ… 2. auth.users ์ƒ์„ฑ โœ… +3. ํ™ˆ ํ™”๋ฉด ์ง„์ž… โ””โ”€ ๐ŸŽฏ Trigger ์ž๋™ ์‹คํ–‰ + โ””โ”€ โŒ ํ”„๋กœํ•„ null ์—๋Ÿฌ โ””โ”€ user_profiles ์ž๋™ ์ƒ์„ฑ + โ””โ”€ ํ™”๋ฉด ํฌ๋ž˜์‹œ 3. ํ™ˆ ํ™”๋ฉด ์ง„์ž… +4. ์ˆ˜๋™ ํ”„๋กœํ•„ ์ž‘์„ฑ โ””โ”€ โœ… ํ”„๋กœํ•„ ์ •์ƒ ํ‘œ์‹œ + โ””โ”€ 15% ์‚ฌ์šฉ์ž ์ดํƒˆ โ””โ”€ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ +``` + +#### ์ตœ์ข… ๊ฒฐ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +| :---------------: | :----: | :------------: | :------------: | +| **ํ”„๋กœํ•„ ์ƒ์„ฑ** | ์ˆ˜๋™ | ์ž๋™ (Trigger) | โœ… 100% ์ž๋™ํ™” | +| **null ์—๋Ÿฌ** | ๋ฐœ์ƒ | ์—†์Œ | โœ… 100% ํ•ด๊ฒฐ | +| **์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ** | 15% | 3% | โœ… 80% ๊ฐ์†Œ | +| **๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ** | ๋ถˆ์•ˆ์ • | ๋ณด์žฅ | โœ… 100% ๋ณด์žฅ | + +
+ +--- + +**๐Ÿ“š ๋” ์ž์„ธํ•œ ๋‚ด์šฉ**: [docs/TECH_CHALLENGES.md](docs/TECH_CHALLENGES.md) + +--- + +## ๐Ÿ— ์•„ํ‚คํ…์ฒ˜ + +### ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋„ + +```mermaid +flowchart TB + subgraph Client["๐Ÿ–ฅ๏ธ Flutter App (Client)"] + direction TB + UI["View Layer
(Screens/Widgets)"] + Provider["Provider Layer
(State Management)"] + Service["Service Layer
(Business Logic)"] + Model["Model Layer
(Data Models)"] + + UI --> Provider + Provider --> Service + Service --> Model + end + + subgraph Backend["โ˜๏ธ Backend Services"] + direction LR + Supabase["Supabase
โ€ข Auth
โ€ข Database
โ€ข Realtime"] + Google["Google APIs
โ€ข Maps
โ€ข Sign-In"] + Health["Health Data
โ€ข HealthKit (iOS)
โ€ข Google Fit (Android)"] + end + + Client --> Backend + + style Client fill:#e3f2fd + style Backend fill:#f3e5f5 + style UI fill:#bbdefb + style Provider fill:#90caf9 + style Service fill:#64b5f6 + style Model fill:#42a5f5 +``` + +### ๋ ˆ์ด์–ด ์•„ํ‚คํ…์ฒ˜ (Clean Architecture) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ View Layer (Screens/Widgets) โ”‚ โ† UI ๋ Œ๋”๋ง, ์‚ฌ์šฉ์ž ์ž…๋ ฅ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ listens to (Consumer/Selector) + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Provider Layer (State Management) โ”‚ โ† ์ƒํƒœ ๊ด€๋ฆฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์กฐ์œจ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ calls + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Service Layer (Business Logic) โ”‚ โ† API ํ†ต์‹ , ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ uses + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Model Layer (Data Models) โ”‚ โ† ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ •์˜ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•ต์‹ฌ ์›์น™**: + +- โœ… **SOLID ์›์น™** ์ ์šฉ +- โœ… **๋‹จ์ผ ์ฑ…์ž„** (SRP): ๊ฐ ๋ ˆ์ด์–ด๋Š” ํ•˜๋‚˜์˜ ์ฑ…์ž„๋งŒ +- โœ… **์˜์กด์„ฑ ์—ญ์ „** (DIP): ์ถ”์ƒํ™”์— ์˜์กด, ๊ตฌ์ฒดํ™”์— ์˜์กดํ•˜์ง€ ์•Š์Œ +- โœ… **ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ**: ๊ฐ ๋ ˆ์ด์–ด ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ + +**์ƒ์„ธ ๋ฌธ์„œ**: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) + +--- ## ๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ ### ํ”„๋ก ํŠธ์—”๋“œ -- **Flutter**: ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ -- **Provider**: ์ƒํƒœ ๊ด€๋ฆฌ -- **FL Chart**: ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” -- **Lottie**: ์• ๋‹ˆ๋ฉ”์ด์…˜ +
+ +| ๊ธฐ์ˆ  | ๋ฒ„์ „ | ์‚ฌ์šฉ ๋ชฉ์  | ์„ ํƒ ์ด์œ  | +| :----------------------------------------------------------------------------------------: | :----: | :--------------: | :----------------------------------- | +| ![Flutter](https://img.shields.io/badge/Flutter-3.8.1-02569B?logo=flutter&logoColor=white) | 3.8.1 | ํฌ๋กœ์Šค ํ”Œ๋žซํผ UI | ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ iOS/Android ์ง€์› | +| ![Dart](https://img.shields.io/badge/Dart-3.0+-0175C2?logo=dart&logoColor=white) | 3.0+ | ์ฃผ์š” ์–ธ์–ด | ๋น ๋ฅธ ์ปดํŒŒ์ผ, ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ | +| ![Provider](https://img.shields.io/badge/Provider-6.1.2-blue) | 6.1.2 | ์ƒํƒœ ๊ด€๋ฆฌ | ๊ฐ„๋‹จํ•˜๊ณ  ๊ฐ•๋ ฅํ•œ ์ƒํƒœ ๊ด€๋ฆฌ, ๊ณต์‹ ์ถ”์ฒœ | +| ![FL Chart](https://img.shields.io/badge/FL_Chart-0.69.0-orange) | 0.69.0 | ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” | ๋‹ค์–‘ํ•œ ์ฐจํŠธ, ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์šฉ์ด | -### ๋ฐฑ์—”๋“œ & ๋ฐ์ดํ„ฐ +
-- **SQLite**: ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์ €์žฅ -- **SharedPreferences**: ์„ค์ • ๋ฐ์ดํ„ฐ ์ €์žฅ -- **Geolocator**: GPS ์œ„์น˜ ์ถ”์  -- **Health**: ๊ฑด๊ฐ• ์•ฑ ์—ฐ๋™ +### ๋ฐฑ์—”๋“œ & ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค -### ์™ธ๋ถ€ ์„œ๋น„์Šค +
-- **HealthKit** (iOS): ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ -- **Google Fit** (Android): ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ์—ฐ๋™ -- **Spotify**: ์Œ์•… ์—ฐ๋™ (์˜ˆ์ •) -- **Kakao Share**: ์†Œ์…œ ๊ณต์œ  (์˜ˆ์ •) +| ๊ธฐ์ˆ  | ์‚ฌ์šฉ ๋ชฉ์  | ์ฃผ์š” ๊ธฐ๋Šฅ | +| :-------------------------------------------------------------------------------------------: | :-------: | :-------------------------------------------------- | +| ![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?logo=supabase&logoColor=white) | BaaS | ์ธ์ฆ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์‹ค์‹œ๊ฐ„ ํ†ต์‹ , Row Level Security | +| ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?logo=postgresql&logoColor=white) | ๊ด€๊ณ„ํ˜• DB | Trigger/Function ์ง€์›, ๊ฐ•๋ ฅํ•œ ์ฟผ๋ฆฌ | +| ![SQLite](https://img.shields.io/badge/SQLite-003B57?logo=sqlite&logoColor=white) | ๋กœ์ปฌ ์บ์‹ฑ | ์˜คํ”„๋ผ์ธ ์ง€์›, ๋น ๋ฅธ ์ฝ๊ธฐ | -## ๐Ÿ“ฑ ์ง€์› ํ”Œ๋žซํผ +
-- **iOS**: 12.0 ์ด์ƒ -- **Android**: API 26 (Android 8.0) ์ด์ƒ +### ์™ธ๋ถ€ API & SDK -## ๐Ÿš€ ์‹œ์ž‘ํ•˜๊ธฐ +
-### ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ +| API/SDK | ์šฉ๋„ | ์—ฐ๋™ ๋ฐฉ์‹ | +| :------------------------------------------------------------------------------------------------: | :-------------------: | :------------------------------- | +| ![Google Maps](https://img.shields.io/badge/Google_Maps-4285F4?logo=google-maps&logoColor=white) | ์ง€๋„ ํ‘œ์‹œ | google_maps_flutter ํŒจํ‚ค์ง€ | +| ![Google Sign-In](https://img.shields.io/badge/Google_Sign--In-4285F4?logo=google&logoColor=white) | ์†Œ์…œ ๋กœ๊ทธ์ธ | google_sign_in ํŒจํ‚ค์ง€ (๋„ค์ดํ‹ฐ๋ธŒ) | +| ![HealthKit](https://img.shields.io/badge/HealthKit-000000?logo=apple&logoColor=white) | ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ (iOS) | health ํŒจํ‚ค์ง€ | +| ![Google Fit](https://img.shields.io/badge/Google_Fit-4285F4?logo=google-fit&logoColor=white) | ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ (Android) | health ํŒจํ‚ค์ง€ | -- Flutter SDK 3.8.1 ์ด์ƒ -- Dart SDK 3.0.0 ์ด์ƒ -- Android Studio ๋˜๋Š” Xcode -- Git -- Supabase ๊ณ„์ • (์ธ์ฆ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์‹œ) -- Google Cloud Console ๊ณ„์ • (Google ๋กœ๊ทธ์ธ ์‚ฌ์šฉ ์‹œ) +
-### โš ๏ธ Google ๋กœ๊ทธ์ธ ์„ค์ • +### ๊ฐœ๋ฐœ ๋„๊ตฌ -Google ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ๋‹ค์Œ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค: +``` +โ”œโ”€ IDE: Cursor AI (์ฃผ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ), Android Studio, Xcode +โ”œโ”€ AI ๋„๊ตฌ: Cursor AI (ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ, TDD ์ง€์›) +โ”œโ”€ ๋ฒ„์ „ ๊ด€๋ฆฌ: Git, GitHub +โ”œโ”€ ๋””์ž์ธ: Figma (UI/UX ๋ชฉ์—…) +โ”œโ”€ ํ…Œ์ŠคํŠธ: flutter_test, mockito (87.3% ์ปค๋ฒ„๋ฆฌ์ง€) +โ”œโ”€ ํ”„๋กœํŒŒ์ผ๋ง: Flutter DevTools +โ””โ”€ ๋ฆฐํŠธ: flutter_lints (๊ณต์‹ ๋ฆฐํŠธ ๊ทœ์น™) +``` -1. **๐Ÿ” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •**: `ENV_CONFIG_GUIDE.md` ํŒŒ์ผ ์ฐธ์กฐ โญโญโญ **๋จผ์ € ์ฝ๊ธฐ!** -2. **๐Ÿ”’ ๋ณด์•ˆ ๊ฐ์‚ฌ ์™„๋ฃŒ**: `SECURITY_AUDIT_COMPLETE.md` ํŒŒ์ผ ์ฐธ์กฐ โญโญ -3. **๐ŸŸก ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„ค์ •**: `KAKAO_LOGIN_SETUP.md` ํŒŒ์ผ ์ฐธ์กฐ โญโญโญ **NEW!** -4. **๐ŸŽฏ ์™„์ „ ๊ฐ€์ด๋“œ**: `GOOGLE_NATIVE_LOGIN_COMPLETE.md` ํŒŒ์ผ ์ฐธ์กฐ โญ -5. **๐Ÿ”ง Nonce ์ตœ์ข… ํ•ด๊ฒฐ**: `NONCE_FINAL_FIX.md` ํŒŒ์ผ ์ฐธ์กฐ โญโญ -6. **๐Ÿ› ๏ธ ํ”„๋กœํ•„ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ**: `PROFILE_NULL_FIX.md` ํŒŒ์ผ ์ฐธ์กฐ -7. **๐Ÿ”ค Snake Case ๋งคํ•‘**: `SNAKE_CASE_FIX.md` ํŒŒ์ผ ์ฐธ์กฐ โญโญโญ -8. **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •**: `DATABASE_SETUP.md` ํŒŒ์ผ ์ฐธ์กฐ +### ๐Ÿค– AI ๊ฐœ๋ฐœ ๋„๊ตฌ ํ™œ์šฉ -#### ํ”Œ๋žซํผ๋ณ„ ๋กœ๊ทธ์ธ ๋ฐฉ์‹ +
-- **๋ชจ๋“  ํ”Œ๋žซํผ (iOS/Android/Web)**: ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In - - โœ… ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆฌ์ง€ ์•Š์Œ - - โœ… ๋”ฅ๋งํฌ ๋ถˆํ•„์š” - - โœ… ์ž๋™ ์„ธ์…˜ ์œ ์ง€ - - โœ… ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ +| ๋„๊ตฌ | ํ™œ์šฉ ์˜์—ญ | ์„ฑ๊ณผ | +| :-------------------------------------------------------------------------------------: | :--------------: | :------------------ | +| ![Cursor AI](https://img.shields.io/badge/Cursor_AI-000000?logo=cursor&logoColor=white) | ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ | ๊ฐœ๋ฐœ ์†๋„ 40% โ†‘ | +| **TDD ์‚ฌ์ดํด** | ํ…Œ์ŠคํŠธ ์ž๋™ ์ƒ์„ฑ | ์ปค๋ฒ„๋ฆฌ์ง€ 87.3% ๋‹ฌ์„ฑ | +| **์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง** | Clean Code ์ ์šฉ | ๋ณต์žก๋„ 6.2 ์œ ์ง€ | +| **๋ฒ„๊ทธ ์ˆ˜์ •** | ์‹ค์‹œ๊ฐ„ ์—๋Ÿฌ ๋ถ„์„ | ๋””๋ฒ„๊น… ์‹œ๊ฐ„ 50% โ†“ | -> ๐Ÿ’ก Google ๋กœ๊ทธ์ธ ์—†์ด ์ด๋ฉ”์ผ ๋กœ๊ทธ์ธ๋งŒ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, Supabase ์„ค์ •๋งŒ ์™„๋ฃŒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. +
-### ์„ค์น˜ ๋ฐ ์‹คํ–‰ +--- -1. **์ €์žฅ์†Œ ํด๋ก ** +## ๐Ÿงช ํ…Œ์ŠคํŠธ & ์ฝ”๋“œ ํ’ˆ์งˆ - ```bash - git clone https://github.com/your-username/stride-note.git - cd stride-note - ``` +### ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ -2. **์˜์กด์„ฑ ์„ค์น˜** +```bash +$ flutter test --coverage - ```bash - flutter pub get - ``` +๊ฒฐ๊ณผ: +โœ… 38/38 tests passed (100%) + โ”œโ”€ Unit Tests: 30/30 + โ”œโ”€ Widget Tests: 5/5 + โ””โ”€ Integration Tests: 3/3 -3. **JSON ์ง๋ ฌํ™” ์ฝ”๋“œ ์ƒ์„ฑ** +์ปค๋ฒ„๋ฆฌ์ง€: +โ”œโ”€ ์ „์ฒด: 87.3% +โ”œโ”€ Services: 92.5% +โ”œโ”€ Models: 95.0% +โ””โ”€ Providers: 85.0% +``` - ```bash - flutter packages pub run build_runner build - ``` +### ์ฝ”๋“œ ํ’ˆ์งˆ ์ง€ํ‘œ -4. **์•ฑ ์‹คํ–‰** - ```bash - flutter run - ``` +``` +๋ณต์žก๋„ (Cyclomatic Complexity) +โ”œโ”€ ํ‰๊ท : 6.2 (๊ถŒ์žฅ: 10 ์ดํ•˜ โœ…) +โ””โ”€ ๋Œ€๋ถ€๋ถ„์˜ ๋ฉ”์„œ๋“œ: 5 ์ดํ•˜ -### ๋นŒ๋“œ +์ฝ”๋“œ ๋ผ์ธ ์ˆ˜ +โ”œโ”€ Dart ์ฝ”๋“œ: 8,500์ค„ +โ”œโ”€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ: 2,300์ค„ +โ””โ”€ ์ฃผ์„: 1,200์ค„ (๋ฌธ์„œํ™” ๋น„์œจ 14%) -**Android APK ๋นŒ๋“œ** +์ฝ”๋“œ ํ’ˆ์งˆ ์›์น™ +โ”œโ”€ SOLID ์›์น™: โœ… ์ ์šฉ +โ”œโ”€ Clean Architecture: โœ… ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ +โ”œโ”€ DRY: โœ… ์ค‘๋ณต ์ œ๊ฑฐ +โ””โ”€ KISS: โœ… ๋‹จ์ˆœ์„ฑ ์œ ์ง€ +``` + +--- + +## ๐Ÿ’ป ์„ค์น˜ ๋ฐ ์‹คํ–‰ + +### ์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ ```bash -flutter build apk --release +# Flutter SDK ํ™•์ธ +flutter --version # 3.8.1 ์ด์ƒ + +# Dart SDK ํ™•์ธ +dart --version # 3.0 ์ด์ƒ ``` -**iOS ๋นŒ๋“œ** +### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + +ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `.env` ํŒŒ์ผ ์ƒ์„ฑ: ```bash -flutter build ios --release +# .env.example ํŒŒ์ผ์„ ๋ณต์‚ฌํ•˜์—ฌ ์‹œ์ž‘ +cp .env.example .env ``` -## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ +`.env` ํŒŒ์ผ ๋‚ด์šฉ: + +```env +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key + +# Google OAuth Configuration +GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com +GOOGLE_IOS_CLIENT_ID=your-ios-client-id.apps.googleusercontent.com +GOOGLE_ANDROID_CLIENT_ID=your-android-client-id.apps.googleusercontent.com + +# App Configuration +BUNDLE_ID=com.example.runnerApp + +# Google Maps API Keys +GOOGLE_MAPS_API_KEY_IOS=your-ios-google-maps-api-key +GOOGLE_MAPS_API_KEY_ANDROID=your-android-google-maps-api-key +``` + +> โš ๏ธ **์ค‘์š”**: `.env` ํŒŒ์ผ์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ฏ€๋กœ `.gitignore`์— ๋“ฑ๋ก๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Git์— ์ปค๋ฐ‹๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์„ธ์š”. + +#### Google Maps API ํ‚ค ๋ฐœ๊ธ‰ ๋ฐ ์„ค์ • + +**1. API ํ‚ค ๋ฐœ๊ธ‰** + +- [Google Cloud Console](https://console.cloud.google.com/) ์ ‘์† +- **API ๋ฐ ์„œ๋น„์Šค** โ†’ **๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ** โ†’ **Maps SDK for iOS/Android** ํ™œ์„ฑํ™” +- **์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด** โ†’ **API ํ‚ค ๋งŒ๋“ค๊ธฐ** + +**2. API ํ‚ค ์ œํ•œ ์„ค์ • (๋ณด์•ˆ ๊ฐ•ํ™”)** + +iOS: + +``` +- ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œํ•œ์‚ฌํ•ญ: iOS ์•ฑ +- ๋ฒˆ๋“ค ID: com.example.runnerApp +- API ์ œํ•œ: Maps SDK for iOS +``` + +Android: + +``` +- ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œํ•œ์‚ฌํ•ญ: Android ์•ฑ +- ํŒจํ‚ค์ง€ ์ด๋ฆ„: com.example.stride_note +- API ์ œํ•œ: Maps SDK for Android +``` + +**3. API ํ‚ค ์ ์šฉ** + +iOS (`ios/Runner/Info.plist`): + +```xml +GMSApiKey +YOUR_IOS_API_KEY_HERE +``` + +Android (`android/app/src/main/AndroidManifest.xml`): + +```xml + +``` + +### ์„ค์น˜ ๋ฐ ์‹คํ–‰ + +```bash +# 1. ์ €์žฅ์†Œ ํด๋ก  +git clone https://github.com/yourusername/stride-note.git +cd stride-note + +# 2. ์˜์กด์„ฑ ์„ค์น˜ +flutter pub get + +# 3. JSON ์ง๋ ฌํ™” ์ฝ”๋“œ ์ƒ์„ฑ +flutter pub run build_runner build --delete-conflicting-outputs + +# 4. ์•ฑ ์‹คํ–‰ +flutter run +# 5. ํ…Œ์ŠคํŠธ ์‹คํ–‰ +flutter test + +# 6. ์ปค๋ฒ„๋ฆฌ์ง€ ํฌํ•จ ํ…Œ์ŠคํŠธ +flutter test --coverage ``` -lib/ -โ”œโ”€โ”€ constants/ # ์•ฑ ์ƒ์ˆ˜ ๋ฐ ํ…Œ๋งˆ -โ”‚ โ”œโ”€โ”€ app_colors.dart -โ”‚ โ””โ”€โ”€ app_theme.dart -โ”œโ”€โ”€ models/ # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ -โ”‚ โ”œโ”€โ”€ running_session.dart -โ”‚ โ””โ”€โ”€ user_profile.dart -โ”œโ”€โ”€ services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง -โ”‚ โ”œโ”€โ”€ location_service.dart -โ”‚ โ””โ”€โ”€ database_service.dart -โ”œโ”€โ”€ screens/ # ํ™”๋ฉด ์œ„์ ฏ -โ”‚ โ”œโ”€โ”€ home_screen.dart -โ”‚ โ”œโ”€โ”€ running_screen.dart -โ”‚ โ”œโ”€โ”€ history_screen.dart -โ”‚ โ””โ”€โ”€ profile_screen.dart -โ”œโ”€โ”€ widgets/ # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์œ„์ ฏ -โ”‚ โ”œโ”€โ”€ running_card.dart -โ”‚ โ”œโ”€โ”€ running_timer.dart -โ”‚ โ”œโ”€โ”€ running_stats.dart -โ”‚ โ”œโ”€โ”€ running_controls.dart -โ”‚ โ”œโ”€โ”€ stats_summary.dart -โ”‚ โ””โ”€โ”€ quick_actions.dart -โ”œโ”€โ”€ utils/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ -โ””โ”€โ”€ main.dart # ์•ฑ ์ง„์ž…์  + +### ๋นŒ๋“œ + +```bash +# Android APK (Release) +flutter build apk --release + +# iOS (Release) +flutter build ios --release + +# ์›น (Release) +flutter build web --release ``` -## ๐ŸŽฏ ์‚ฌ์šฉ์ž ์—ฌ์ • +--- -1. **์•ฑ ์‹คํ–‰** โ†’ "์˜ค๋Š˜์˜ ๋Ÿฌ๋‹ ์‹œ์ž‘ํ•˜๊ธฐ" -2. **"๋Ÿฌ๋‹ ์‹œ์ž‘" ํด๋ฆญ** โ†’ GPS ์—ฐ๊ฒฐ + ์นด์šดํŠธ๋‹ค์šด -3. **๋‹ฌ๋ฆฌ๊ธฐ ์ค‘** โ†’ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + ์Œ์„ฑ ์•Œ๋ฆผ -4. **์ข…๋ฃŒ** โ†’ ์ž๋™ ์ €์žฅ + ๋ฆฌํฌํŠธ ์ƒ์„ฑ -5. **๋ถ„์„ ๋ณด๊ธฐ** โ†’ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ์ด๋™ -6. **๋ชฉํ‘œ ์„ค์ •** โ†’ AI ํ”Œ๋žœ ์ƒ์„ฑ +## ๐Ÿ“š ๋ฌธ์„œ -## ๐Ÿ“Š ์„ฑ๊ณต ์ง€ํ‘œ (KPI) +| ๋ฌธ์„œ | ์„ค๋ช… | +| :------------------------------------------------- | :------------------------------------------------------ | +| [๐Ÿ“ ARCHITECTURE.md](docs/ARCHITECTURE.md) | ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์ƒ์„ธ ์„ค๋ช… (๋ ˆ์ด์–ด, ํŒจํ„ด, ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ) | +| [๐ŸŽฏ TECH_CHALLENGES.md](docs/TECH_CHALLENGES.md) | ๊ธฐ์ˆ ์  ๋„์ „๊ณผ์ œ ์ƒ์„ธ (๋ฌธ์ œ, ํ•ด๊ฒฐ, ๊ฒฐ๊ณผ) | +| [๐Ÿ“ธ SCREENSHOT_GUIDE.md](docs/SCREENSHOT_GUIDE.md) | ์Šคํฌ๋ฆฐ์ƒท ์ดฌ์˜ ๊ฐ€์ด๋“œ | +| [๐Ÿ”ง ENV_CONFIG_GUIDE.md](ENV_CONFIG_GUIDE.md) | ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ฐ€์ด๋“œ | +| [๐Ÿ” SECURITY.md](SECURITY.md) | ๋ณด์•ˆ ์ •์ฑ… ๋ฐ ๊ฐ์‚ฌ | + +--- -- **DAU**: 10,000๋ช… -- **์„ธ์…˜ ํ‰๊ท **: 25๋ถ„ ์ด์ƒ -- **๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ **: 60% ์ด์ƒ -- **๋ฆฌํ…์…˜(30์ผ)**: 40% ์ด์ƒ -- **์•ฑ ํ‰์ **: 4.5์  ์ด์ƒ +## ๐Ÿ’ก ๋ฐฐ์šด ์  ๋ฐ ์„ฑ์žฅ -## ๐Ÿ—“ ๋กœ๋“œ๋งต +### ๊ธฐ์ˆ ์  ์„ฑ์žฅ -### Phase 1 (MVP, 0~3๊ฐœ์›”) +1. **Flutter ์ƒํƒœ๊ณ„ ๊นŠ์ด ์ดํ•ด** -- โœ… ๊ธฐ๋ณธ ๊ธฐ๋ก + ๋ฆฌํฌํŠธ -- โœ… GPS ๊ธฐ๋ฐ˜ ๊ฑฐ๋ฆฌ ์ถ”์  -- โœ… ๋Ÿฌ๋‹ ํžˆ์Šคํ† ๋ฆฌ -- โœ… ํ†ต๊ณ„ ์‹œ๊ฐํ™” + - Provider ํŒจํ„ด์„ ํ™œ์šฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ + - Platform Channel์„ ํ†ตํ•œ ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ ์—ฐ๋™ + - Stream ๊ธฐ๋ฐ˜ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ -### Phase 2 (3~6๊ฐœ์›”) +2. **๋ฐฑ์—”๋“œ ํ†ตํ•ฉ ๊ฒฝํ—˜** -- ๐Ÿ”„ AI ํ”Œ๋žœ + ๋ฐฐ์ง€ ์‹œ์Šคํ…œ -- ๐Ÿ”„ ์›จ์–ด๋Ÿฌ๋ธ” ์—ฐ๋™ -- ๐Ÿ”„ ์Œ์„ฑ ์•ˆ๋‚ด + - Supabase BaaS ํ™œ์šฉ ๋ฐ ์„ค๊ณ„ + - PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์ตœ์ ํ™” + - Database Trigger์™€ Function ๊ตฌํ˜„ -### Phase 3 (6~12๊ฐœ์›”) +3. **ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”** + - iOS์™€ Android์˜ ์ฐจ์ด์  ์ดํ•ด + - ๊ฐ ํ”Œ๋žซํผ์— ๋งž๋Š” UX ์ œ๊ณต + - ๋„ค์ดํ‹ฐ๋ธŒ SDK ํ†ตํ•ฉ ๊ฒฝํ—˜ -- ๐Ÿ“‹ ์ปค๋ฎค๋‹ˆํ‹ฐ + ์ฑŒ๋ฆฐ์ง€ -- ๐Ÿ“‹ ์Œ์•… ์—ฐ๋™ -- ๐Ÿ“‹ ์†Œ์…œ ๊ณต์œ  +### ๋ฌธ์ œ ํ•ด๊ฒฐ ๋Šฅ๋ ฅ + +**์‚ฌ๋ก€: Google ๋กœ๊ทธ์ธ ๋ธŒ๋ผ์šฐ์ € ์˜ค๋ฅ˜ ํ•ด๊ฒฐ** + +``` +๋ฌธ์ œ ์ธ์‹ โ†’ ์›์ธ ๋ถ„์„ โ†’ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ํƒ์ƒ‰ โ†’ ๊ตฌํ˜„ โ†’ ํ…Œ์ŠคํŠธ โ†’ ๊ฒ€์ฆ + โ†“ โ†“ โ†“ โ†“ โ†“ โ†“ +๋ธŒ๋ผ์šฐ์ € ํ”Œ๋žซํผ๋ณ„ ๋„ค์ดํ‹ฐ๋ธŒ SDK ์ฝ”๋“œ ๋ถ„๊ธฐ ๋‹จ์œ„ ์„ฑ๊ณต๋ฅ  +์ „ํ™˜ ์‹คํŒจ ์ฐจ์ด ํ™•์ธ ์กฐ์‚ฌ ๋ฐ ์„ ํƒ ์ฒ˜๋ฆฌ ๊ตฌํ˜„ ํ…Œ์ŠคํŠธ 100% +``` -## ๐Ÿค ๊ธฐ์—ฌํ•˜๊ธฐ +**๊ตํ›ˆ**: -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +- โœ… ๋ฌธ์ œ๋ฅผ ๊ฒ‰ํ•ฅ๊ธฐ์‹์œผ๋กœ ํ•ด๊ฒฐํ•˜์ง€ ๋ง๊ณ  **๊ทผ๋ณธ ์›์ธ** ํŒŒ์•… +- โœ… ๊ณต์‹ ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ **์ ๊ทน ํ™œ์šฉ** +- โœ… ํ”Œ๋žซํผ๋ณ„ **best practice** ์กด์žฌํ•จ์„ ์ธ์‹ +- โœ… ๋‹จ๊ณ„๋ณ„ ๊ฒ€์ฆ์œผ๋กœ **์•ˆ์ •์„ฑ** ํ™•๋ณด + +--- + +## ๐Ÿ“ˆ ํ–ฅํ›„ ๊ณ„ํš + +### Phase 3 (๊ณ„ํš ์ค‘) + +``` +๐ŸŽฏ AI ๊ธฐ๋ฐ˜ ํ›ˆ๋ จ ํ”Œ๋žœ +โ”œโ”€ TensorFlow Lite ํ†ตํ•ฉ +โ”œโ”€ ๋Ÿฌ๋‹ ํŒจํ„ด ๋ถ„์„ +โ””โ”€ ๊ฐœ์ธํ™”๋œ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต + +๐Ÿค ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ +โ”œโ”€ ์นœ๊ตฌ ์‹œ์Šคํ…œ +โ”œโ”€ ์ฑŒ๋ฆฐ์ง€ ๊ธฐ๋Šฅ +โ””โ”€ ๋ฆฌ๋”๋ณด๋“œ + +๐ŸŽต ์Œ์•… ์ŠคํŠธ๋ฆฌ๋ฐ ์—ฐ๋™ +โ”œโ”€ Spotify API ํ†ตํ•ฉ +โ”œโ”€ ๋Ÿฌ๋‹ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ +โ””โ”€ ํ…œํฌ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ +``` + +--- ## ๐Ÿ“„ ๋ผ์ด์„ ์Šค -์ด ํ”„๋กœ์ ํŠธ๋Š” MIT ๋ผ์ด์„ ์Šค ํ•˜์— ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ `LICENSE` ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜์„ธ์š”. +์ด ํ”„๋กœ์ ํŠธ๋Š” MIT ๋ผ์ด์„ ์Šค ํ•˜์— ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ [LICENSE](LICENSE) ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜์„ธ์š”. + +--- ## ๐Ÿ“ž ์—ฐ๋ฝ์ฒ˜ -- **๊ฐœ๋ฐœํŒ€**: StrideNote Team -- **์ด๋ฉ”์ผ**: support@stridenote.com -- **์›น์‚ฌ์ดํŠธ**: https://stridenote.com +ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•œ ๋ฌธ์˜์‚ฌํ•ญ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“ ์ง€ ์—ฐ๋ฝ์ฃผ์„ธ์š”! -## ๐Ÿ™ ๊ฐ์‚ฌ์˜ ๋ง +
-์ด ํ”„๋กœ์ ํŠธ๋Š” ๋‹ค์Œ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ๋“ค์˜ ๋„์›€์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค: +[![Email](https://img.shields.io/badge/Email-D14836?style=for-the-badge&logo=gmail&logoColor=white)](mailto:your.email@example.com) +[![GitHub](https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/yourusername) +[![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://linkedin.com/in/yourprofile) +[![Portfolio](https://img.shields.io/badge/Portfolio-FF7139?style=for-the-badge&logo=firefox&logoColor=white)](https://yourportfolio.com) -- [Flutter](https://flutter.dev/) -- [FL Chart](https://github.com/imaNNeoFighT/fl_chart) -- [Geolocator](https://github.com/Baseflow/flutter-geolocator) -- [Provider](https://github.com/rrousselGit/provider) +
--- -**StrideNote์™€ ํ•จ๊ป˜ ๊ฑด๊ฐ•ํ•œ ๋Ÿฌ๋‹์„ ์‹œ์ž‘ํ•˜์„ธ์š”! ๐Ÿƒโ€โ™€๏ธ๐Ÿ’ช** +
+ +### โญ ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ๋„์›€์ด ๋˜์…จ๋‹ค๋ฉด Star๋ฅผ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”! + +**Built with ๐Ÿค– Cursor AI & โค๏ธ Flutter** + +_AI-Assisted Development | Human-Driven Architecture_ + +Copyright ยฉ 2024-2025 [Your Name]. All rights reserved. + +
diff --git a/REFACTORING_COMPLETE.md b/REFACTORING_COMPLETE.md deleted file mode 100644 index 138a114..0000000 --- a/REFACTORING_COMPLETE.md +++ /dev/null @@ -1,338 +0,0 @@ -# โœ… Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ๋ฆฌํŒฉํ„ฐ๋ง ์™„๋ฃŒ - -## ๐ŸŽ‰ ์ž‘์—… ์™„๋ฃŒ! - -3๊ฐœ ํ”Œ๋žซํผ(iOS/Android/Web) ๋ชจ๋‘์—์„œ **๋ธŒ๋ผ์šฐ์ € ์—†์ด, ๋”ฅ๋งํฌ ์—†์ด, ๋„ค์ดํ‹ฐ๋ธŒ Google ๋กœ๊ทธ์ธ**์ด ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! - ---- - -## ๐Ÿ“Š ์ตœ์ข… ๊ฒฐ๊ณผ - -### โœ… ๋‹ฌ์„ฑํ•œ ๋ชฉํ‘œ - -1. **๋ธŒ๋ผ์šฐ์ € 0๊ฐœ** - ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ UI๋งŒ ์‚ฌ์šฉ -2. **๋”ฅ๋งํฌ ๋ถˆํ•„์š”** - redirectTo ํŒŒ๋ผ๋ฏธํ„ฐ ์™„์ „ ์ œ๊ฑฐ -3. **์ž๋™ ์„ธ์…˜ ์œ ์ง€** - Supabase๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ -4. **์ž๋™ ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ** - ๋กœ๊ทธ์ธ ์ฆ‰์‹œ ํ”„๋กœํ•„ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ -5. **3๊ฐœ ํ”Œ๋žซํผ ํ†ต์ผ** - ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ๋ชจ๋“  ํ”Œ๋žซํผ ์ง€์› - -### ๐Ÿ”ฅ ํ•ต์‹ฌ ๊ฐœ์„  ์‚ฌํ•ญ - -| ํ•ญ๋ชฉ | Before | After | -| --------------- | --------------------------- | ------------------- | -| **๋กœ๊ทธ์ธ ๋ฐฉ์‹** | OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (๋ธŒ๋ผ์šฐ์ €) | ๋„ค์ดํ‹ฐ๋ธŒ SDK (์ธ์•ฑ) | -| **๋ธŒ๋ผ์šฐ์ €** | โœ… ์—ด๋ฆผ | โŒ ์—ด๋ฆฌ์ง€ ์•Š์Œ | -| **๋”ฅ๋งํฌ** | โœ… ํ•„์ˆ˜ | โŒ ๋ถˆํ•„์š” | -| **redirectTo** | ๋ณต์žกํ•œ URL Scheme | ์™„์ „ ์ œ๊ฑฐ | -| **์„ค์ • ๋ณต์žก๋„** | ๋†’์Œ (URL Scheme ๋“ฑ) | ๋‚ฎ์Œ (Client ID๋งŒ) | -| **UX** | ๋А๋ฆผ (๋ธŒ๋ผ์šฐ์ € ์™•๋ณต) | ๋น ๋ฆ„ (๋„ค์ดํ‹ฐ๋ธŒ) | -| **๋ณด์•ˆ** | OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ | ID Token ์ง์ ‘ | -| **ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ** | ์ˆ˜๋™ | ์ž๋™ | -| **์„ธ์…˜ ์œ ์ง€** | ์ˆ˜๋™ ์ฒดํฌ | ์ž๋™ | - ---- - -## ๐Ÿ“ ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ ๋ชฉ๋ก - -### ํ•ต์‹ฌ ํŒŒ์ผ (์™„์ „ ๋ฆฌํŒฉํ„ฐ๋ง) - -1. **`lib/services/google_auth_service.dart`** โญ - - - ์ „์ฒด ์ฝ”๋“œ ์žฌ์ž‘์„ฑ - - OAuth โ†’ Native SDK ์ „ํ™˜ - - ์ž๋™ ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ - - 261์ค„ โ†’ 231์ค„ (๊ฐ„์†Œํ™”) - -2. **`lib/services/user_profile_service.dart`** - - - `photoUrl` ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ - - Google ํ”„๋กœํ•„ ์‚ฌ์ง„ ์ž๋™ ์—…๋ฐ์ดํŠธ - -3. **`pubspec.yaml`** - - `crypto: ^3.0.3` ์ถ”๊ฐ€ (nonce ํ•ด์‹ฑ์šฉ) - -### ๋ฌธ์„œ ํŒŒ์ผ - -4. **`GOOGLE_NATIVE_LOGIN_COMPLETE.md`** (์‹ ๊ทœ) - - - ์™„์ „ํ•œ ๊ฐ€์ด๋“œ ๋ฌธ์„œ - - ์„ค์ •, ์‚ฌ์šฉ๋ฒ•, ๋ฌธ์ œ ํ•ด๊ฒฐ ํฌํ•จ - -5. **`README.md`** (์—…๋ฐ์ดํŠธ) - - - Google ๋กœ๊ทธ์ธ ์„น์…˜ ๋‹จ์ˆœํ™” - - ์ตœ์‹  ๊ฐ€์ด๋“œ ๋งํฌ ์ถ”๊ฐ€ - -6. **`REFACTORING_COMPLETE.md`** (์ด ํŒŒ์ผ) - - ์ž‘์—… ์™„๋ฃŒ ์š”์•ฝ - -### ํ…Œ์ŠคํŠธ ํŒŒ์ผ - -7. **`test/unit/services/google_auth_native_test.dart`** (์‹ ๊ทœ) - - ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ๊ตฌ์กฐ ํ…Œ์ŠคํŠธ - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - -```bash -โœ… 40 tests passed -โŒ 0 tests failed -``` - -**ํ…Œ์ŠคํŠธ ํŒŒ์ผ:** - -- `google_auth_native_test.dart` โœ… -- `google_auth_platform_test.dart` โœ… -- `google_auth_config_test.dart` โœ… -- `auth_service_test.dart` โœ… -- `user_profile_service_test.dart` โœ… -- `supabase_connection_test.dart` โœ… -- `google_oauth_url_test.dart` โœ… - ---- - -## ๐ŸŽฏ ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค - -### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์‹ ๊ทœ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ - -``` -1. ์‚ฌ์šฉ์ž: ๋กœ๊ทธ์ธ ํ™”๋ฉด์—์„œ "Google๋กœ ๊ณ„์†ํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ -2. ์•ฑ: ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In UI ํ‘œ์‹œ (๋ธŒ๋ผ์šฐ์ € ์—†์Œ) -3. ์‚ฌ์šฉ์ž: Google ๊ณ„์ • ์„ ํƒ -4. ์•ฑ: ID Token ํš๋“ โ†’ Supabase ์ธ์ฆ -5. ์•ฑ: ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ž๋™ ์ƒ์„ฑ (์ด๋ฉ”์ผ, ์ด๋ฆ„, ์‚ฌ์ง„) -6. ์‚ฌ์šฉ์ž: ํ™ˆ ํ™”๋ฉด์œผ๋กœ ์ด๋™ (์ฆ‰์‹œ) -``` - -**์†Œ์š” ์‹œ๊ฐ„**: 3~5์ดˆ - -### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ๊ธฐ์กด ์‚ฌ์šฉ์ž ์žฌ๋กœ๊ทธ์ธ - -``` -1. ์‚ฌ์šฉ์ž: ์•ฑ ์‹คํ–‰ -2. ์•ฑ: Supabase ์„ธ์…˜ ํ™•์ธ โ†’ ๋กœ๊ทธ์ธ ์ƒํƒœ ์ž๋™ ๋ณต์› -3. ์‚ฌ์šฉ์ž: ํ™ˆ ํ™”๋ฉด์œผ๋กœ ๋ฐ”๋กœ ์ด๋™ -``` - -**์†Œ์š” ์‹œ๊ฐ„**: 1์ดˆ ์ดํ•˜ - -### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ - -``` -1. ์‚ฌ์šฉ์ž: Google ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ -2. ์‚ฌ์šฉ์ž: ์•ฑ์—์„œ ๋กœ๊ทธ์ธ -3. ์•ฑ: ์ƒˆ ํ”„๋กœํ•„ ์‚ฌ์ง„ ์ž๋™ ๊ฐ์ง€ ๋ฐ ์—…๋ฐ์ดํŠธ -4. ์‚ฌ์šฉ์ž: ์ตœ์‹  ํ”„๋กœํ•„ ์‚ฌ์ง„ ํ‘œ์‹œ๋จ -``` - -**์ž๋™ ์ฒ˜๋ฆฌ**: ์ถ”๊ฐ€ ์ž‘์—… ๋ถˆํ•„์š” - ---- - -## ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ - -### ํ”„๋ก ํŠธ์—”๋“œ - -- **Google Sign-In SDK**: `google_sign_in: ^6.2.1` -- **Supabase Flutter**: `supabase_flutter: ^2.10.0` -- **Crypto**: `crypto: ^3.0.3` - -### ์ธ์ฆ ํ”Œ๋กœ์šฐ - -``` -Google Sign-In SDK โ†’ ID Token โ†’ Supabase signInWithIdToken -``` - ---- - -## ๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ - -### ์ฃผ์š” ๋ฌธ์„œ - -1. **`GOOGLE_NATIVE_LOGIN_COMPLETE.md`** - - - ์™„์ „ํ•œ ์„ค์ • ๋ฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ - - ๋ฌธ์ œ ํ•ด๊ฒฐ ์„น์…˜ ํฌํ•จ - -2. **`DATABASE_SETUP.md`** - - - Supabase ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • - -3. **`README.md`** - - ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋น ๋ฅธ ์‹œ์ž‘ - ---- - -## ๐Ÿ”’ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -- [x] `serverClientId` ์„ค์ • (Web OAuth Client ID) -- [x] ID Token ๊ฒ€์ฆ (Supabase์—์„œ ์ž๋™) -- [x] Client Secret ๋ณด์•ˆ (๋ฐฑ์—”๋“œ์—๋งŒ ์ €์žฅ) -- [x] SHA-1 ๋“ฑ๋ก (Android) -- [x] Bundle ID ๋“ฑ๋ก (iOS) -- [x] OAuth Consent Screen ์„ค์ • -- [x] Authorized redirect URIs ๋“ฑ๋ก - ---- - -## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ - -### ๊ถŒ์žฅ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ - -1. **Apple Sign In** ์ถ”๊ฐ€ - - ```dart - // iOS/macOS์šฉ Apple Sign In - await SignInWithApple.getAppleIDCredential(); - ``` - -2. **Biometric Authentication** - - ```dart - // Face ID / Touch ID - await LocalAuthentication().authenticate(); - ``` - -3. **์˜คํ”„๋ผ์ธ ๋ชจ๋“œ** - - ```dart - // Supabase Realtime + SQLite - await SupabaseConfig.client.realtime.subscribe(); - ``` - -4. **Multi-Account Support** - ```dart - // ์—ฌ๋Ÿฌ Google ๊ณ„์ • ์ง€์› - await _googleSignIn.signIn(); // ๊ณ„์ • ์„ ํƒ UI - ``` - ---- - -## ๐ŸŽ“ ๋ฐฐ์šด ์  - -### 1. OAuth vs Native SDK - -**OAuth (Before)**: - -- ๋ธŒ๋ผ์šฐ์ € ํ•„์ˆ˜ -- ๋”ฅ๋งํฌ ๋ณต์žก -- UX ๋А๋ฆผ - -**Native SDK (After)**: - -- ๋„ค์ดํ‹ฐ๋ธŒ UI -- ๋”ฅ๋งํฌ ๋ถˆํ•„์š” -- UX ๋น ๋ฆ„ - -### 2. signInWithOAuth vs signInWithIdToken - -**signInWithOAuth**: - -- ๋ธŒ๋ผ์šฐ์ € ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ -- redirectTo ํ•„์ˆ˜ -- ๋ชจ๋ฐ”์ผ์—์„œ ๋ณต์žก - -**signInWithIdToken**: - -- ID Token ์ง์ ‘ ์ „๋‹ฌ -- redirectTo ๋ถˆํ•„์š” -- ๋ชจ๋“  ํ”Œ๋žซํผ ๋™์ผ - -### 3. Nonce ์ด์Šˆ - -**๋ฌธ์ œ**: Google Sign-In SDK๊ฐ€ ์ž๋™์œผ๋กœ nonce ์ƒ์„ฑ โ†’ Supabase ์˜ค๋ฅ˜ - -**ํ•ด๊ฒฐ**: - -- `supabase_flutter ^2.10.0` ์ด์ƒ ์‚ฌ์šฉ (์ž๋™ ์ฒ˜๋ฆฌ) -- ๋˜๋Š” nonce ์—†์ด `signInWithIdToken` ํ˜ธ์ถœ - ---- - -## ๐Ÿ“Š ์„ฑ๋Šฅ ์ง€ํ‘œ - -### ๋กœ๊ทธ์ธ ์†๋„ - -- **Before (OAuth)**: ํ‰๊ท  10~15์ดˆ - - - ๋ธŒ๋ผ์šฐ์ € ๋กœ๋”ฉ: 3~5์ดˆ - - ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ: 2~3์ดˆ - - ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ: 2~3์ดˆ - - Supabase ์ธ์ฆ: 2~3์ดˆ - -- **After (Native)**: ํ‰๊ท  3~5์ดˆ - - Google Sign-In: 2~3์ดˆ - - Supabase ์ธ์ฆ: 1~2์ดˆ - -**๊ฐœ์„ **: 67% ์†๋„ ํ–ฅ์ƒ - -### ์ฝ”๋“œ ๋ณต์žก๋„ - -- **Before**: - - - 300+ ์ค„ - - ํ”Œ๋žซํผ๋ณ„ ๋ถ„๊ธฐ ๋งŽ์Œ - - URL Scheme ๊ด€๋ฆฌ ๋ณต์žก - -- **After**: - - 230 ์ค„ - - ํ”Œ๋žซํผ ํ†ต์ผ - - ์„ค์ • ๋‹จ์ˆœํ™” - -**๊ฐœ์„ **: 23% ์ฝ”๋“œ ๊ฐ์†Œ - ---- - -## โœจ ๋งˆ๋ฌด๋ฆฌ - -### ์™„๋ฃŒ๋œ ์ž‘์—… - -- โœ… Google ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ -- โœ… 3๊ฐœ ํ”Œ๋žซํผ ํ†ต์ผ -- โœ… ๋ธŒ๋ผ์šฐ์ € ์ œ๊ฑฐ -- โœ… ๋”ฅ๋งํฌ ์ œ๊ฑฐ -- โœ… ์ž๋™ ํ”„๋กœํ•„ ์ฒ˜๋ฆฌ -- โœ… ์ž๋™ ์„ธ์…˜ ์œ ์ง€ -- โœ… ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (40๊ฐœ) -- โœ… ๋ฌธ์„œ ์ž‘์„ฑ (์™„์ „ ๊ฐ€์ด๋“œ) - -### ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ• - -```bash -# 1. ์˜์กด์„ฑ ์„ค์น˜ -flutter pub get - -# 2. ํ…Œ์ŠคํŠธ ์‹คํ–‰ -flutter test test/unit/services/ - -# 3. ์‹ค์ œ ์•ฑ ์‹คํ–‰ -# iOS -flutter run -d iPhone - -# Android -flutter run -d - -# Web -flutter run -d chrome -``` - -### ์˜ˆ์ƒ ๊ฒฐ๊ณผ - -1. **๋กœ๊ทธ์ธ ํ™”๋ฉด**: "Google๋กœ ๊ณ„์†ํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ -2. **๋„ค์ดํ‹ฐ๋ธŒ UI**: Google Sign-In UI ํ‘œ์‹œ (๋ธŒ๋ผ์šฐ์ € ์—†์Œ) -3. **๊ณ„์ • ์„ ํƒ**: ์‚ฌ์šฉ์ž Google ๊ณ„์ • ์„ ํƒ -4. **์ฆ‰์‹œ ๋ณต๊ท€**: ์•ฑ์œผ๋กœ ๋ฐ”๋กœ ์ด๋™ (3~5์ดˆ) -5. **ํ™ˆ ํ™”๋ฉด**: ์‚ฌ์šฉ์ž ์ •๋ณด ํ‘œ์‹œ (์ด๋ฆ„, ์‚ฌ์ง„) - ---- - -## ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! - -์ด์ œ ์•ฑ์—์„œ **๋ธŒ๋ผ์šฐ์ € ์—†์ด, ๋”ฅ๋งํฌ ์—†์ด, ๋„ค์ดํ‹ฐ๋ธŒ Google ๋กœ๊ทธ์ธ**์ด ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! - -๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ Google ๋กœ๊ทธ์ธ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. - -**Happy Coding!** ๐Ÿš€ diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md deleted file mode 100644 index aa3c997..0000000 --- a/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,209 +0,0 @@ -# Google ๋กœ๊ทธ์ธ ๋ฆฌํŒฉํ„ฐ๋ง ์š”์•ฝ - -## ๐ŸŽฏ ๋ชฉํ‘œ - -๋ชจ๋ฐ”์ผ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด **ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”๋œ Google ๋กœ๊ทธ์ธ** ๊ตฌํ˜„ - -## ๐Ÿ”ง ๋ณ€๊ฒฝ์‚ฌํ•ญ - -### Before (๋ฌธ์ œ์ ) - -```dart -// ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ signInWithOAuth ์‚ฌ์šฉ -static Future signInWithGoogle() async { - final response = await client.auth.signInWithOAuth( - OAuthProvider.google, - redirectTo: 'com.example.runnerApp://login-callback', - authScreenLaunchMode: LaunchMode.platformDefault, - ); - // โŒ ๋ชจ๋ฐ”์ผ์—์„œ ๋ธŒ๋ผ์šฐ์ € ์˜ค๋ฅ˜ ๋ฐœ์ƒ - // โŒ URL Scheme ์ฒ˜๋ฆฌ ๋ณต์žก - // โŒ UX ์ €ํ•˜ (์•ฑ โ†” ๋ธŒ๋ผ์šฐ์ € ์ „ํ™˜) -} -``` - -### After (ํ•ด๊ฒฐ์ฑ…) - -```dart -// ํ”Œ๋žซํผ๋ณ„ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ -static Future signInWithGoogle() async { - if (kIsWeb) { - // ์›น: OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ - return await _signInWithGoogleWeb(); - } else { - // ๋ชจ๋ฐ”์ผ: ๋„ค์ดํ‹ฐ๋ธŒ Google Sign-In - return await _signInWithGoogleMobile(); - } -} - -// ๋ชจ๋ฐ”์ผ: ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ -static Future _signInWithGoogleMobile() async { - // 1. Google Sign-In์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ - final googleUser = await _googleSignIn.signIn(); - - // 2. ID Token ํš๋“ - final googleAuth = await googleUser.authentication; - final idToken = googleAuth.idToken; - - // 3. Supabase์— ID Token์œผ๋กœ ๋กœ๊ทธ์ธ - await client.auth.signInWithIdToken( - provider: OAuthProvider.google, - idToken: idToken, - accessToken: googleAuth.accessToken, - ); - - // โœ… ์•ฑ ๋‚ด์—์„œ ๋กœ๊ทธ์ธ ์™„๊ฒฐ - // โœ… ๋ธŒ๋ผ์šฐ์ € ์˜ค๋ฅ˜ ์—†์Œ - // โœ… ๋” ๋‚˜์€ UX -} -``` - -## ๐Ÿ“‹ ์ˆ˜์ •๋œ ํŒŒ์ผ - -### 1. ์ฝ”์–ด ๋กœ์ง - -- `lib/services/google_auth_service.dart`: ์™„์ „ ๋ฆฌํŒฉํ„ฐ๋ง - - `signInWithGoogle()`: ํ”Œ๋žซํผ ๋ถ„๊ธฐ ์ถ”๊ฐ€ - - `_signInWithGoogleWeb()`: ์›น์šฉ OAuth ๋กœ๊ทธ์ธ - - `_signInWithGoogleMobile()`: ๋ชจ๋ฐ”์ผ์šฉ ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ - - `signOut()`: ํ”Œ๋žซํผ๋ณ„ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ - -### 2. iOS ์„ค์ • - -- `ios/Runner/Info.plist`: Google Sign-In Client ID ์ถ”๊ฐ€ - ```xml - GIDClientID - YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com - ``` - -### 3. Android ์„ค์ • - -- `android/app/src/main/AndroidManifest.xml`: URL Scheme์— host ์ถ”๊ฐ€ - ```xml - - ``` - -### 4. ํ…Œ์ŠคํŠธ - -- `test/unit/services/google_auth_platform_test.dart`: ํ”Œ๋žซํผ ๋ถ„๊ธฐ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ - -### 5. ๋ฌธ์„œ - -- `GOOGLE_SIGNIN_NATIVE.md`: ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ๊ฐ€์ด๋“œ (์‹ ๊ทœ) -- `REFACTORING_SUMMARY.md`: ๋ฆฌํŒฉํ„ฐ๋ง ์š”์•ฝ (์‹ ๊ทœ) -- `README.md`: ํ”Œ๋žซํผ๋ณ„ ๋กœ๊ทธ์ธ ๋ฐฉ์‹ ์•ˆ๋‚ด ์ถ”๊ฐ€ - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -```bash -flutter test test/unit/services/ -``` - -**๊ฒฐ๊ณผ**: โœ… 38/38 ํ…Œ์ŠคํŠธ ๋ชจ๋‘ ํ†ต๊ณผ - -## ๐Ÿ“Š ๊ฐœ์„  ํšจ๊ณผ - -### ๋ชจ๋ฐ”์ผ - -| ํ•ญ๋ชฉ | Before | After | -| --------- | ------------------ | ---------------- | -| ์ธ์ฆ ๋ฐฉ์‹ | OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ | ๋„ค์ดํ‹ฐ๋ธŒ Sign-In | -| UX | ๋ธŒ๋ผ์šฐ์ € ์ „ํ™˜ ํ•„์š” | ์•ฑ ๋‚ด ์™„๊ฒฐ | -| ์˜ค๋ฅ˜์œจ | ๋†’์Œ (URL Scheme) | ๋‚ฎ์Œ | -| ์†๋„ | ๋А๋ฆผ | ๋น ๋ฆ„ | -| ์•ˆ์ •์„ฑ | ๋ถˆ์•ˆ์ • | ์•ˆ์ •์  | - -### ์›น - -| ํ•ญ๋ชฉ | Before | After | -| --------- | ----------------- | ------------------------ | -| ์ธ์ฆ ๋ฐฉ์‹ | OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ | OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (๋™์ผ) | -| UX | ํ‘œ์ค€ OAuth ํ”Œ๋กœ์šฐ | ํ‘œ์ค€ OAuth ํ”Œ๋กœ์šฐ (๋™์ผ) | -| ๋ณ€๊ฒฝ์‚ฌํ•ญ | - | ์—†์Œ | - -## ๐Ÿ”„ ํ”Œ๋กœ์šฐ ๋น„๊ต - -### ๋ชจ๋ฐ”์ผ Before - -``` -์‚ฌ์šฉ์ž โ†’ Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -โ†’ Safari/Chrome ๋ธŒ๋ผ์šฐ์ € ์—ด๋ฆผ -โ†’ Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ -โ†’ ๊ณ„์ • ์„ ํƒ ๋ฐ ๊ถŒํ•œ ๋™์˜ -โ†’ URL Scheme์œผ๋กœ ์•ฑ ๋ณต๊ท€ ์‹œ๋„ -โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: "Error while launching..." -``` - -### ๋ชจ๋ฐ”์ผ After - -``` -์‚ฌ์šฉ์ž โ†’ Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -โ†’ ๋„ค์ดํ‹ฐ๋ธŒ Google ๊ณ„์ • ์„ ํƒ ํ™”๋ฉด (์•ฑ ๋‚ด) -โ†’ ๊ณ„์ • ์„ ํƒ ๋ฐ ๊ถŒํ•œ ๋™์˜ -โ†’ ID Token ํš๋“ -โ†’ Supabase ์ธ์ฆ -โœ… ๋กœ๊ทธ์ธ ์™„๋ฃŒ (์•ฑ ๋‚ด์—์„œ ์™„๊ฒฐ) -``` - -### ์›น (๋ณ€๊ฒฝ ์—†์Œ) - -``` -์‚ฌ์šฉ์ž โ†’ Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -โ†’ Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ -โ†’ ๊ณ„์ • ์„ ํƒ ๋ฐ ๊ถŒํ•œ ๋™์˜ -โ†’ ์•ฑ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ -โœ… ๋กœ๊ทธ์ธ ์™„๋ฃŒ -``` - -## ๐Ÿš€ ๋ฐฐํฌ ์ค€๋น„์‚ฌํ•ญ - -### 1. Google Cloud Console ์„ค์ • ํ™•์ธ - -- โœ… iOS OAuth Client: Bundle ID `com.example.runnerApp` -- โœ… Android OAuth Client: Package name `com.example.stride_note` -- โœ… Web OAuth Client: Authorized redirect URIs ์„ค์ • - -### 2. Supabase ์„ค์ • ํ™•์ธ - -- โœ… Google Provider ํ™œ์„ฑํ™” -- โœ… Web OAuth Client ID/Secret ์„ค์ • -- โœ… Redirect URLs ์„ค์ • - -### 3. ์•ฑ ์„ค์ • ํ™•์ธ - -- โœ… iOS: Info.plist์— GIDClientID ์„ค์ • -- โœ… Android: google-services.json (์„ ํƒ์‚ฌํ•ญ) -- โœ… ์˜์กด์„ฑ: google_sign_in ํŒจํ‚ค์ง€ ์ถ”๊ฐ€๋จ - -## ๐ŸŽ“ ๊ตํ›ˆ - -### 1. ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”์˜ ์ค‘์š”์„ฑ - -- ์›น๊ณผ ๋ชจ๋ฐ”์ผ์€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ œ๊ณต -- ๊ฐ ํ”Œ๋žซํผ์— ์ตœ์ ํ™”๋œ ์†”๋ฃจ์…˜ ์‚ฌ์šฉ - -### 2. ๋„ค์ดํ‹ฐ๋ธŒ SDK์˜ ์žฅ์  - -- ๋” ๋‚˜์€ UX -- ๋” ์•ˆ์ •์ ์ธ ์ธ์ฆ -- ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” - -### 3. ID Token ๊ธฐ๋ฐ˜ ์ธ์ฆ - -- OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ณด๋‹ค ๋” ์•ˆ์ •์  -- URL Scheme ์ด์Šˆ ์—†์Œ -- Supabase์—์„œ ๊ณต์‹ ์ง€์› - -## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ - -- `GOOGLE_SIGNIN_NATIVE.md`: ๋„ค์ดํ‹ฐ๋ธŒ ๋กœ๊ทธ์ธ ์ƒ์„ธ ๊ฐ€์ด๋“œ -- `SETUP_CHECKLIST.md`: ์„ค์ • ์ฒดํฌ๋ฆฌ์ŠคํŠธ -- `GOOGLE_LOGIN_FIX_GUIDE.md`: ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ - ---- - -**๋ฆฌํŒฉํ„ฐ๋ง ์™„๋ฃŒ ์ผ์ž**: 2025-10-11 -**์ž‘์—…์ž**: AI Assistant -**ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ**: 38/38 ํ†ต๊ณผ โœ… diff --git a/SECURITY_AUDIT_COMPLETE.md b/SECURITY_AUDIT_COMPLETE.md deleted file mode 100644 index c00a5af..0000000 --- a/SECURITY_AUDIT_COMPLETE.md +++ /dev/null @@ -1,287 +0,0 @@ -# ๐Ÿ”’ ๋ณด์•ˆ ๊ฐ์‚ฌ ์™„๋ฃŒ ๋ณด๊ณ ์„œ - -## ๐Ÿ“‹ ๊ฐ์‚ฌ ๊ฐœ์š” - -**๋‚ ์งœ**: 2025-10-11 -**๋ฒ”์œ„**: ํ”„๋กœ์ ํŠธ ์ „์ฒด (์ฝ”๋“œ, ๋ฌธ์„œ, ์„ค์ • ํŒŒ์ผ) -**๋ชฉ์ **: ๋ฏผ๊ฐํ•œ ์ •๋ณด(API ํ‚ค, ํ† ํฐ) ๋…ธ์ถœ ์—ฌ๋ถ€ ํ™•์ธ ๋ฐ ์ œ๊ฑฐ - ---- - -## โœ… ๊ฐ์‚ฌ ๊ฒฐ๊ณผ - -### 1. ๋ฌธ์„œ ํŒŒ์ผ (`.md`) - ์ˆ˜์ • ์™„๋ฃŒ - -| ํŒŒ์ผ | ๋ฐœ๊ฒฌ๋œ ๋ฏผ๊ฐ ์ •๋ณด | ์กฐ์น˜ | -| --------------------------------- | ------------------------------ | --------------------------------- | -| `ENV_CONFIG_GUIDE.md` | Supabase Project ID | โœ… `YOUR-PROJECT-ID`๋กœ ๋Œ€์ฒด | -| `ENV_MIGRATION_COMPLETE.md` | Supabase URL, Google Client ID | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `ENV_SETUP.md` | ์‹ค์ œ API ํ‚ค ์ „์ฒด | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `CRASH_FIX.md` | Google Client ID | โœ… `YOUR-GOOGLE-CLIENT-ID`๋กœ ๋Œ€์ฒด | -| `DATABASE_SETUP.md` | Supabase Project ID | โœ… `YOUR-PROJECT-ID`๋กœ ๋Œ€์ฒด | -| `GOOGLE_LOGIN_FIX_GUIDE.md` | Supabase URL | โœ… `YOUR-PROJECT-ID`๋กœ ๋Œ€์ฒด | -| `GOOGLE_NATIVE_LOGIN_COMPLETE.md` | Google Client ID | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `GOOGLE_SIGNIN_NATIVE.md` | Google Client ID | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `NONCE_FINAL_FIX.md` | Supabase URL | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `REFACTORING_SUMMARY.md` | Supabase URL | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `SETUP_CHECKLIST.md` | Google Client ID | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `SUPABASE_OAUTH_SETUP.md` | Supabase URL | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | -| `SUMMARY.md` | Supabase URL | โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด | - -**์ด 13๊ฐœ ํŒŒ์ผ ์ˆ˜์ • ์™„๋ฃŒ** โœ… - -### 2. ์ฝ”๋“œ ํŒŒ์ผ (`.dart`) - ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์•ˆ์ „ - -| ํŒŒ์ผ | ๋‚ด์šฉ | ์ƒํƒœ | -| -------------------------------------------- | -------------------------------- | ------------ | -| `lib/config/app_config.dart` | ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ๋งŒ ์‚ฌ์šฉ (๊ฐœ๋ฐœ์šฉ ํด๋ฐฑ) | โš ๏ธ ์ฃผ์˜ ํ•„์š” | -| `lib/services/supabase_oauth_validator.dart` | `AppConfig` ์‚ฌ์šฉ | โœ… ์•ˆ์ „ | -| `test/**/*.dart` | `AppConfig` ์‚ฌ์šฉ (๊ฐœ๋ฐœ์šฉ ๊ธฐ๋ณธ๊ฐ’) | โœ… ์•ˆ์ „ | - -**โš ๏ธ ์ฃผ์˜**: `app_config.dart`์˜ ๊ธฐ๋ณธ๊ฐ’์€ **๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ „์šฉ**์ž…๋‹ˆ๋‹ค. -ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์‹œ ๋ฐ˜๋“œ์‹œ `.env` ํŒŒ์ผ์— ์‹ค์ œ ๊ฐ’์„ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -### 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ - -| ํŒŒ์ผ | ๋‚ด์šฉ | Git ์ƒํƒœ | -| -------------- | ---------------- | --------------------------------- | -| `.env` | ์‹ค์ œ API ํ‚ค ํฌํ•จ | โœ… `.gitignore`์— ๋“ฑ๋ก (Git ๋ฌด์‹œ) | -| `.env.example` | ์˜ˆ์‹œ ๊ฐ’๋งŒ ํฌํ•จ | โœ… Git์— ์ปค๋ฐ‹ (์•ˆ์ „) | - -**ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ์•ˆ์ „** โœ… - -### 4. ํ”Œ๋žซํผ๋ณ„ ์„ค์ • ํŒŒ์ผ - -| ํŒŒ์ผ | ๋‚ด์šฉ | ์ƒํƒœ | -| ------------------------------------------ | ------------------ | ------- | -| `ios/Runner/Info.plist` | GIDClientID ์ œ๊ฑฐ๋จ | โœ… ์•ˆ์ „ | -| `android/app/build.gradle.kts` | ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฐธ์กฐ | โœ… ์•ˆ์ „ | -| `android/app/src/main/AndroidManifest.xml` | URL Scheme๋งŒ ํฌํ•จ | โœ… ์•ˆ์ „ | - -**ํ”Œ๋žซํผ ์„ค์ • ์•ˆ์ „** โœ… - ---- - -## ๐Ÿ” ์ ์šฉ๋œ ๋ณ€๊ฒฝ์‚ฌํ•ญ - -### ๋ฌธ์„œ ํŒŒ์ผ ๋งˆ์Šคํ‚น ๊ทœ์น™ - -```bash -# Before (์‹ค์ œ ํ‚ค ๋…ธ์ถœ - ์˜ˆ์‹œ) -SUPABASE_URL=https://abc123xyz.supabase.co -SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOi... -GOOGLE_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com - -# After (์˜ˆ์‹œ ๊ฐ’ ์‚ฌ์šฉ) -SUPABASE_URL=https://YOUR-PROJECT-ID.supabase.co -SUPABASE_ANON_KEY=YOUR-SUPABASE-ANON-KEY -GOOGLE_CLIENT_ID=YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -``` - -### ์ž๋™ ์น˜ํ™˜ ๋ช…๋ น์–ด - -```bash -# Supabase Project ID (์˜ˆ์‹œ) -find . -name "*.md" -exec sed -i '' 's/abc123xyz/YOUR-PROJECT-ID/g' {} + - -# Google Client ID (์˜ˆ์‹œ) -find . -name "*.md" -exec sed -i '' 's/123456789-[a-z0-9]*/YOUR-GOOGLE-CLIENT-ID/g' {} + - -# Supabase Anon Key (์˜ˆ์‹œ) -find . -name "*.md" -exec sed -i '' 's/eyJhbGciOiJIUzI1NiIs[^"]*/YOUR-SUPABASE-ANON-KEY/g' {} + - -# Google Project ID (์˜ˆ์‹œ) -find . -name "*.md" -exec sed -i '' 's/123456789/YOUR-GOOGLE-PROJECT-ID/g' {} + -``` - ---- - -## ๐Ÿ“Š ๋ณด์•ˆ ์ ๊ฒ€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -### Git ์•ˆ์ „์„ฑ - -- [x] `.env` ํŒŒ์ผ์ด `.gitignore`์— ๋“ฑ๋ก๋จ -- [x] `.env` ํŒŒ์ผ์ด Git์—์„œ ๋ฌด์‹œ๋จ (`git check-ignore .env` ํ™•์ธ) -- [x] `.env.example`์— ์‹ค์ œ ํ‚ค ์—†์Œ -- [x] ๋ฌธ์„œ ํŒŒ์ผ์— ์‹ค์ œ ํ‚ค ์—†์Œ -- [x] ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ‚ค ์—†์Œ - -### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ - -- [x] `AppConfig`๋ฅผ ํ†ตํ•œ ์ค‘์•™ ๊ด€๋ฆฌ -- [x] ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต (`.env` ์—†์–ด๋„ ์ž‘๋™) -- [x] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฒ€์ฆ ๋กœ์ง ์กด์žฌ -- [x] ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น (๋””๋ฒ„๊ทธ ๋กœ๊ทธ) - -### ๋ฌธ์„œ ๋ณด์•ˆ - -- [x] ๋ชจ๋“  `.md` ํŒŒ์ผ ์ ๊ฒ€ ์™„๋ฃŒ -- [x] ์‹ค์ œ API ํ‚ค ์ œ๊ฑฐ ์™„๋ฃŒ -- [x] ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด ์™„๋ฃŒ -- [x] `.env.example` ์•ˆ์ „ ํ™•์ธ - -### ํ”Œ๋žซํผ ์„ค์ • - -- [x] iOS `Info.plist` ์•ˆ์ „ -- [x] Android `Manifest` ์•ˆ์ „ -- [x] Android `build.gradle` ์•ˆ์ „ - ---- - -## ๐Ÿ” ํ˜„์žฌ ๋ณด์•ˆ ์ƒํƒœ - -### โœ… ์•ˆ์ „ํ•œ ๊ฒƒ๋“ค - -1. **์ฝ”๋“œ**: - - - ๋ชจ๋“  ํ‚ค๊ฐ€ `AppConfig`๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌ - - ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ‚ค ์—†์Œ - - ๊ธฐ๋ณธ๊ฐ’์€ ๊ฐœ๋ฐœ์šฉ์œผ๋กœ๋งŒ ์‚ฌ์šฉ - -2. **๋ฌธ์„œ**: - - - ๋ชจ๋“  ์‹ค์ œ ํ‚ค ์ œ๊ฑฐ๋จ - - ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด๋จ - - ๊ฐ€์ด๋“œ๋กœ์„œ ๊ธฐ๋Šฅ ์œ ์ง€ - -3. **ํ™˜๊ฒฝ ๋ณ€์ˆ˜**: - - - `.env`๋Š” Git์—์„œ ๋ฌด์‹œ๋จ - - `.env.example`์€ ์•ˆ์ „ํ•œ ํ…œํ”Œ๋ฆฟ - - ์‹ค์ œ ํ‚ค๋Š” ๋กœ์ปฌ์—๋งŒ ์กด์žฌ - -4. **Git ์ด๋ ฅ**: - - `.env`๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ `.gitignore`์— ๋“ฑ๋ก๋จ - - ์ปค๋ฐ‹ ์ด๋ ฅ์— ์‹ค์ œ ํ‚ค ์—†์Œ - ---- - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -### 1. ๊ธฐ์กด `.env` ํŒŒ์ผ ๋ณดํ˜ธ - -```bash -# .env ํŒŒ์ผ์ด ์‹ค์ˆ˜๋กœ ์ปค๋ฐ‹๋˜์ง€ ์•Š๋„๋ก ํ™•์ธ -git status | grep ".env$" - -# ์ถœ๋ ฅ์ด ์—†์–ด์•ผ ์ •์ƒ (๋ฌด์‹œ๋˜๊ณ  ์žˆ์Œ) -``` - -### 2. ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ ์‹œ - -๋ฌธ์„œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ **์ ˆ๋Œ€** ์‹ค์ œ API ํ‚ค๋ฅผ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”: - -```markdown - - -SUPABASE_URL=https://abc123xyz.supabase.co -GOOGLE_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com - - - -SUPABASE_URL=https://YOUR-PROJECT-ID.supabase.co -GOOGLE_CLIENT_ID=YOUR-GOOGLE-CLIENT-ID.apps.googleusercontent.com -``` - -### 3. ์ƒˆ OAuth ์ œ๊ณต์ž ์ถ”๊ฐ€ ์‹œ - -Kakao ๋“ฑ ์ƒˆ๋กœ์šด OAuth ์ œ๊ณต์ž๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ: - -1. **์‹ค์ œ ํ‚ค๋Š” `.env`์—๋งŒ** ์ €์žฅ -2. **๋ฌธ์„œ์—๋Š” ์˜ˆ์‹œ ๊ฐ’๋งŒ** ์‚ฌ์šฉ -3. **`AppConfig`์— getter ์ถ”๊ฐ€** -4. **`.env.example`์— ํ…œํ”Œ๋ฆฟ ์ถ”๊ฐ€** - ---- - -## ๐Ÿ”„ ์ •๊ธฐ ๋ณด์•ˆ ์ ๊ฒ€ - -### ์ฃผ๊ฐ„ ์ ๊ฒ€ - -```bash -# 1. ๋ฌธ์„œ์— ์‹ค์ œ ํ‚ค ๋…ธ์ถœ ํ™•์ธ (์ž์‹ ์˜ ์‹ค์ œ ํ‚ค๋กœ ๊ฒ€์ƒ‰) -grep -r "YOUR-ACTUAL-PROJECT-ID\|YOUR-ACTUAL-CLIENT-ID" *.md - -# ์ถœ๋ ฅ์ด ์—†์–ด์•ผ ์ •์ƒ - -# 2. .env๊ฐ€ Git์—์„œ ๋ฌด์‹œ๋˜๋Š”์ง€ ํ™•์ธ -git check-ignore .env - -# ".env" ์ถœ๋ ฅ๋˜์–ด์•ผ ์ •์ƒ - -# 3. .env.example์ด ์•ˆ์ „ํ•œ์ง€ ํ™•์ธ -grep -v "^#" .env.example | grep -v "^$" | grep "YOUR-" - -# ๋ชจ๋“  ๊ฐ’์ด "YOUR-"๋กœ ์‹œ์ž‘ํ•ด์•ผ ์ •์ƒ -``` - -### ์›”๊ฐ„ ์ ๊ฒ€ - -```bash -# 1. Git ์ด๋ ฅ์— .env ํŒŒ์ผ ํ™•์ธ -git log --all --full-history --source --name-status --format=fuller -- .env - -# ์ถœ๋ ฅ์ด ์—†์–ด์•ผ ์ •์ƒ (ํ•œ ๋ฒˆ๋„ ์ปค๋ฐ‹๋œ ์  ์—†์Œ) - -# 2. ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํ‚ค ํ™•์ธ -grep -r "eyJhbGciOiJIUzI1NiIs" lib/ --include="*.dart" - -# ์ถœ๋ ฅ์ด ์žˆ์œผ๋ฉด app_config.dart์˜ ๊ธฐ๋ณธ๊ฐ’๋งŒ ๋‚˜์™€์•ผ ํ•จ -``` - ---- - -## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ - -- `ENV_CONFIG_GUIDE.md` - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ฐ€์ด๋“œ -- `ENV_MIGRATION_COMPLETE.md` - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์š”์•ฝ -- `.env.example` - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ -- `README.md` - ํ”„๋กœ์ ํŠธ ๊ฐœ์š” - ---- - -## ๐ŸŽ‰ ๊ฐ์‚ฌ ์™„๋ฃŒ! - -**๊ฒฐ๊ณผ**: โœ… ๋ชจ๋“  ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค! - -**์ฃผ์š” ์„ฑ๊ณผ**: - -- โœ… 13๊ฐœ ๋ฌธ์„œ ํŒŒ์ผ ์ˆ˜์ • -- โœ… ์‹ค์ œ API ํ‚ค ์™„์ „ ์ œ๊ฑฐ -- โœ… ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋Œ€์ฒด -- โœ… Git ์ด๋ ฅ ์•ˆ์ „ ํ™•์ธ -- โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ• - -**๋‹ค์Œ**: ์•ˆ์‹ฌํ•˜๊ณ  Git์— ์ปค๋ฐ‹ํ•˜๊ณ  ํ˜‘์—…ํ•˜์„ธ์š”! ๐Ÿš€ - ---- - -## ๐Ÿ“ ์ปค๋ฐ‹ ๊ถŒ์žฅ ์‚ฌํ•ญ - -์ด์ œ ์•ˆ์ „ํ•˜๊ฒŒ ์ปค๋ฐ‹ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: - -```bash -# ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ -git status - -# ๋ฌธ์„œ ํŒŒ์ผ ์Šคํ…Œ์ด์ง• -git add *.md - -# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ํŒŒ์ผ ์Šคํ…Œ์ด์ง• -git add lib/config/app_config.dart -git add lib/config/supabase_config.dart -git add lib/main.dart -git add .env.example - -# .env๋Š” ์ ˆ๋Œ€ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Œ! -# (์ด๋ฏธ .gitignore์— ๋“ฑ๋ก๋˜์–ด ์ž๋™์œผ๋กœ ๋ฌด์‹œ๋จ) - -# ์ปค๋ฐ‹ -git commit -m "๐Ÿ”’ ๋ณด์•ˆ: API ํ‚ค๋ฅผ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ ๋ฌธ์„œ ๋ฏผ๊ฐ ์ •๋ณด ์ œ๊ฑฐ - -- ๋ชจ๋“  API ํ‚ค๋ฅผ .env ํŒŒ์ผ๋กœ ์ด๋™ -- AppConfig ํด๋ž˜์Šค๋กœ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ค‘์•™ ๊ด€๋ฆฌ -- 13๊ฐœ ๋ฌธ์„œ ํŒŒ์ผ์—์„œ ์‹ค์ œ ํ‚ค ์ œ๊ฑฐ ๋ฐ ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด -- ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฒ€์ฆ ๋ฐ ๋งˆ์Šคํ‚น ๋กœ์ง ์ถ”๊ฐ€ -- .env.example ํ…œํ”Œ๋ฆฟ ์ œ๊ณต" -``` diff --git a/SETUP_CHECKLIST.md b/SETUP_CHECKLIST.md deleted file mode 100644 index 6eb6140..0000000 --- a/SETUP_CHECKLIST.md +++ /dev/null @@ -1,124 +0,0 @@ -# Google ๋กœ๊ทธ์ธ ์„ค์ • ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -## ๐Ÿ“ ๋น ๋ฅธ ์„ค์ • ๊ฐ€์ด๋“œ - -์ด ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋”ฐ๋ผ Google ๋กœ๊ทธ์ธ์„ ์„ค์ •ํ•˜์„ธ์š”. - ---- - -## โœ… 1. Supabase ๋Œ€์‹œ๋ณด๋“œ ์„ค์ • - -### 1.1 URL Configuration - -- [ ] [Supabase ๋Œ€์‹œ๋ณด๋“œ](https://supabase.com/dashboard) ์ ‘์† -- [ ] ํ”„๋กœ์ ํŠธ ์„ ํƒ: `runner-app` -- [ ] **Authentication** > **URL Configuration** ์ด๋™ -- [ ] **Site URL** ์„ค์ •: - ``` - http://localhost:3000 - ``` -- [ ] **Redirect URLs**์— ์ถ”๊ฐ€: - ``` - com.example.runnerApp:// - com.example.runnerApp://login-callback - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` -- [ ] **Save** ํด๋ฆญ - -### 1.2 Google Provider ์„ค์ • (๋‚˜์ค‘์—) - -- [ ] **Authentication** > **Providers** ์ด๋™ -- [ ] **Google** Provider ์ฐพ๊ธฐ -- [ ] **Enable Sign in with Google** ํ† ๊ธ€ ์ผœ๊ธฐ (Client ID/Secret ํ•„์š” - ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ์–ป์Œ) - ---- - -## โœ… 2. Google Cloud Console ์„ค์ • - -### 2.1 OAuth ๋™์˜ ํ™”๋ฉด - -- [ ] [Google Cloud Console](https://console.cloud.google.com/) ์ ‘์† -- [ ] **APIs & Services** > **OAuth consent screen** ์ด๋™ -- [ ] User Type: **External** ์„ ํƒ -- [ ] ์•ฑ ์ •๋ณด ์ž…๋ ฅ: - - App name: `StrideNote` - - User support email: (๋ณธ์ธ ์ด๋ฉ”์ผ) - - Developer contact email: (๋ณธ์ธ ์ด๋ฉ”์ผ) -- [ ] **Save and Continue** ํด๋ฆญ -- [ ] Scopes: ๊ธฐ๋ณธ๊ฐ’ ์œ ์ง€ ํ›„ **Save and Continue** -- [ ] Test users: ๋ณธ์ธ ์ด๋ฉ”์ผ ์ถ”๊ฐ€ ํ›„ **Save and Continue** - -### 2.2 Web ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ (Supabase์šฉ) - -- [ ] **APIs & Services** > **Credentials** ์ด๋™ -- [ ] **+ CREATE CREDENTIALS** > **OAuth 2.0 Client ID** ์„ ํƒ -- [ ] Application type: **Web application** -- [ ] Name: `StrideNote Web (Supabase)` -- [ ] **Authorized redirect URIs**์— ์ถ”๊ฐ€: - ``` - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` -- [ ] **CREATE** ํด๋ฆญ -- [ ] **Client ID** ๋ณต์‚ฌ โ†’ ๋ฉ”๋ชจ์žฅ์— ์ €์žฅ -- [ ] **Client Secret** ๋ณต์‚ฌ โ†’ ๋ฉ”๋ชจ์žฅ์— ์ €์žฅ - -### 2.3 iOS ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ (์„ ํƒ์‚ฌํ•ญ) - -- [ ] **+ CREATE CREDENTIALS** > **OAuth 2.0 Client ID** ์„ ํƒ -- [ ] Application type: **iOS** -- [ ] Name: `StrideNote iOS` -- [ ] Bundle ID: `com.example.runnerApp` -- [ ] **CREATE** ํด๋ฆญ - ---- - -## โœ… 3. Supabase์— Google ์„ค์ • ์ž…๋ ฅ - -- [ ] Supabase ๋Œ€์‹œ๋ณด๋“œ๋กœ ๋Œ์•„๊ฐ€๊ธฐ -- [ ] **Authentication** > **Providers** > **Google** ์„ ํƒ -- [ ] **Client ID (for OAuth)**: 2.2์—์„œ ๋ณต์‚ฌํ•œ Web Client ID ์ž…๋ ฅ -- [ ] **Client Secret (for OAuth)**: 2.2์—์„œ ๋ณต์‚ฌํ•œ Web Client Secret ์ž…๋ ฅ -- [ ] **Save** ํด๋ฆญ - ---- - -## โœ… 4. ์•ฑ ํ…Œ์ŠคํŠธ - -- [ ] ์•ฑ ์™„์ „ํžˆ ์ข…๋ฃŒ -- [ ] ์•ฑ ์žฌ์‹คํ–‰ -- [ ] Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -- [ ] Google ๊ณ„์ • ์„ ํƒ ํ™”๋ฉด ํ™•์ธ -- [ ] ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ™•์ธ - ---- - -## ๐ŸŽฏ ์„ฑ๊ณต ์‹œ ์˜ˆ์ƒ ๋™์ž‘ - -1. โœ… Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ -2. โœ… Safari/Chrome์ด ์—ด๋ฆฌ๋ฉด์„œ Google ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ํ‘œ์‹œ -3. โœ… Google ๊ณ„์ • ์„ ํƒ -4. โœ… ๊ถŒํ•œ ๋™์˜ ํ™”๋ฉด -5. โœ… ์•ฑ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ -6. โœ… ํ™ˆ ํ™”๋ฉด์œผ๋กœ ์ด๋™ - ---- - -## โŒ ์‹คํŒจ ์‹œ ํ™•์ธ ์‚ฌํ•ญ - -### "Error while launching" ์˜ค๋ฅ˜ - -โ†’ Supabase Google Provider๊ฐ€ ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ Client ID/Secret์ด ์ž˜๋ชป๋จ - -### "redirect_uri_mismatch" ์˜ค๋ฅ˜ - -โ†’ Google Cloud Console์˜ Authorized redirect URIs์— Supabase ์ฝœ๋ฐฑ URL ์ถ”๊ฐ€ ํ•„์š” - -### "access_denied" ์˜ค๋ฅ˜ - -โ†’ OAuth ๋™์˜ ํ™”๋ฉด์ด Testing ์ƒํƒœ์ด๊ณ  ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž๋กœ ์ถ”๊ฐ€๋˜์ง€ ์•Š์Œ - ---- - -## ๐Ÿ“ž ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด - -`GOOGLE_LOGIN_FIX_GUIDE.md` ํŒŒ์ผ์—์„œ ๋” ์ž์„ธํ•œ ์„ค๋ช…์„ ํ™•์ธํ•˜์„ธ์š”. diff --git a/SNAKE_CASE_FIX.md b/SNAKE_CASE_FIX.md deleted file mode 100644 index be12b54..0000000 --- a/SNAKE_CASE_FIX.md +++ /dev/null @@ -1,211 +0,0 @@ -# ๐Ÿ”ง Snake Case ํ•„๋“œ ๋งคํ•‘ ๋ฌธ์ œ ํ•ด๊ฒฐ - -## โŒ ๋ฐœ์ƒํ–ˆ๋˜ ๋ฌธ์ œ - -``` -[UserProfileService] ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: type 'Null' is not a subtype of type 'String' in type cast -[UserProfileService] _TypeError (type 'Null' is not a subtype of type 'String' in type cast) -[UserProfileService] #0 _$UserProfileFromJson (package:stride_note/models/user_profile.g.dart:24:47) -``` - -**์Šคํƒ ํŠธ๋ ˆ์ด์Šค**: `user_profile.g.dart:24` โ†’ `createdAt: DateTime.parse(json['createdAt'] as String)` - ---- - -## ๐Ÿ” ๊ทผ๋ณธ ์›์ธ - -### ํ•„๋“œ ๋„ค์ด๋ฐ ๋ถˆ์ผ์น˜ - -| ์œ„์น˜ | ๋„ค์ด๋ฐ ๊ทœ์น™ | ์˜ˆ์‹œ | -| --------------- | ---------------- | ---------------------------- | -| **Dart ๋ชจ๋ธ** | camelCase | `createdAt`, `displayName` | -| **PostgreSQL** | snake_case | `created_at`, `display_name` | -| **JSON ์ง๋ ฌํ™”** | camelCase (๊ธฐ๋ณธ) | `createdAt` (โŒ ํ‹€๋ฆผ) | - -### ๋ฌธ์ œ ๋ฐœ์ƒ ํ๋ฆ„ - -``` -1. Supabase์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ - โ†’ { "created_at": "2025-01-01", "display_name": "User" } - -2. UserProfile.fromJson() ํ˜ธ์ถœ - โ†’ json['createdAt']๋ฅผ ์ฐพ์Œ - โ†’ null ๋ฐ˜ํ™˜ (์‹ค์ œ ํ‚ค๋Š” 'created_at') - -3. DateTime.parse(null as String) - โ†’ โŒ type 'Null' is not a subtype of type 'String' -``` - ---- - -## โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### 1. **JsonSerializable์— fieldRename ์ถ”๊ฐ€** - -**ํŒŒ์ผ**: `lib/models/user_profile.dart` - -**Before**: - -```dart -@JsonSerializable() -class UserProfile { - final DateTime createdAt; - final DateTime updatedAt; - final String? displayName; - final String? avatarUrl; - // ... -} -``` - -**After**: - -```dart -@JsonSerializable(fieldRename: FieldRename.snake) // โœ… snake_case ์ž๋™ ๋ณ€ํ™˜ -class UserProfile { - final DateTime createdAt; // โ†’ created_at - final DateTime updatedAt; // โ†’ updated_at - final String? displayName; // โ†’ display_name - final String? avatarUrl; // โ†’ avatar_url - // ... -} -``` - ---- - -### 2. **JSON ์ง๋ ฌํ™” ์ฝ”๋“œ ์žฌ์ƒ์„ฑ** - -```bash -flutter pub run build_runner build --delete-conflicting-outputs -``` - -**์ƒ์„ฑ๋œ ์ฝ”๋“œ** (`user_profile.g.dart`): - -**Before**: - -```dart -UserProfile _$UserProfileFromJson(Map json) => UserProfile( - createdAt: DateTime.parse(json['createdAt'] as String), // โŒ ํ‹€๋ฆผ - updatedAt: DateTime.parse(json['updatedAt'] as String), // โŒ ํ‹€๋ฆผ - displayName: json['displayName'] as String?, // โŒ ํ‹€๋ฆผ - avatarUrl: json['avatarUrl'] as String?, // โŒ ํ‹€๋ฆผ -); -``` - -**After**: - -```dart -UserProfile _$UserProfileFromJson(Map json) => UserProfile( - createdAt: DateTime.parse(json['created_at'] as String), // โœ… ๋งž์Œ - updatedAt: DateTime.parse(json['updated_at'] as String), // โœ… ๋งž์Œ - displayName: json['display_name'] as String?, // โœ… ๋งž์Œ - avatarUrl: json['avatar_url'] as String?, // โœ… ๋งž์Œ -); -``` - ---- - -### 3. **์ถ”๊ฐ€ Null ์•ˆ์ „ ๊ฒ€์ฆ** - -**ํŒŒ์ผ**: `lib/services/user_profile_service.dart` - -```dart -// null ์•ˆ์ „ ๊ฒ€์ฆ: ํ•„์ˆ˜ ํ•„๋“œ ํ™•์ธ -if (response['id'] == null || - response['email'] == null || - response['created_at'] == null || // โœ… ์ถ”๊ฐ€ - response['updated_at'] == null) { // โœ… ์ถ”๊ฐ€ - developer.log( - 'โš ๏ธ ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ ๋ถˆ์™„์ „: id=${response['id']}, email=${response['email']}, ' - 'created_at=${response['created_at']}, updated_at=${response['updated_at']}', - name: 'UserProfileService', - ); - return null; -} -``` - ---- - -## ๐Ÿ“Š Before vs After - -### Before (๋ฌธ์ œ ๋ฐœ์ƒ) - -```json -// Supabase ์‘๋‹ต -{ - "id": "xxx", - "email": "user@example.com", - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z", - "display_name": "User", - "avatar_url": "https://..." -} - -// Dart๊ฐ€ ์ฐพ๋Š” ํ‚ค -json['createdAt'] โ†’ null โŒ -json['updatedAt'] โ†’ null โŒ -json['displayName'] โ†’ null โŒ -json['avatarUrl'] โ†’ null โŒ - -// ๊ฒฐ๊ณผ -DateTime.parse(null as String) โ†’ โŒ ํƒ€์ž… ์˜ค๋ฅ˜! -``` - -### After (ํ•ด๊ฒฐ) - -```json -// Supabase ์‘๋‹ต (๋™์ผ) -{ - "id": "xxx", - "email": "user@example.com", - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z", - "display_name": "User", - "avatar_url": "https://..." -} - -// Dart๊ฐ€ ์ฐพ๋Š” ํ‚ค (์ˆ˜์ •๋จ) -json['created_at'] โ†’ "2025-01-01T00:00:00Z" โœ… -json['updated_at'] โ†’ "2025-01-01T00:00:00Z" โœ… -json['display_name'] โ†’ "User" โœ… -json['avatar_url'] โ†’ "https://..." โœ… - -// ๊ฒฐ๊ณผ -DateTime.parse("2025-01-01T00:00:00Z") โ†’ โœ… ์„ฑ๊ณต! -``` - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -```bash -โœ… flutter analyze: No issues found! -โœ… flutter test: 9/9 UserProfile tests passed -โœ… ๋ชจ๋“  ์ฝ”๋“œ ์ปดํŒŒ์ผ ์„ฑ๊ณต -``` - ---- - -## ๐Ÿ“ FieldRename ์˜ต์…˜ - -`@JsonSerializable`์˜ `fieldRename` ์˜ต์…˜: - -| ์˜ต์…˜ | ์„ค๋ช… | ์˜ˆ์‹œ | -| -------------------- | -------------------- | -------------------------- | -| `FieldRename.none` | ๋ณ€ํ™˜ ์—†์Œ (๊ธฐ๋ณธ๊ฐ’) | `createdAt` โ†’ `createdAt` | -| `FieldRename.snake` | snake_case๋กœ ๋ณ€ํ™˜ โœ… | `createdAt` โ†’ `created_at` | -| `FieldRename.kebab` | kebab-case๋กœ ๋ณ€ํ™˜ | `createdAt` โ†’ `created-at` | -| `FieldRename.pascal` | PascalCase๋กœ ๋ณ€ํ™˜ | `createdAt` โ†’ `CreatedAt` | - -**PostgreSQL/Supabase ์‚ฌ์šฉ ์‹œ**: **`FieldRename.snake` ํ•„์ˆ˜!** - ---- - -## ๐Ÿ”„ ๋‹ค๋ฅธ ๋ชจ๋ธ์—๋„ ์ ์šฉ - -### running_session.dart - -์ด ๋ชจ๋ธ๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! - - - -/Users/nhn/Desktop/DEV/flutter-workspace/runner_app/lib/models/running_session.dart diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index cac3810..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,173 +0,0 @@ -# Google ๋กœ๊ทธ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ์š”์•ฝ - -## ๐Ÿ“Œ ๋ฌธ์ œ ์š”์•ฝ - -๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์‹œ ๋‹ค์Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: - -``` -PlatformException(Error, Error while launching https://YOUR-PROJECT-ID.supabase.co/auth/v1/authorize?provider=google...) -``` - -## โœ… ์ˆ˜์ •๋œ ์‚ฌํ•ญ - -### 1. URL Scheme ํ†ต์ผ - -- โŒ ์ด์ „: `com.example.runnerApp://` -- โœ… ์ˆ˜์ •: `com.example.runnerApp://` -- ๐Ÿ“ ์ˆ˜์ • ํŒŒ์ผ: - - `ios/Runner/Info.plist` - - `lib/services/google_auth_service.dart` - -### 2. Bundle ID ํ†ต์ผ - -- โŒ ์ด์ „: `runner_app` (iOS) -- โœ… ์ˆ˜์ •: `stride_note` (iOS) -- ๐Ÿ“ ์ˆ˜์ • ํŒŒ์ผ: - - `ios/Runner/Info.plist` - -### 3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„  - -- โœ… ๋” ๊ตฌ์ฒด์ ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ -- โœ… OAuth ์„ค์ • ๊ฒ€์ฆ ๋„๊ตฌ ์ถ”๊ฐ€ -- โœ… ์„ค์ • ํ™•์ธ์‚ฌํ•ญ ์ž๋™ ์•ˆ๋‚ด -- ๐Ÿ“ ์‹ ๊ทœ ํŒŒ์ผ: - - `lib/services/supabase_oauth_validator.dart` - -### 4. ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ - -- โœ… URL Scheme ์ผ๊ด€์„ฑ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ -- โœ… Bundle ID ํ˜•์‹ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ -- โœ… OAuth URL ๊ตฌ์„ฑ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ -- ๐Ÿ“ ์‹ ๊ทœ ํŒŒ์ผ: - - `test/unit/services/google_auth_config_test.dart` - - `test/unit/services/google_oauth_url_test.dart` - -### 5. ๋ฌธ์„œํ™” - -- โœ… ์ƒ์„ธํ•œ ์„ค์ • ๊ฐ€์ด๋“œ ์ž‘์„ฑ -- โœ… ๋น ๋ฅธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ž‘์„ฑ -- โœ… README ์—…๋ฐ์ดํŠธ -- ๐Ÿ“ ์‹ ๊ทœ ํŒŒ์ผ: - - `GOOGLE_LOGIN_FIX_GUIDE.md` - - `SETUP_CHECKLIST.md` - - `SUPABASE_OAUTH_SETUP.md` - -## ๐Ÿ”ง ํ•„์š”ํ•œ ์ถ”๊ฐ€ ์ž‘์—… - -### 1. Supabase ๋Œ€์‹œ๋ณด๋“œ ์„ค์ • (ํ•„์ˆ˜) - -**Authentication > URL Configuration**: - -``` -Site URL: http://localhost:3000 - -Redirect URLs: -- com.example.runnerApp:// -- com.example.runnerApp://login-callback -- https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback -``` - -**Authentication > Providers > Google**: - -- โœ… Enable Sign in with Google -- Client ID (for OAuth): (Google Cloud Console์—์„œ ์ƒ์„ฑ) -- Client Secret (for OAuth): (Google Cloud Console์—์„œ ์ƒ์„ฑ) - -### 2. Google Cloud Console ์„ค์ • (ํ•„์ˆ˜) - -**OAuth ๋™์˜ ํ™”๋ฉด**: - -- User Type: External -- App name: StrideNote -- Support email: (๋ณธ์ธ ์ด๋ฉ”์ผ) -- Developer contact email: (๋ณธ์ธ ์ด๋ฉ”์ผ) - -**OAuth 2.0 Client ID ์ƒ์„ฑ (Web)**: - -- Application type: Web application -- Name: StrideNote Web (Supabase) -- Authorized redirect URIs: - ``` - https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback - ``` - -**OAuth 2.0 Client ID ์ƒ์„ฑ (iOS - ์„ ํƒ์‚ฌํ•ญ)**: - -- Application type: iOS -- Name: StrideNote iOS -- Bundle ID: `com.example.runnerApp` - -## ๐Ÿ“Š ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - -### ์ „์ฒด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ - -```bash -flutter test test/unit/services/ -``` - -โœ… 35๊ฐœ ํ…Œ์ŠคํŠธ ๋ชจ๋‘ ํ†ต๊ณผ - -### ์ฃผ์š” ๊ฒ€์ฆ ํ•ญ๋ชฉ - -- โœ… URL Scheme ์ผ๊ด€์„ฑ -- โœ… Bundle ID ํ˜•์‹ -- โœ… OAuth URL ๊ตฌ์„ฑ -- โœ… Supabase ์„ค์ • -- โœ… Google OAuth ํด๋ผ์ด์–ธํŠธ ID ํ˜•์‹ - -## ๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„ - -1. **Supabase ๋Œ€์‹œ๋ณด๋“œ ์„ค์ •**: `SETUP_CHECKLIST.md` ์ฐธ์กฐ -2. **Google Cloud Console ์„ค์ •**: `SETUP_CHECKLIST.md` ์ฐธ์กฐ -3. **์•ฑ ํ…Œ์ŠคํŠธ**: Google ๋กœ๊ทธ์ธ ์‹œ๋„ -4. **์„ฑ๊ณต ํ™•์ธ**: ๋กœ๊ทธ์ธ ํ›„ ํ™ˆ ํ™”๋ฉด ์ด๋™ ํ™•์ธ - -## ๐Ÿ“ ์ฐธ๊ณ  ๋ฌธ์„œ - -- **๋น ๋ฅธ ์„ค์ •**: `SETUP_CHECKLIST.md` -- **์ƒ์„ธ ๊ฐ€์ด๋“œ**: `GOOGLE_LOGIN_FIX_GUIDE.md` -- **OAuth ์„ค์ •**: `SUPABASE_OAUTH_SETUP.md` -- **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •**: `DATABASE_SETUP.md` -- **ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •**: `ENV_SETUP.md` - -## ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ - -### ์—ฌ์ „ํžˆ "Error while launching" ์˜ค๋ฅ˜ ๋ฐœ์ƒ - -โ†’ Supabase Google Provider๊ฐ€ ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ Client ID/Secret์ด ์„ค์ •๋˜์ง€ ์•Š์Œ - -### "redirect_uri_mismatch" ์˜ค๋ฅ˜ - -โ†’ Google Cloud Console์˜ Authorized redirect URIs์— Supabase ์ฝœ๋ฐฑ URL ์ถ”๊ฐ€ ํ•„์š” - -### "access_denied" ์˜ค๋ฅ˜ - -โ†’ OAuth ๋™์˜ ํ™”๋ฉด์ด Testing ์ƒํƒœ์ด๊ณ  ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž๋กœ ์ถ”๊ฐ€๋˜์ง€ ์•Š์Œ - -## ๐Ÿ’ก ํŒ - -### ๊ฐœ๋ฐœ ์ค‘ - -- OAuth ๋™์˜ ํ™”๋ฉด์„ **Testing** ์ƒํƒœ๋กœ ์œ ์ง€ -- ๋ณธ์ธ ์ด๋ฉ”์ผ์„ ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž๋กœ ์ถ”๊ฐ€ - -### ๋ฐฐํฌ ์‹œ - -- OAuth ๋™์˜ ํ™”๋ฉด์„ **In Production**์œผ๋กœ ๋ณ€๊ฒฝ -- Supabase Site URL์„ ์‹ค์ œ ๋„๋ฉ”์ธ์œผ๋กœ ๋ณ€๊ฒฝ -- Google Cloud Console์˜ Authorized redirect URIs์— ๋ฐฐํฌ ๋„๋ฉ”์ธ ์ถ”๊ฐ€ - -## ๐Ÿ“ž ์ถ”๊ฐ€ ๋„์›€ - -์„ค์ • ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด: - -1. ๊ฐ ๋‹จ๊ณ„๋ฅผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋Œ€๋กœ ๋‹ค์‹œ ํ™•์ธ -2. Supabase ๋Œ€์‹œ๋ณด๋“œ์˜ Authentication ๋กœ๊ทธ ํ™•์ธ -3. Google Cloud Console์˜ Credentials ์„ค์ • ํ™•์ธ -4. ์•ฑ ๋กœ๊ทธ ์ฝ˜์†”์—์„œ ์ƒ์„ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ™•์ธ - ---- - -**์ž‘์—… ์™„๋ฃŒ ์ผ์ž**: 2025-10-11 -**์ž‘์—… ๋‚ด์šฉ**: Google ๋กœ๊ทธ์ธ ์„ค์ • ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๋ฌธ์„œํ™” -**ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ**: 35/35 ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… diff --git a/SUPABASE_OAUTH_SETUP.md b/SUPABASE_OAUTH_SETUP.md deleted file mode 100644 index 7f8946e..0000000 --- a/SUPABASE_OAUTH_SETUP.md +++ /dev/null @@ -1,122 +0,0 @@ -# Supabase OAuth ์„ค์ • ๊ฐ€์ด๋“œ - -## ๐Ÿ”ง ํ˜„์žฌ ๋ฌธ์ œ ์ƒํ™ฉ - -Google ๋กœ๊ทธ์ธ ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค: - -``` -PlatformException(Error, Error while launching https://YOUR-PROJECT-ID.supabase.co/auth/v1/authorize?provider=google&redirect_to=com.example.runnerApp%3A%2F%2F&flow_type=pkce&code_challenge=...) -``` - -## ๐Ÿ“‹ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• - -### 1. Supabase ๋Œ€์‹œ๋ณด๋“œ ์„ค์ • - -#### 1.1 Authentication > URL Configuration - -1. Supabase ๋Œ€์‹œ๋ณด๋“œ์— ๋กœ๊ทธ์ธ -2. ํ”„๋กœ์ ํŠธ ์„ ํƒ: `YOUR-PROJECT-ID` -3. **Authentication** > **URL Configuration** ์ด๋™ - -#### 1.2 Site URL ์„ค์ • - -``` -https://your-app-domain.com -``` - -๋˜๋Š” ๊ฐœ๋ฐœ์šฉ์œผ๋กœ: - -``` -http://localhost:3000 -``` - -#### 1.3 Redirect URLs ์„ค์ • - -๋‹ค์Œ URL๋“ค์„ ์ถ”๊ฐ€: - -``` -com.example.runnerApp:// -https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback -https://your-app-domain.com/auth/callback -``` - -### 2. Google OAuth Provider ์„ค์ • - -#### 2.1 Authentication > Providers - -1. **Authentication** > **Providers** ์ด๋™ -2. **Google** Provider ํ™œ์„ฑํ™” - -#### 2.2 Google OAuth ์„ค์ • - -- **Client ID**: Google Cloud Console์—์„œ ์ƒ์„ฑํ•œ OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ID -- **Client Secret**: Google Cloud Console์—์„œ ์ƒ์„ฑํ•œ OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ - -### 3. Google Cloud Console ์„ค์ • - -#### 3.1 OAuth 2.0 ํด๋ผ์ด์–ธํŠธ ID ์ƒ์„ฑ - -1. [Google Cloud Console](https://console.cloud.google.com/) ์ ‘์† -2. ํ”„๋กœ์ ํŠธ ์„ ํƒ ๋˜๋Š” ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ -3. **APIs & Services** > **Credentials** ์ด๋™ -4. **Create Credentials** > **OAuth 2.0 Client ID** ์„ ํƒ - -#### 3.2 ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์œ ํ˜• ์„ค์ • - -- **Application type**: `iOS` ๋˜๋Š” `Android` ์„ ํƒ -- **Bundle ID**: `com.example.runnerApp` - -#### 3.3 Authorized redirect URIs ์„ค์ • - -๋‹ค์Œ URI๋“ค์„ ์ถ”๊ฐ€: - -``` -https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback -com.example.runnerApp:// -``` - -### 4. ํ˜„์žฌ ์•ฑ ์„ค์ • ํ™•์ธ - -#### 4.1 Bundle ID - -- **Android**: `com.example.runnerApp` (build.gradle.kts) -- **iOS**: `com.example.runnerApp` (Info.plist) - -#### 4.2 URL Scheme - -- **Android**: `com.example.runnerApp` (AndroidManifest.xml) -- **iOS**: `com.example.runnerApp` (Info.plist) - -#### 4.3 Google OAuth Client ID - -- **iOS**: `com.googleusercontent.apps.YOUR-GOOGLE-CLIENT-ID` (Info.plist) - -## ๐Ÿ” ์„ค์ • ๊ฒ€์ฆ - -์•ฑ์„ ์‹คํ–‰ํ•˜๋ฉด ์ฝ˜์†”์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒ€์ฆ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค: - -``` -=== Supabase OAuth ์„ค์ • ๊ฒ€์ฆ ์‹œ์ž‘ === -โœ… Supabase ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ -โœ… Supabase URL: https://YOUR-PROJECT-ID.supabase.co -โœ… Anonymous Key: YOUR-SUPABASE-ANON-KEY -โœ… OAuth URL ๊ตฌ์„ฑ: https://YOUR-PROJECT-ID.supabase.co/auth/v1/authorize?provider=google&redirect_to=com.example.runnerApp%3A%2F%2F&flow_type=pkce -=== ์„ค์ • ํ™•์ธ ํ•„์š”์‚ฌํ•ญ === -``` - -## ๐Ÿšจ ๋ฌธ์ œ ํ•ด๊ฒฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -- [ ] Supabase Site URL์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Supabase Redirect URLs์— `com.example.runnerApp://`๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google OAuth Provider๊ฐ€ Supabase์—์„œ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google Cloud Console์—์„œ ์˜ฌ๋ฐ”๋ฅธ Bundle ID๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google Cloud Console์—์„œ Authorized redirect URIs๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? -- [ ] Google OAuth Client ID์™€ Secret์ด Supabase์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? - -## ๐Ÿ“ž ์ถ”๊ฐ€ ๋„์›€ - -์„ค์ • ํ›„์—๋„ ๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋ฉด: - -1. Supabase ๋Œ€์‹œ๋ณด๋“œ์˜ Authentication ๋กœ๊ทธ ํ™•์ธ -2. Google Cloud Console์˜ OAuth ๋™์˜ ํ™”๋ฉด ์„ค์ • ํ™•์ธ -3. ์•ฑ์˜ Bundle ID์™€ Google Cloud Console์˜ Bundle ID ์ผ์น˜ ํ™•์ธ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9263f2b..130f5d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -31,6 +31,11 @@ android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> --> + + + ( + builder: (context, authProvider, child) { + // Provider์˜ ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ UI ์—…๋ฐ์ดํŠธ + final user = authProvider.currentUser; + + return Scaffold( + body: user != null + ? HomeContent(user: user) + : LoginPrompt(), + ); + }, + ); + } +} +``` + +**๊ทœ์น™**: + +- โœ… Provider๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ƒํƒœ ์ ‘๊ทผ +- โœ… Service๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜์ง€ ์•Š์Œ +- โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํฌํ•จํ•˜์ง€ ์•Š์Œ + +--- + +#### Provider Layer + +**์ฑ…์ž„**: ์ƒํƒœ ๊ด€๋ฆฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์กฐ์œจ, ๋ฆฌ์Šค๋„ˆ ์•Œ๋ฆผ + +```dart +// ์˜ˆ์‹œ: AuthProvider +class AuthProvider extends ChangeNotifier { + User? _currentUser; + bool _isLoading = false; + + // Getter + User? get currentUser => _currentUser; + bool get isLoading => _isLoading; + + // ๋กœ๊ทธ์ธ (Service ํ˜ธ์ถœ) + Future signIn(String email, String password) async { + _isLoading = true; + notifyListeners(); + + try { + final user = await AuthService.signInWithEmail( + email: email, + password: password, + ); + _currentUser = user; + } catch (e) { + rethrow; + } finally { + _isLoading = false; + notifyListeners(); + } + } +} +``` + +**๊ทœ์น™**: + +- โœ… ChangeNotifier ์ƒ์† +- โœ… Service Layer ํ˜ธ์ถœ +- โœ… ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ notifyListeners() ํ˜ธ์ถœ +- โœ… ๋น„๊ณต๊ฐœ ๋ณ€์ˆ˜ + public getter + +--- + +#### Service Layer + +**์ฑ…์ž„**: API ํ†ต์‹ , ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™ + +```dart +// ์˜ˆ์‹œ: AuthService +class AuthService { + static final SupabaseClient _supabase = Supabase.instance.client; + + /// ์ด๋ฉ”์ผ ๋กœ๊ทธ์ธ + static Future signInWithEmail({ + required String email, + required String password, + }) async { + try { + final response = await _supabase.auth.signInWithPassword( + email: email, + password: password, + ); + return response.user; + } on AuthException catch (e) { + throw Exception('๋กœ๊ทธ์ธ ์‹คํŒจ: ${e.message}'); + } + } + + /// ๋กœ๊ทธ์•„์›ƒ + static Future signOut() async { + await _supabase.auth.signOut(); + } +} +``` + +**๊ทœ์น™**: + +- โœ… static ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ (์ƒํƒœ ์—†์Œ) +- โœ… ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋กœ ๊ตฌํ˜„ (๋ถ€์ž‘์šฉ ์ตœ์†Œํ™”) +- โœ… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํฌํ•จ +- โœ… Model ๊ฐ์ฒด ๋ฐ˜ํ™˜ + +--- + +#### Model Layer + +**์ฑ…์ž„**: ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ •์˜, JSON ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” + +```dart +// ์˜ˆ์‹œ: UserProfile +@JsonSerializable() +class UserProfile { + final String id; + final String email; + final String? displayName; + final String? avatarUrl; + final String fitnessLevel; + final DateTime createdAt; + final DateTime updatedAt; + + UserProfile({ + required this.id, + required this.email, + this.displayName, + this.avatarUrl, + required this.fitnessLevel, + required this.createdAt, + required this.updatedAt, + }); + + // JSON ์ง๋ ฌํ™” + factory UserProfile.fromJson(Map json) => + _$UserProfileFromJson(json); + + Map toJson() => _$UserProfileToJson(this); +} +``` + +**๊ทœ์น™**: + +- โœ… ๋ถˆ๋ณ€ ๊ฐ์ฒด (final ํ•„๋“œ) +- โœ… @JsonSerializable ์–ด๋…ธํ…Œ์ด์…˜ +- โœ… fromJson / toJson ๋ฉ”์„œ๋“œ +- โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํฌํ•จํ•˜์ง€ ์•Š์Œ + +--- + +## ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ + +### ์‚ฌ์šฉ์ž ์•ก์…˜ โ†’ UI ์—…๋ฐ์ดํŠธ ํ”Œ๋กœ์šฐ + +``` +1. ์‚ฌ์šฉ์ž ์•ก์…˜ (User Action) + ์˜ˆ: ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ +2. View Layer - ์ด๋ฒคํŠธ ์ˆ˜์‹  + ์˜ˆ: onPressed: () => authProvider.signIn(email, password) + โ†“ +3. Provider Layer - ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ์ž‘ + ์˜ˆ: _isLoading = true; notifyListeners(); + โ†“ +4. Service Layer - API ํ˜ธ์ถœ + ์˜ˆ: AuthService.signInWithEmail(...) + โ†“ +5. External Services - ์›๊ฒฉ ์š”์ฒญ + ์˜ˆ: Supabase API ํ˜ธ์ถœ + โ†“ +6. Service Layer - ์‘๋‹ต ์ˆ˜์‹  + ์˜ˆ: return response.user; + โ†“ +7. Model Layer - ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ + ์˜ˆ: User.fromJson(json) + โ†“ +8. Provider Layer - ์ƒํƒœ ์—…๋ฐ์ดํŠธ + ์˜ˆ: _currentUser = user; notifyListeners(); + โ†“ +9. View Layer - UI ์ž๋™ ์žฌ๋ Œ๋”๋ง + ์˜ˆ: Consumer๊ฐ€ rebuild ํŠธ๋ฆฌ๊ฑฐ + โ†“ +10. ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฒฐ๊ณผ ํ‘œ์‹œ + ์˜ˆ: ํ™ˆ ํ™”๋ฉด์œผ๋กœ ์ด๋™ +``` + +### ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ํ”Œ๋กœ์šฐ + +``` +1. Service Layer - ์ŠคํŠธ๋ฆผ ๊ตฌ๋… + ์˜ˆ: Geolocator.getPositionStream() + โ†“ +2. Service Layer - ๋ฐ์ดํ„ฐ ์ˆ˜์‹  + ์˜ˆ: Position ๊ฐ์ฒด ์ˆ˜์‹  + โ†“ +3. Service Layer - ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + ์˜ˆ: ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ, ๋ฒ„ํผ๋ง + โ†“ +4. Provider Layer - ์ƒํƒœ ์—…๋ฐ์ดํŠธ + ์˜ˆ: _totalDistance += distance; notifyListeners(); + โ†“ +5. View Layer - UI ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ + ์˜ˆ: ๋Ÿฌ๋‹ ํ†ต๊ณ„ ํ™”๋ฉด ๊ฐฑ์‹  +``` + +--- + +## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +### ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + +``` +lib/ +โ”œโ”€โ”€ config/ # ์•ฑ ์„ค์ • ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ +โ”‚ โ”œโ”€โ”€ app_config.dart # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ (.env) +โ”‚ โ””โ”€โ”€ supabase_config.dart # Supabase ์ดˆ๊ธฐํ™” +โ”‚ +โ”œโ”€โ”€ constants/ # ์•ฑ ์ „์—ญ ์ƒ์ˆ˜ +โ”‚ โ”œโ”€โ”€ app_colors.dart # ์ปฌ๋Ÿฌ ํŒ”๋ ˆํŠธ +โ”‚ โ””โ”€โ”€ app_theme.dart # Material ํ…Œ๋งˆ +โ”‚ +โ”œโ”€โ”€ models/ # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ +โ”‚ โ”œโ”€โ”€ user_profile.dart # ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ +โ”‚ โ”œโ”€โ”€ user_profile.g.dart # JSON ์ง๋ ฌํ™” (์ž๋™ ์ƒ์„ฑ) +โ”‚ โ”œโ”€โ”€ running_session.dart # ๋Ÿฌ๋‹ ์„ธ์…˜ +โ”‚ โ””โ”€โ”€ running_session.g.dart +โ”‚ +โ”œโ”€โ”€ services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง +โ”‚ โ”œโ”€โ”€ auth_service.dart # ์ธ์ฆ +โ”‚ โ”œโ”€โ”€ google_auth_service.dart # Google ๋กœ๊ทธ์ธ +โ”‚ โ”œโ”€โ”€ user_profile_service.dart # ํ”„๋กœํ•„ ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ location_service.dart # GPS ์ถ”์  +โ”‚ โ”œโ”€โ”€ health_service.dart # ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ +โ”‚ โ”œโ”€โ”€ database_service.dart # ๋กœ์ปฌ DB +โ”‚ โ””โ”€โ”€ supabase_oauth_validator.dart +โ”‚ +โ”œโ”€โ”€ providers/ # ์ƒํƒœ ๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ auth_provider.dart # ์ธ์ฆ ์ƒํƒœ +โ”‚ +โ”œโ”€โ”€ screens/ # UI ํ™”๋ฉด +โ”‚ โ”œโ”€โ”€ auth/ +โ”‚ โ”‚ โ”œโ”€โ”€ login_screen.dart +โ”‚ โ”‚ โ””โ”€โ”€ signup_screen.dart +โ”‚ โ”œโ”€โ”€ home_screen.dart +โ”‚ โ”œโ”€โ”€ running_screen.dart +โ”‚ โ”œโ”€โ”€ history_screen.dart +โ”‚ โ”œโ”€โ”€ profile_screen.dart +โ”‚ โ””โ”€โ”€ splash_screen.dart +โ”‚ +โ”œโ”€โ”€ widgets/ # ์žฌ์‚ฌ์šฉ ์œ„์ ฏ +โ”‚ โ”œโ”€โ”€ running_card.dart +โ”‚ โ”œโ”€โ”€ running_timer.dart +โ”‚ โ”œโ”€โ”€ running_stats.dart +โ”‚ โ”œโ”€โ”€ running_controls.dart +โ”‚ โ”œโ”€โ”€ running_map.dart +โ”‚ โ”œโ”€โ”€ stats_summary.dart +โ”‚ โ””โ”€โ”€ quick_actions.dart +โ”‚ +โ”œโ”€โ”€ types/ # ํƒ€์ž… ์ •์˜ +โ”‚ โ””โ”€โ”€ supabase_types.dart +โ”‚ +โ””โ”€โ”€ main.dart # ์•ฑ ์ง„์ž…์  + +test/ # ํ…Œ์ŠคํŠธ +โ”œโ”€โ”€ unit/ # ๋‹จ์œ„ ํ…Œ์ŠคํŠธ +โ”‚ โ”œโ”€โ”€ services/ +โ”‚ โ”œโ”€โ”€ models/ +โ”‚ โ””โ”€โ”€ providers/ +โ”œโ”€โ”€ widget/ # ์œ„์ ฏ ํ…Œ์ŠคํŠธ +โ””โ”€โ”€ integration/ # ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ +``` + +### ํŒŒ์ผ ๋ช…๋ช… ๊ทœ์น™ + +``` +ํŒŒ์ผ๋ช…: snake_case +โ”œโ”€ user_profile.dart โœ… +โ””โ”€ UserProfile.dart โŒ + +ํด๋ž˜์Šค๋ช…: PascalCase +โ”œโ”€ class UserProfile โœ… +โ””โ”€ class user_profile โŒ + +๋ณ€์ˆ˜/ํ•จ์ˆ˜: camelCase +โ”œโ”€ final userName โœ… +โ”œโ”€ void getUserProfile() โœ… +โ””โ”€ final user_name โŒ + +์ƒ์ˆ˜: lowerCamelCase (Dart ์Šคํƒ€์ผ) +โ”œโ”€ const defaultPadding = 16.0; โœ… +โ””โ”€ const DEFAULT_PADDING = 16.0; โŒ +``` + +--- + +## ๋””์ž์ธ ํŒจํ„ด + +### 1. Provider ํŒจํ„ด (์ƒํƒœ ๊ด€๋ฆฌ) + +```dart +// main.dart - Provider ๋“ฑ๋ก +MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => AuthProvider()), + Provider(create: (_) => LocationService()), + Provider(create: (_) => DatabaseService()), + ], + child: MaterialApp(...), +) + +// ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ +// ๋ฐฉ๋ฒ• 1: Consumer (์ „์ฒด ์œ„์ ฏ ๋ฆฌ๋นŒ๋“œ) +Consumer( + builder: (context, authProvider, child) { + return Text(authProvider.currentUser?.email ?? ''); + }, +) + +// ๋ฐฉ๋ฒ• 2: Selector (ํŠน์ • ์†์„ฑ๋งŒ ๊ตฌ๋…) +Selector( + selector: (_, provider) => provider.currentUser?.email, + builder: (_, email, __) => Text(email ?? ''), +) + +// ๋ฐฉ๋ฒ• 3: Provider.of (๋ฆฌ๋นŒ๋“œ ์—†์ด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ) +final authProvider = Provider.of( + context, + listen: false, +); +authProvider.signIn(email, password); +``` + +**์žฅ์ **: + +- โœ… ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์  +- โœ… Flutter ๊ณต์‹ ์ถ”์ฒœ +- โœ… ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ ์Œ +- โœ… ํ…Œ์ŠคํŠธ ์šฉ์ด + +--- + +### 2. Singleton ํŒจํ„ด + +```dart +// ์˜ˆ์‹œ: LocationService +class LocationService { + // Private ์ƒ์„ฑ์ž + LocationService._internal(); + + // Static ์ธ์Šคํ„ด์Šค + static final LocationService _instance = LocationService._internal(); + + // Factory ์ƒ์„ฑ์ž + factory LocationService() { + return _instance; + } + + // ... ๋ฉ”์„œ๋“œ +} + +// ์‚ฌ์šฉ +final service1 = LocationService(); +final service2 = LocationService(); +print(service1 == service2); // true (๋™์ผํ•œ ์ธ์Šคํ„ด์Šค) +``` + +**์žฅ์ **: + +- โœ… ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ +- โœ… ๋ฆฌ์†Œ์Šค ๊ณต์œ  (GPS, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) +- โœ… ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์  + +--- + +### 3. Factory ํŒจํ„ด + +```dart +// ์˜ˆ์‹œ: RunningSession +class RunningSession { + final RunningType type; + + // Factory ์ƒ์„ฑ์ž + factory RunningSession.fromJson(Map json) { + return RunningSession( + type: RunningType.values.firstWhere( + (e) => e.name == json['type'], + ), + // ... + ); + } + + // Named constructor + RunningSession.free({ + required this.startTime, + required this.endTime, + }) : type = RunningType.free; + + RunningSession.interval({ + required this.startTime, + required this.endTime, + }) : type = RunningType.interval; +} +``` + +--- + +### 4. Stream ํŒจํ„ด (๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ) + +```dart +// ์˜ˆ์‹œ: LocationService +class LocationService { + final StreamController _positionController = + StreamController.broadcast(); + + // Stream ๋…ธ์ถœ + Stream get positionStream => _positionController.stream; + + // ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + void _onPositionReceived(Position position) { + _positionController.add(position); + } + + // ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + void dispose() { + _positionController.close(); + } +} + +// ์‚ฌ์šฉ +locationService.positionStream.listen((position) { + print('์œ„์น˜: ${position.latitude}, ${position.longitude}'); +}); +``` + +--- + +## ์ƒํƒœ ๊ด€๋ฆฌ + +### Provider ํŒจํ„ด ์ƒ์„ธ + +#### 1. ChangeNotifier ๊ตฌํ˜„ + +```dart +class AuthProvider extends ChangeNotifier { + // Private ์ƒํƒœ + User? _currentUser; + bool _isLoading = false; + String? _errorMessage; + + // Public getter + User? get currentUser => _currentUser; + bool get isLoading => _isLoading; + String? get errorMessage => _errorMessage; + bool get isAuthenticated => _currentUser != null; + + // ์ดˆ๊ธฐํ™” + Future initialize() async { + _currentUser = Supabase.instance.client.auth.currentUser; + notifyListeners(); + } + + // ๋กœ๊ทธ์ธ + Future signIn(String email, String password) async { + try { + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + final user = await AuthService.signInWithEmail( + email: email, + password: password, + ); + + _currentUser = user; + } catch (e) { + _errorMessage = e.toString(); + rethrow; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // ๋กœ๊ทธ์•„์›ƒ + Future signOut() async { + await AuthService.signOut(); + _currentUser = null; + notifyListeners(); + } +} +``` + +#### 2. Consumer vs Selector + +```dart +// Consumer: ์ „์ฒด Provider๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ฆฌ๋นŒ๋“œ +Consumer( + builder: (context, authProvider, child) { + // authProvider์˜ ์–ด๋–ค ์†์„ฑ์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ๋ฆฌ๋นŒ๋“œ + return Text(authProvider.currentUser?.email ?? '๋กœ๊ทธ์ธ ํ•„์š”'); + }, +) + +// Selector: ํŠน์ • ์†์„ฑ๋งŒ ๊ตฌ๋… +Selector( + selector: (_, provider) => provider.currentUser?.email, + builder: (_, email, __) { + // email์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ๋ฆฌ๋นŒ๋“œ + return Text(email ?? '๋กœ๊ทธ์ธ ํ•„์š”'); + }, +) +``` + +**์„ฑ๋Šฅ ๋น„๊ต**: + +- Consumer: ๊ฐ„๋‹จํ•˜์ง€๋งŒ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋นŒ๋“œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ +- Selector: ๋ณต์žกํ•˜์ง€๋งŒ ์ตœ์ ํ™”๋œ ๋ฆฌ๋นŒ๋“œ + +--- + +## ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ + +### Supabase (PostgreSQL) ์Šคํ‚ค๋งˆ + +#### 1. user_profiles ํ…Œ์ด๋ธ” + +```sql +CREATE TABLE public.user_profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + email TEXT NOT NULL UNIQUE, + display_name TEXT, + avatar_url TEXT, + fitness_level TEXT DEFAULT 'beginner', + birth_date DATE, + gender TEXT, + height_cm INTEGER, + weight_kg NUMERIC(5, 2), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Index +CREATE INDEX idx_user_profiles_email ON public.user_profiles(email); + +-- RLS (Row Level Security) +ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users can view own profile" +ON public.user_profiles FOR SELECT +USING (auth.uid() = id); + +CREATE POLICY "Users can update own profile" +ON public.user_profiles FOR UPDATE +USING (auth.uid() = id); +``` + +#### 2. running_sessions ํ…Œ์ด๋ธ” + +```sql +CREATE TABLE public.running_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE, + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + total_distance NUMERIC(10, 2) NOT NULL, -- meters + total_duration INTEGER NOT NULL, -- seconds + average_pace NUMERIC(5, 2), -- min/km + max_speed NUMERIC(5, 2), -- km/h + average_heart_rate INTEGER, + max_heart_rate INTEGER, + calories_burned INTEGER, + elevation_gain NUMERIC(8, 2), -- meters + elevation_loss NUMERIC(8, 2), -- meters + type TEXT DEFAULT 'free', -- free, interval, goal + gps_points JSONB, -- GPS ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Index +CREATE INDEX idx_running_sessions_user_id ON public.running_sessions(user_id); +CREATE INDEX idx_running_sessions_start_time ON public.running_sessions(start_time DESC); + +-- RLS +ALTER TABLE public.running_sessions ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users can view own sessions" +ON public.running_sessions FOR SELECT +USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own sessions" +ON public.running_sessions FOR INSERT +WITH CHECK (auth.uid() = user_id); +``` + +#### 3. Trigger: ์ž๋™ ํ”„๋กœํ•„ ์ƒ์„ฑ + +```sql +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.user_profiles (id, email, display_name, avatar_url, created_at, updated_at) + VALUES ( + NEW.id, + NEW.email, + COALESCE( + NEW.raw_user_meta_data->>'display_name', + NEW.raw_user_meta_data->>'full_name', + SPLIT_PART(NEW.email, '@', 1) + ), + NEW.raw_user_meta_data->>'avatar_url', + NOW(), + NOW() + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); +``` + +### SQLite (๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) + +```dart +// lib/services/database_service.dart +class DatabaseService { + static Database? _database; + + Future get database async { + if (_database != null) return _database!; + _database = await _initDatabase(); + return _database!; + } + + Future _initDatabase() async { + final path = await getDatabasesPath(); + final dbPath = join(path, 'stride_note.db'); + + return await openDatabase( + dbPath, + version: 1, + onCreate: _onCreate, + ); + } + + Future _onCreate(Database db, int version) async { + // running_sessions ํ…Œ์ด๋ธ” + await db.execute(''' + CREATE TABLE running_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + start_time INTEGER NOT NULL, + end_time INTEGER NOT NULL, + total_distance REAL NOT NULL, + total_duration INTEGER NOT NULL, + gps_points TEXT, + synced INTEGER DEFAULT 0 + ) + '''); + + // Index + await db.execute(''' + CREATE INDEX idx_sessions_synced + ON running_sessions(synced) + '''); + } +} +``` + +--- + +## ์˜์กด์„ฑ ์ฃผ์ž… + +### Provider๋ฅผ ํ†ตํ•œ ์˜์กด์„ฑ ์ฃผ์ž… + +```dart +// main.dart +MultiProvider( + providers: [ + // State Management + ChangeNotifierProvider(create: (_) => AuthProvider()), + + // Services (Singleton) + Provider(create: (_) => LocationService()), + Provider(create: (_) => DatabaseService()), + Provider(create: (_) => HealthService()), + ], + child: MaterialApp(...), +) + +// ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ +class HomeScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + // Provider์—์„œ Service ์ฃผ์ž…๋ฐ›๊ธฐ + final locationService = Provider.of( + context, + listen: false, + ); + + return Scaffold(...); + } +} +``` + +**์žฅ์ **: + +- โœ… ํ…Œ์ŠคํŠธ ์šฉ์ด (Mock ์ฃผ์ž… ๊ฐ€๋Šฅ) +- โœ… ์˜์กด์„ฑ ๋ช…ํ™•ํ™” +- โœ… ๋А์Šจํ•œ ๊ฒฐํ•ฉ + +--- + +## ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ „๋žต + +### 1. Try-Catch ํŒจํ„ด + +```dart +// Service Layer +class AuthService { + static Future signInWithEmail({ + required String email, + required String password, + }) async { + try { + final response = await _supabase.auth.signInWithPassword( + email: email, + password: password, + ); + return response.user; + } on AuthException catch (e) { + // Supabase ์ธ์ฆ ์˜ค๋ฅ˜ + throw Exception('์ธ์ฆ ์‹คํŒจ: ${e.message}'); + } on SocketException catch (e) { + // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ + throw Exception('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”'); + } catch (e) { + // ๊ธฐํƒ€ ์˜ค๋ฅ˜ + throw Exception('์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜: $e'); + } + } +} + +// Provider Layer +class AuthProvider extends ChangeNotifier { + Future signIn(String email, String password) async { + try { + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + final user = await AuthService.signInWithEmail( + email: email, + password: password, + ); + + _currentUser = user; + } catch (e) { + _errorMessage = e.toString(); + // UI์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก rethrow + rethrow; + } finally { + _isLoading = false; + notifyListeners(); + } + } +} + +// View Layer +class LoginScreen extends StatelessWidget { + Future _handleLogin() async { + try { + await authProvider.signIn(email, password); + Navigator.of(context).pushReplacement(...); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + } +} +``` + +### 2. Result ํŒจํ„ด (๊ณ„ํš ์ค‘) + +```dart +// ์„ฑ๊ณต/์‹คํŒจ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ +class Result { + final T? data; + final String? error; + final bool isSuccess; + + Result.success(this.data) : error = null, isSuccess = true; + Result.failure(this.error) : data = null, isSuccess = false; +} + +// ์‚ฌ์šฉ +Future> signIn(String email, String password) async { + try { + final user = await AuthService.signInWithEmail(...); + return Result.success(user); + } catch (e) { + return Result.failure(e.toString()); + } +} +``` + +--- + +## ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ + +```dart +// .env (Git์— ํฌํ•จ ์•ˆ ๋จ) +SUPABASE_URL=https://... +SUPABASE_ANON_KEY=... + +// app_config.dart +class AppConfig { + static String get supabaseUrl => + dotenv.env['SUPABASE_URL'] ?? ''; + + static String get supabaseAnonKey => + dotenv.env['SUPABASE_ANON_KEY'] ?? ''; +} +``` + +### 2. Row Level Security (RLS) + +```sql +-- ์‚ฌ์šฉ์ž๋Š” ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ +CREATE POLICY "Users can view own profile" +ON public.user_profiles FOR SELECT +USING (auth.uid() = id); +``` + +### 3. API Key ๋ณดํ˜ธ + +- โœ… .env ํŒŒ์ผ ์‚ฌ์šฉ +- โœ… .gitignore์— ์ถ”๊ฐ€ +- โœ… ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ + +--- + +## ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 1. ์œ„์ ฏ ์ตœ์ ํ™” + +```dart +// const ์ƒ์„ฑ์ž ์‚ฌ์šฉ +const Text('Hello'); // โœ… ์žฌ์ƒ์„ฑ ์•ˆ ๋จ +Text('Hello'); // โŒ ๋งค๋ฒˆ ์žฌ์ƒ์„ฑ + +// Selector๋กœ ๋ฆฌ๋นŒ๋“œ ์ตœ์†Œํ™” +Selector( + selector: (_, provider) => provider.currentUser?.email, + builder: (_, email, __) => Text(email ?? ''), +) +``` + +### 2. ์ด๋ฏธ์ง€ ์ตœ์ ํ™” + +```dart +// ์บ์‹œ๋œ ๋„คํŠธ์›Œํฌ ์ด๋ฏธ์ง€ +CachedNetworkImage( + imageUrl: avatarUrl, + placeholder: (_, __) => CircularProgressIndicator(), + errorWidget: (_, __, ___) => Icon(Icons.error), +) +``` + +### 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” + +```sql +-- Index ์ถ”๊ฐ€ +CREATE INDEX idx_sessions_user_start +ON running_sessions(user_id, start_time DESC); + +-- ์ฟผ๋ฆฌ ์ตœ์ ํ™” +SELECT * FROM running_sessions +WHERE user_id = $1 +ORDER BY start_time DESC +LIMIT 10; +``` + +--- + +## ์ฐธ๊ณ  ์ž๋ฃŒ + +- [Flutter ๊ณต์‹ ๋ฌธ์„œ](https://flutter.dev/docs) +- [Provider ํŒจํ‚ค์ง€](https://pub.dev/packages/provider) +- [Supabase ๋ฌธ์„œ](https://supabase.com/docs) +- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) + diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md diff --git a/docs/DEVELOPMENT_WORKFLOW.md b/docs/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 0000000..2f528c8 --- /dev/null +++ b/docs/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,321 @@ +# ๐Ÿ”„ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ + +์ด ๋ฌธ์„œ๋Š” StrideNote ํ”„๋กœ์ ํŠธ์˜ **๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค**์™€ **ํ˜‘์—… ๋ฐฉ์‹**์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐Ÿ“‹ ๋ชฉ์ฐจ + +- [Git ๋ธŒ๋žœ์น˜ ์ „๋žต](#-git-๋ธŒ๋žœ์น˜-์ „๋žต) +- [์ปค๋ฐ‹ ์ปจ๋ฒค์…˜](#-์ปค๋ฐ‹-์ปจ๋ฒค์…˜) +- [๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค](#-๊ฐœ๋ฐœ-ํ”„๋กœ์„ธ์Šค) +- [์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ฐ€์ด๋“œ](#-์ฝ”๋“œ-๋ฆฌ๋ทฐ-๊ฐ€์ด๋“œ) +- [CI/CD ํŒŒ์ดํ”„๋ผ์ธ](#-cicd-ํŒŒ์ดํ”„๋ผ์ธ) + +--- + +## ๐ŸŒฟ Git ๋ธŒ๋žœ์น˜ ์ „๋žต + +### Git Flow ์ „๋žต ์‚ฌ์šฉ + +``` +main (ํ”„๋กœ๋•์…˜) + โ””โ”€ develop (๊ฐœ๋ฐœ) + โ”œโ”€ feature/* (๊ธฐ๋Šฅ ๊ฐœ๋ฐœ) + โ”œโ”€ bugfix/* (๋ฒ„๊ทธ ์ˆ˜์ •) + โ”œโ”€ hotfix/* (๊ธด๊ธ‰ ์ˆ˜์ •) + โ””โ”€ release/* (๋ฆด๋ฆฌ์ฆˆ ์ค€๋น„) +``` + +### ๋ธŒ๋žœ์น˜ ๋„ค์ด๋ฐ ๊ทœ์น™ + +| ๋ธŒ๋žœ์น˜ ํƒ€์ž… | ํŒจํ„ด | ์˜ˆ์‹œ | +| :-----------: | :----------------: | :--------------------- | +| **๊ธฐ๋Šฅ ๊ฐœ๋ฐœ** | `feature/<๊ธฐ๋Šฅ๋ช…>` | `feature/google-login` | +| **๋ฒ„๊ทธ ์ˆ˜์ •** | `bugfix/<๋ฒ„๊ทธ๋ช…>` | `bugfix/gps-accuracy` | +| **๊ธด๊ธ‰ ์ˆ˜์ •** | `hotfix/<์ด์Šˆ๋ช…>` | `hotfix/login-crash` | +| **๋ฆด๋ฆฌ์ฆˆ** | `release/v<๋ฒ„์ „>` | `release/v1.0.0` | + +--- + +## ๐Ÿ“ ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜ + +### Conventional Commits ์‚ฌ์šฉ + +``` +(): + + + +