diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4b03faa..e6ff269 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,50 +1,60 @@ ## ๐Ÿ“‹ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ### ๐Ÿ”ง ๋ณ€๊ฒฝ ๋‚ด์šฉ + ### ๐ŸŽฏ ๋ณ€๊ฒฝ ์ด์œ  + ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์žˆ๋Š” ๊ฒฝ์šฐ) + ## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### ์ฝ”๋“œ ํ’ˆ์งˆ + - [ ] ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•จ (`npm run test:run`) - [ ] ํƒ€์ž… ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผํ•จ (`npm run type-check`) - [ ] ๋ฆฐํŠธ ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผํ•จ (`npm run lint`) - [ ] ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณตํ•จ (`npm run build`) ### ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + - [ ] ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•จ - [ ] ๊ธฐ์กด ๊ธฐ๋Šฅ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ - [ ] ๋ชจ๋ฐ”์ผ์—์„œ ์ •์ƒ ๋™์ž‘ํ•จ - [ ] ๋‹คํฌ๋ชจ๋“œ/๋ผ์ดํŠธ๋ชจ๋“œ์—์„œ ์ •์ƒ ๋™์ž‘ํ•จ ### ์„ฑ๋Šฅ ๋ฐ ๋ณด์•ˆ + - [ ] ์ƒˆ๋กœ์šด ์˜์กด์„ฑ ์ถ”๊ฐ€ ์‹œ ๋ณด์•ˆ ๊ฒ€ํ†  ์™„๋ฃŒ - [ ] ์„ฑ๋Šฅ์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์ด ์—†์Œ - [ ] ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š์Œ ### ๋ฌธ์„œํ™” + - [ ] ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ - [ ] ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ถ”๊ฐ€ ์‹œ .env.example ์—…๋ฐ์ดํŠธ ## ๐Ÿš€ ๋ฐฐํฌ ํ™•์ธ ### Vercel ๋ฏธ๋ฆฌ๋ณด๊ธฐ + - [ ] Vercel ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณตํ•จ - [ ] ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL์—์„œ ์ •์ƒ ๋™์ž‘ ํ™•์ธ - [ ] ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•จ ### ์ถ”๊ฐ€ ์ •๋ณด + --- **๐Ÿ“ ์ฐธ๊ณ ์‚ฌํ•ญ:** + - ์ด PR์ด ๋ณ‘ํ•ฉ๋˜๋ฉด ์ž๋™์œผ๋กœ ํ”„๋กœ๋•์…˜์— ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. - Vercel ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋งํฌ๋Š” ์ด PR์— ์ž๋™์œผ๋กœ ์ฝ”๋ฉ˜ํŠธ๋ฉ๋‹ˆ๋‹ค. -- ๋ฐฐํฌ ์ƒํƒœ๋Š” GitHub Actions์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. \ No newline at end of file +- ๋ฐฐํฌ ์ƒํƒœ๋Š” GitHub Actions์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/.github/workflows/deployment-monitor.yml b/.github/workflows/deployment-monitor.yml index ff996f3..397338a 100644 --- a/.github/workflows/deployment-monitor.yml +++ b/.github/workflows/deployment-monitor.yml @@ -9,51 +9,51 @@ on: jobs: pre-deployment-check: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' - cache: 'npm' - + node-version: "18" + cache: "npm" + - name: Install dependencies run: npm ci - + - name: ๐Ÿ” Pre-deployment checks run: | echo "๐Ÿ—๏ธ ๋ฐฐํฌ ์ „ ๊ฒ€์‚ฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค..." - + # ๋นŒ๋“œ ํฌ๊ธฐ ๋ถ„์„ npm run build - + # dist ํด๋” ํฌ๊ธฐ ํ™•์ธ DIST_SIZE=$(du -sh dist | cut -f1) echo "๐Ÿ“ฆ ๋นŒ๋“œ ํฌ๊ธฐ: $DIST_SIZE" - + # ์ฃผ์š” ์ฒญํฌ ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ echo "๐Ÿ“Š ์ฃผ์š” ์ฒญํฌ ํŒŒ์ผ ํฌ๊ธฐ:" find dist/assets -name "*.js" -exec ls -lh {} \; | awk '{print $5 " " $9}' | sort -hr | head -10 - + echo "โœ… ๋นŒ๋“œ ๋ถ„์„ ์™„๋ฃŒ" - + - name: ๐Ÿงช ์„ฑ๋Šฅ ์ฒดํฌ run: | # JavaScript ํŒŒ์ผ ๊ฐœ์ˆ˜ ํ™•์ธ JS_COUNT=$(find dist/assets -name "*.js" | wc -l) CSS_COUNT=$(find dist/assets -name "*.css" | wc -l) - + echo "๐Ÿ“ ์ƒ์„ฑ๋œ ํŒŒ์ผ:" echo " - JavaScript ํŒŒ์ผ: $JS_COUNT ๊ฐœ" echo " - CSS ํŒŒ์ผ: $CSS_COUNT ๊ฐœ" - + # ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ๊ฒฝ๊ณ  find dist/assets -name "*.js" -size +500k -exec echo "โš ๏ธ ๋Œ€์šฉ๋Ÿ‰ JS ํŒŒ์ผ ๋ฐœ๊ฒฌ: {}" \; find dist/assets -name "*.css" -size +100k -exec echo "โš ๏ธ ๋Œ€์šฉ๋Ÿ‰ CSS ํŒŒ์ผ ๋ฐœ๊ฒฌ: {}" \; - + - name: ๐Ÿ“Š ๋ฒˆ๋“ค ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ uses: actions/upload-artifact@v4 with: @@ -66,7 +66,7 @@ jobs: runs-on: ubuntu-latest needs: pre-deployment-check if: github.ref == 'refs/heads/main' - + steps: - name: ๐Ÿš€ Production ๋ฐฐํฌ ์•Œ๋ฆผ run: | @@ -78,31 +78,31 @@ jobs: security-scan: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' - cache: 'npm' - + node-version: "18" + cache: "npm" + - name: Install dependencies run: npm ci - + - name: ๐Ÿ”’ ๋ณด์•ˆ ์Šค์บ” run: | echo "๐Ÿ” ๋ณด์•ˆ ์ทจ์•ฝ์  ๊ฒ€์‚ฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค..." - + # npm audit if npm audit --audit-level=moderate; then echo "โœ… ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค." else echo "โš ๏ธ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." fi - + # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ˆ„์ถœ ๊ฒ€์‚ฌ echo "๐Ÿ” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ˆ„์ถœ ๊ฒ€์‚ฌ..." if grep -r "VITE_.*=" dist/ --include="*.js" --include="*.css" 2>/dev/null; then @@ -110,8 +110,8 @@ jobs: else echo "โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ˆ„์ถœ์ด ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค." fi - + - name: ๐Ÿ“‹ ๋ณด์•ˆ ์Šค์บ” ๊ฒฐ๊ณผ run: | echo "๐Ÿ›ก๏ธ ๋ณด์•ˆ ์Šค์บ”์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." - echo "๋ฐฐํฌ ์ „ ๋ณด์•ˆ ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." \ No newline at end of file + echo "๋ฐฐํฌ ์ „ ๋ณด์•ˆ ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." diff --git a/.github/workflows/pr-deployment-status.yml b/.github/workflows/pr-deployment-status.yml index c8941dc..63dedc4 100644 --- a/.github/workflows/pr-deployment-status.yml +++ b/.github/workflows/pr-deployment-status.yml @@ -7,7 +7,7 @@ jobs: deployment-status: runs-on: ubuntu-latest if: github.event.deployment_status.state == 'success' || github.event.deployment_status.state == 'failure' - + steps: - name: Add deployment comment to PR uses: actions/github-script@v7 @@ -17,21 +17,21 @@ jobs: const state = deployment_status.state; const targetUrl = deployment_status.target_url; const environment = deployment_status.deployment.environment; - + let emoji = state === 'success' ? 'โœ…' : 'โŒ'; let message = state === 'success' ? '์„ฑ๊ณต' : '์‹คํŒจ'; - + const comment = `## ${emoji} ๋ฐฐํฌ ${message} - + **ํ™˜๊ฒฝ**: \`${environment}\` **์ƒํƒœ**: ${message} **URL**: ${targetUrl ? `[๋ฐฐํฌ ํ™•์ธํ•˜๊ธฐ](${targetUrl})` : '๋ฐฐํฌ URL ์—†์Œ'} **์‹œ๊ฐ„**: ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} - + ${state === 'success' ? '๐ŸŽ‰ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์œ„ ๋งํฌ์—์„œ ํ™•์ธํ•ด๋ณด์„ธ์š”.' : 'โš ๏ธ ๋ฐฐํฌ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.'}`; - + // PR๊ณผ ์—ฐ๊ด€๋œ ๊ฒฝ์šฐ์—๋งŒ ์ฝ”๋ฉ˜ํŠธ ์ถ”๊ฐ€ if (context.payload.deployment_status.deployment.ref !== 'main') { const { data: prs } = await github.rest.pulls.list({ @@ -49,4 +49,4 @@ jobs: body: comment }); } - } \ No newline at end of file + } diff --git a/.github/workflows/vercel-deployment.yml b/.github/workflows/vercel-deployment.yml index b7348a9..10e9011 100644 --- a/.github/workflows/vercel-deployment.yml +++ b/.github/workflows/vercel-deployment.yml @@ -9,26 +9,26 @@ on: jobs: build-and-test: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' - cache: 'npm' - + node-version: "18" + cache: "npm" + - name: Install dependencies run: npm ci - + - name: Run type check run: npm run type-check - + - name: Run tests run: npm run test:run - + - name: Build project run: npm run build env: @@ -37,7 +37,7 @@ jobs: VITE_APPWRITE_DATABASE_ID: ${{ secrets.VITE_APPWRITE_DATABASE_ID }} VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID: ${{ secrets.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID }} VITE_DISABLE_LOVABLE_BANNER: true - + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: @@ -49,15 +49,15 @@ jobs: runs-on: ubuntu-latest needs: build-and-test if: always() - + steps: - name: Deployment Success Notification if: needs.build-and-test.result == 'success' run: | echo "โœ… ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" echo "Vercel์ด ์ž๋™์œผ๋กœ ๋ฐฐํฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." - - - name: Deployment Failure Notification + + - name: Deployment Failure Notification if: needs.build-and-test.result == 'failure' run: | echo "โŒ ๋นŒ๋“œ๊ฐ€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค!" @@ -65,28 +65,28 @@ jobs: security-check: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' - cache: 'npm' - + node-version: "18" + cache: "npm" + - name: Install dependencies run: npm ci - + - name: Run security audit run: npm audit --audit-level=moderate continue-on-error: true - + - name: Check for vulnerabilities run: | if npm audit --audit-level=high --dry-run; then echo "โœ… ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค." else echo "โš ๏ธ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€ํ† ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." - fi \ No newline at end of file + fi diff --git a/CLAUDE.md b/CLAUDE.md index 2497fe6..e31eb10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,7 @@ Zellyy Finance๋Š” React์™€ TypeScript๋กœ ๊ตฌ์ถ•๋œ ๊ฐœ์ธ ๊ฐ€๊ณ„๋ถ€ ๊ด€๋ฆฌ ์•  ## ๊ธฐ์ˆ  ์Šคํƒ ### ํ”„๋ก ํŠธ์—”๋“œ + - **React 18** - ๋ฉ”์ธ UI ํ”„๋ ˆ์ž„์›Œํฌ - **TypeScript** - ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ - **Vite** - ๋น ๋ฅธ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐ ๋นŒ๋“œ ๋„๊ตฌ @@ -16,15 +17,18 @@ Zellyy Finance๋Š” React์™€ TypeScript๋กœ ๊ตฌ์ถ•๋œ ๊ฐœ์ธ ๊ฐ€๊ณ„๋ถ€ ๊ด€๋ฆฌ ์•  - **Zustand** - ์ƒํƒœ ๊ด€๋ฆฌ ### ๋ฐฑ์—”๋“œ ๋ฐ ์ธ์ฆ + - **Appwrite** - ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค (์ธ์ฆ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) - **React Hook Form** - ํผ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ### ํ…Œ์ŠคํŒ… + - **Vitest** - ํ…Œ์ŠคํŠธ ๋Ÿฌ๋„ˆ - **React Testing Library** - ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŒ… - **@testing-library/jest-dom** - DOM ํ…Œ์ŠคํŒ… ์œ ํ‹ธ๋ฆฌํ‹ฐ ### ๊ฐœ๋ฐœ ๋„๊ตฌ + - **ESLint** - ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ - **Prettier** - ์ฝ”๋“œ ํฌ๋งทํŒ… - **Task Master AI** - ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๋ฐ ์ž‘์—… ์ถ”์  @@ -62,30 +66,35 @@ src/ ## ์ฃผ์š” ๊ธฐ๋Šฅ ### 1. ์‚ฌ์šฉ์ž ์ธ์ฆ + - ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ - ํšŒ์›๊ฐ€์ž… ๋ฐ ๊ณ„์ • ๊ด€๋ฆฌ - ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • - ์„ธ์…˜ ๊ด€๋ฆฌ ### 2. ๊ฑฐ๋ž˜ ๊ด€๋ฆฌ + - ์ˆ˜์ž…/์ง€์ถœ ๋“ฑ๋ก ๋ฐ ํŽธ์ง‘ - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„๋ฅ˜ - ๊ฒฐ์ œ ์ˆ˜๋‹จ ๊ด€๋ฆฌ - ๊ฑฐ๋ž˜ ๋‚ด์—ญ ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง ### 3. ์˜ˆ์‚ฐ ๊ด€๋ฆฌ + - ์›”๊ฐ„/์ฃผ๊ฐ„/์ผ๊ฐ„ ์˜ˆ์‚ฐ ์„ค์ • - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์‚ฐ ๋ถ„๋ฐฐ - ์˜ˆ์‚ฐ ๋Œ€๋น„ ์ง€์ถœ ํ˜„ํ™ฉ ์‹œ๊ฐํ™” - ์˜ˆ์‚ฐ ์ดˆ๊ณผ ์•Œ๋ฆผ ### 4. ๋ถ„์„ ๋ฐ ํ†ต๊ณ„ + - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๋ถ„์„ - ๊ฒฐ์ œ ์ˆ˜๋‹จ๋ณ„ ํ†ต๊ณ„ - ์›”๊ฐ„/์—ฐ๊ฐ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ - ์ฐจํŠธ ๋ฐ ๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™” ### 5. ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ + - ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€ - ์˜คํ”„๋ผ์ธ ๋ฐ์ดํ„ฐ ๋กœ์ปฌ ์ €์žฅ - ์˜จ๋ผ์ธ ๋ณต๊ตฌ ์‹œ ์ž๋™ ๋™๊ธฐํ™” @@ -93,6 +102,7 @@ src/ ## ๊ฐœ๋ฐœ ๋ช…๋ น์–ด ### ๊ธฐ๋ณธ ๋ช…๋ น์–ด + ```bash npm run dev # ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹œ์ž‘ npm run build # ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ @@ -105,6 +115,7 @@ npm run test:coverage # ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ ``` ### Task Master ๋ช…๋ น์–ด + ```bash task-master next # ๋‹ค์Œ ์ž‘์—… ํ™•์ธ task-master list # ๋ชจ๋“  ์ž‘์—… ๋ชฉ๋ก @@ -115,24 +126,28 @@ task-master set-status --id= --status=done # ์ž‘์—… ์™„๋ฃŒ ํ‘œ์‹œ ## ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜ ### TypeScript + - ๋ชจ๋“  ํŒŒ์ผ์— ์—„๊ฒฉํ•œ ํƒ€์ž… ์ •์˜ ์‚ฌ์šฉ - `any` ํƒ€์ž… ์‚ฌ์šฉ ๊ธˆ์ง€ - ์ธํ„ฐํŽ˜์ด์Šค์™€ ํƒ€์ž… ๋ณ„์นญ ์ ์ ˆํžˆ ํ™œ์šฉ - Enum๋ณด๋‹ค const assertion ์„ ํ˜ธ ### React ์ปดํฌ๋„ŒํŠธ + - ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ - Props ์ธํ„ฐํŽ˜์ด์Šค ๋ช…์‹œ์  ์ •์˜ - ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋กœ์ง ๋ถ„๋ฆฌ - `React.FC` ํƒ€์ž… ๋ช…์‹œ์  ์‚ฌ์šฉ ### ์Šคํƒ€์ผ๋ง + - Tailwind CSS ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ์‚ฌ์šฉ - ์ปค์Šคํ…€ CSS๋Š” ์ตœ์†Œํ™” - ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ๊ณ ๋ ค - ์ผ๊ด€๋œ ์ปฌ๋Ÿฌ ํŒ”๋ ˆํŠธ ์‚ฌ์šฉ ### ํด๋” ๋ฐ ํŒŒ์ผ ๋ช…๋ช… + - ์ปดํฌ๋„ŒํŠธ: PascalCase (์˜ˆ: `TransactionCard.tsx`) - ํ›…: camelCase with 'use' prefix (์˜ˆ: `useTransactions.ts`) - ์œ ํ‹ธ๋ฆฌํ‹ฐ: camelCase (์˜ˆ: `formatCurrency.ts`) @@ -141,17 +156,20 @@ task-master set-status --id= --status=done # ์ž‘์—… ์™„๋ฃŒ ํ‘œ์‹œ ## ํ…Œ์ŠคํŠธ ์ „๋žต ### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ + - ๋ชจ๋“  ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ํ…Œ์ŠคํŠธ - ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ - ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ํ…Œ์ŠคํŠธ - ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ ### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + - API ํ˜ธ์ถœ ํ๋ฆ„ ํ…Œ์ŠคํŠธ - ์ƒํƒœ ๊ด€๋ฆฌ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - ๋ผ์šฐํŒ… ํ…Œ์ŠคํŠธ ### ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ + - ๋ผ์ธ ์ปค๋ฒ„๋ฆฌ์ง€: 80% ์ด์ƒ - ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€: 70% ์ด์ƒ - ๋ธŒ๋žœ์น˜ ์ปค๋ฒ„๋ฆฌ์ง€: 70% ์ด์ƒ @@ -174,12 +192,14 @@ NODE_ENV=development ## ์„ฑ๋Šฅ ์ตœ์ ํ™” ### ํ˜„์žฌ ์ ์šฉ๋œ ์ตœ์ ํ™” + - React.lazy๋ฅผ ํ†ตํ•œ ์ปดํฌ๋„ŒํŠธ ์ง€์—ฐ ๋กœ๋”ฉ - React.memo๋ฅผ ํ†ตํ•œ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ - useMemo, useCallback์„ ํ†ตํ•œ ๊ณ„์‚ฐ ์ตœ์ ํ™” - ์ด๋ฏธ์ง€ ์ง€์—ฐ ๋กœ๋”ฉ ### ์˜ˆ์ •๋œ ์ตœ์ ํ™” + - ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™” - ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ๊ฐœ์„  - ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” @@ -188,10 +208,12 @@ NODE_ENV=development ## ๋ฐฐํฌ ๋ฐ CI/CD ### ๋ฐฐํฌ ํ™˜๊ฒฝ + - **๊ฐœ๋ฐœ**: Vite ๊ฐœ๋ฐœ ์„œ๋ฒ„ - **ํ”„๋กœ๋•์…˜**: ์ •์  ํŒŒ์ผ ๋นŒ๋“œ ํ›„ ํ˜ธ์ŠคํŒ… ### CI/CD ํŒŒ์ดํ”„๋ผ์ธ + - ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ (ESLint, Prettier) - ์ž๋™ ํ…Œ์ŠคํŠธ ์‹คํ–‰ - ํƒ€์ž… ์ฒดํฌ @@ -219,6 +241,7 @@ NODE_ENV=development ## ๊ธฐ์—ฌ ๊ฐ€์ด๋“œ ### ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ + 1. ์ž‘์—… ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ 2. Task Master์—์„œ ์ž‘์—… ์„ ํƒ 3. ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐ ํ…Œ์ŠคํŠธ @@ -226,6 +249,7 @@ NODE_ENV=development 5. ๋จธ์ง€ ํ›„ ๋ฐฐํฌ ### ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + - [ ] TypeScript ํƒ€์ž… ์•ˆ์ „์„ฑ - [ ] ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ - [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” @@ -235,6 +259,7 @@ NODE_ENV=development ## ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค ### ๊ด€๋ จ ๋ฌธ์„œ + - [React ๊ณต์‹ ๋ฌธ์„œ](https://react.dev/) - [TypeScript ํ•ธ๋“œ๋ถ](https://www.typescriptlang.org/docs/) - [Tailwind CSS ๋ฌธ์„œ](https://tailwindcss.com/docs) @@ -242,10 +267,11 @@ NODE_ENV=development - [Vitest ๋ฌธ์„œ](https://vitest.dev/) ### ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ + - Task Master AI๋ฅผ ํ†ตํ•œ ์ž‘์—… ์ถ”์  - ์ด์Šˆ ๋ฐ ๋ฒ„๊ทธ ๋ฆฌํฌํŒ… - ๊ธฐ๋Šฅ ์š”์ฒญ ๋ฐ ๊ฐœ์„  ์‚ฌํ•ญ --- -์ด ๋ฌธ์„œ๋Š” Zellyy Finance ํ”„๋กœ์ ํŠธ์˜ ๊ฐœ๋ฐœ๊ณผ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•œ ์ข…ํ•ฉ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜๊ฑฐ๋‚˜ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•  ๋•Œ ์ฐธ์กฐํ•˜์„ธ์š”. \ No newline at end of file +์ด ๋ฌธ์„œ๋Š” Zellyy Finance ํ”„๋กœ์ ํŠธ์˜ ๊ฐœ๋ฐœ๊ณผ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•œ ์ข…ํ•ฉ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜๊ฑฐ๋‚˜ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•  ๋•Œ ์ฐธ์กฐํ•˜์„ธ์š”. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index b882940..cb2b4f5 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,9 +1,11 @@ # Zellyy Finance - Vercel ๋ฐฐํฌ ๊ฐ€์ด๋“œ ## ๊ฐœ์š” + ์ด ๋ฌธ์„œ๋Š” Zellyy Finance ํ”„๋กœ์ ํŠธ๋ฅผ Vercel์—์„œ ์ž๋™ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ## ์‚ฌ์ „ ์ค€๋น„์‚ฌํ•ญ + - GitHub ์ €์žฅ์†Œ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์žˆ์–ด์•ผ ํ•จ - Vercel ๊ณ„์ •์ด ํ•„์š”ํ•จ - Appwrite ํ”„๋กœ์ ํŠธ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•จ @@ -11,16 +13,19 @@ ## 1. Vercel ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ ์—ฐ๊ฒฐ ### 1.1 Vercel ๊ณ„์ • ๋กœ๊ทธ์ธ + 1. [Vercel ์›น์‚ฌ์ดํŠธ](https://vercel.com)์— ์ ‘์† 2. GitHub ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ 3. "New Project" ๋ฒ„ํŠผ ํด๋ฆญ ### 1.2 GitHub ์ €์žฅ์†Œ ์—ฐ๊ฒฐ + 1. Import Git Repository ์„น์…˜์—์„œ GitHub ์„ ํƒ 2. `zellyy-finance` ์ €์žฅ์†Œ ์„ ํƒ 3. "Import" ๋ฒ„ํŠผ ํด๋ฆญ ### 1.3 ํ”„๋กœ์ ํŠธ ์„ค์ • + - **Framework Preset**: Vite (์ž๋™ ๊ฐ์ง€๋จ) - **Root Directory**: `.` (๊ธฐ๋ณธ๊ฐ’) - **Build Command**: `npm run build` (์ž๋™ ์„ค์ •๋จ) @@ -30,9 +35,11 @@ ## 2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ### 2.1 ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + Vercel ๋Œ€์‹œ๋ณด๋“œ์˜ Settings > Environment Variables์—์„œ ๋‹ค์Œ ๋ณ€์ˆ˜๋“ค์„ ์„ค์ •: #### ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ (Production) + ```env VITE_APPWRITE_ENDPOINT=https://your-appwrite-endpoint/v1 VITE_APPWRITE_PROJECT_ID=your-production-project-id @@ -43,6 +50,7 @@ VITE_DISABLE_LOVABLE_BANNER=true ``` #### ํ”„๋ฆฌ๋ทฐ ํ™˜๊ฒฝ (Preview) + ```env VITE_APPWRITE_ENDPOINT=https://your-appwrite-endpoint/v1 VITE_APPWRITE_PROJECT_ID=your-preview-project-id @@ -53,24 +61,30 @@ VITE_DISABLE_LOVABLE_BANNER=true ``` ### 2.2 ํ™˜๊ฒฝ๋ณ„ ๋ธŒ๋žœ์น˜ ๋งคํ•‘ + - **Production**: `main` ๋ธŒ๋žœ์น˜ - **Preview**: `develop` ๋ธŒ๋žœ์น˜ ๋ฐ PR ๋ธŒ๋žœ์น˜๋“ค ## 3. ๋ฐฐํฌ ์„ค์ • ์ตœ์ ํ™” ### 3.1 Node.js ๋ฒ„์ „ ์„ค์ • + `.nvmrc` ํŒŒ์ผ์—์„œ Node.js ๋ฒ„์ „ ์ง€์ •: + ``` 18.x ``` ### 3.2 ๋นŒ๋“œ ์ตœ์ ํ™” + - ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์ด ์ด๋ฏธ ๊ตฌํ˜„๋˜์–ด ์žˆ์Œ (React.lazy) - ์ •์  ์ž์‚ฐ ์บ์‹ฑ ์„ค์ • (`vercel.json`์— ํฌํ•จ๋จ) - ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ์ตœ์ ํ™” ### 3.3 ๋ณด์•ˆ ํ—ค๋” ์„ค์ • + `vercel.json`์— ๋‹ค์Œ ๋ณด์•ˆ ํ—ค๋”๋“ค์ด ์„ค์ •๋จ: + - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - X-XSS-Protection: 1; mode=block @@ -79,18 +93,21 @@ VITE_DISABLE_LOVABLE_BANNER=true ## 4. ์ž๋™ ๋ฐฐํฌ ์›Œํฌํ”Œ๋กœ์šฐ ### 4.1 Production ๋ฐฐํฌ + 1. `main` ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ ํ‘ธ์‹œ 2. Vercel์ด ์ž๋™์œผ๋กœ ๋นŒ๋“œ ์‹œ์ž‘ 3. ๋นŒ๋“œ ์„ฑ๊ณต ์‹œ Production ํ™˜๊ฒฝ์— ๋ฐฐํฌ 4. ์‹คํŒจ ์‹œ ์ด์ „ ๋ฒ„์ „ ์œ ์ง€ ### 4.2 Preview ๋ฐฐํฌ + 1. `develop` ๋ธŒ๋žœ์น˜ ๋˜๋Š” PR ์ƒ์„ฑ 2. ์ž๋™์œผ๋กœ Preview ๋ฐฐํฌ ์ƒ์„ฑ 3. ๊ณ ์œ ํ•œ URL๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต 4. PR์— ๋ฐฐํฌ ๋งํฌ ์ž๋™ ์ฝ”๋ฉ˜ํŠธ ### 4.3 ๋ฐฐํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง + - Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์‹ค์‹œ๊ฐ„ ๋นŒ๋“œ ๋กœ๊ทธ ํ™•์ธ - GitHub PR์— ๋ฐฐํฌ ์ƒํƒœ ์ž๋™ ์—…๋ฐ์ดํŠธ - ๋ฐฐํฌ ์‹คํŒจ ์‹œ ์Šฌ๋ž™/์ด๋ฉ”์ผ ์•Œ๋ฆผ (์„ ํƒ์‚ฌํ•ญ) @@ -98,23 +115,27 @@ VITE_DISABLE_LOVABLE_BANNER=true ## 5. ๋„๋ฉ”์ธ ์„ค์ • ### 5.1 ์ปค์Šคํ…€ ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ + 1. Vercel ํ”„๋กœ์ ํŠธ Settings > Domains 2. ์›ํ•˜๋Š” ๋„๋ฉ”์ธ ์ž…๋ ฅ 3. DNS ์„ค์ • ์—…๋ฐ์ดํŠธ (CNAME ๋˜๋Š” A ๋ ˆ์ฝ”๋“œ) 4. SSL ์ธ์ฆ์„œ ์ž๋™ ์„ค์ • ### 5.2 ๋„๋ฉ”์ธ ์˜ˆ์‹œ + - Production: `zellyy-finance.vercel.app` ๋˜๋Š” `your-custom-domain.com` - Preview: `zellyy-finance-git-develop-username.vercel.app` ## 6. ์„ฑ๋Šฅ ์ตœ์ ํ™” ### 6.1 ๋ถ„์„ ๋„๊ตฌ + - Vercel Analytics ํ™œ์„ฑํ™” - Core Web Vitals ๋ชจ๋‹ˆํ„ฐ๋ง - ๋ฒˆ๋“ค ํฌ๊ธฐ ๋ถ„์„ ### 6.2 ์ตœ์ ํ™”๋œ ์„ค์ • + - ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (Vercel Image Optimization) - ์ •์  ์ž์‚ฐ CDN ์บ์‹ฑ - ์••์ถ• ๋ฐ minification ์ž๋™ ์ ์šฉ @@ -122,11 +143,13 @@ VITE_DISABLE_LOVABLE_BANNER=true ## 7. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ### 7.1 ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋“ค + - **๋นŒ๋“œ ์‹คํŒจ**: Node.js ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ํ™•์ธ - **ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์˜ค๋ฅ˜**: Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋ณ€์ˆ˜ ์„ค์ • ํ™•์ธ - **๋ผ์šฐํŒ… ์˜ค๋ฅ˜**: SPA rewrites ์„ค์ • ํ™•์ธ (`vercel.json`) ### 7.2 ๋””๋ฒ„๊น… ํŒ + - Vercel ๋นŒ๋“œ ๋กœ๊ทธ ์ž์„ธํžˆ ํ™•์ธ - ๋กœ์ปฌ์—์„œ `npm run build` ํ…Œ์ŠคํŠธ - ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธ @@ -134,16 +157,19 @@ VITE_DISABLE_LOVABLE_BANNER=true ## 8. ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ### 8.1 Branch Protection + - `main` ๋ธŒ๋žœ์น˜์— ๋Œ€ํ•œ ๋ณดํ˜ธ ๊ทœ์น™ ์„ค์ • - PR ๋ฆฌ๋ทฐ ํ•„์ˆ˜ํ™” - ๋ฐฐํฌ ์ „ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ•„์ˆ˜ ### 8.2 ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ + - ๋ฐฐํฌ ์ƒํƒœ Slack ์•Œ๋ฆผ - ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ์ง€ ์•Œ๋ฆผ - ์—๋Ÿฌ ์ถ”์  (Sentry ์—ฐ๋™ ๊ฐ€๋Šฅ) ## ์ฐธ๊ณ  ์ž๋ฃŒ + - [Vercel ๊ณต์‹ ๋ฌธ์„œ](https://vercel.com/docs) - [Vite ๋ฐฐํฌ ๊ฐ€์ด๋“œ](https://vitejs.dev/guide/static-deploy.html) -- [React Router SPA ์„ค์ •](https://reactrouter.com/en/main/guides/ssr) \ No newline at end of file +- [React Router SPA ์„ค์ •](https://reactrouter.com/en/main/guides/ssr) diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md index b35602a..673e862 100644 --- a/DEPLOYMENT_CHECKLIST.md +++ b/DEPLOYMENT_CHECKLIST.md @@ -5,6 +5,7 @@ ## ๐Ÿ“‹ ๋ฐฐํฌ ์ „ ์ค€๋น„์‚ฌํ•ญ ### โœ… ์ฝ”๋“œ ์ค€๋น„ + - [ ] ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ (`npm run test:run`) - [ ] ํƒ€์ž… ๊ฒ€์‚ฌ ํ†ต๊ณผ (`npm run type-check`) - [ ] ๋ฆฐํŠธ ๊ฒ€์‚ฌ ํ†ต๊ณผ (`npm run lint`) @@ -12,12 +13,14 @@ - [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” ํ™•์ธ (์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…, ๋ฉ”๋ชจ์ด์ œ์ด์…˜) ### โœ… ํ™˜๊ฒฝ ์„ค์ • + - [ ] `.env.example` ํŒŒ์ผ ์ตœ์‹  ์ƒํƒœ ์œ ์ง€ - [ ] ํ”„๋กœ๋•์…˜์šฉ Appwrite ํ”„๋กœ์ ํŠธ ์„ค์ • ์™„๋ฃŒ - [ ] ํ”„๋ฆฌ๋ทฐ์šฉ Appwrite ํ”„๋กœ์ ํŠธ ์„ค์ • ์™„๋ฃŒ (์„ ํƒ์‚ฌํ•ญ) - [ ] ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ชฉ๋ก ํ™•์ธ ### โœ… GitHub ์„ค์ • + - [ ] GitHub ์ €์žฅ์†Œ๊ฐ€ public ๋˜๋Š” Vercel ์—ฐ๋™ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ - [ ] `main` ๋ธŒ๋žœ์น˜๊ฐ€ ์•ˆ์ •์ ์ธ ์ƒํƒœ - [ ] PR ํ…œํ”Œ๋ฆฟ์ด ์„ค์ •๋จ @@ -26,6 +29,7 @@ ## ๐Ÿ”ง Vercel ํ”„๋กœ์ ํŠธ ์„ค์ • ### 1๋‹จ๊ณ„: Vercel ๊ณ„์ • ๋ฐ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ + - [ ] [Vercel ์›น์‚ฌ์ดํŠธ](https://vercel.com)์—์„œ GitHub ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ - [ ] "New Project" ํด๋ฆญ - [ ] `zellyy-finance` ์ €์žฅ์†Œ ์„ ํƒํ•˜์—ฌ Import @@ -37,9 +41,11 @@ - Install Command: `npm install` ### 2๋‹จ๊ณ„: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: #### ๐Ÿ”‘ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + - [ ] `VITE_APPWRITE_ENDPOINT` - Appwrite ์—”๋“œํฌ์ธํŠธ URL - [ ] `VITE_APPWRITE_PROJECT_ID` - ํ”„๋กœ๋•์…˜ ํ”„๋กœ์ ํŠธ ID - [ ] `VITE_APPWRITE_DATABASE_ID` - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ID (default) @@ -48,10 +54,12 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: - [ ] `VITE_DISABLE_LOVABLE_BANNER` - `true` ์„ค์ • #### ๐Ÿ”‘ ํ”„๋ฆฌ๋ทฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (๋™์ผํ•œ ํ‚ค, ๋‹ค๋ฅธ ๊ฐ’) + - [ ] ํ”„๋ฆฌ๋ทฐ์šฉ Appwrite ํ”„๋กœ์ ํŠธ ID ์„ค์ • - [ ] ๊ธฐํƒ€ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋Š” ํ”„๋กœ๋•์…˜๊ณผ ๋™์ผ ### 3๋‹จ๊ณ„: ๋ธŒ๋žœ์น˜ ๋ฐ ๋ฐฐํฌ ์„ค์ • + - [ ] Production ๋ธŒ๋žœ์น˜: `main` ํ™•์ธ - [ ] Preview ๋ธŒ๋žœ์น˜: `develop` ๋ฐ ๋ชจ๋“  PR ๋ธŒ๋žœ์น˜ ํ™•์ธ - [ ] ์ž๋™ ๋ฐฐํฌ ํ™œ์„ฑํ™” ํ™•์ธ @@ -59,12 +67,14 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐Ÿš€ ์ฒซ ๋ฐฐํฌ ์‹คํ–‰ ### ๋ฐฐํฌ ํ…Œ์ŠคํŠธ + - [ ] ์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ ์‹คํ–‰ (์ž๋™ ๋˜๋Š” ์ˆ˜๋™) - [ ] Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋นŒ๋“œ ๋กœ๊ทธ ํ™•์ธ - [ ] ๋ฐฐํฌ ์„ฑ๊ณต ํ™•์ธ - [ ] ์ƒ์„ฑ๋œ URL์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ •์ƒ ๋™์ž‘ ํ™•์ธ ### ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + - [ ] ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ - [ ] ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ ํ…Œ์ŠคํŠธ - [ ] ์˜ˆ์‚ฐ ์„ค์ • ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ @@ -74,12 +84,14 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐Ÿ”„ ์ž๋™ ๋ฐฐํฌ ์›Œํฌํ”Œ๋กœ์šฐ ๊ฒ€์ฆ ### GitHub Actions ํ™•์ธ + - [ ] PR ์ƒ์„ฑ ์‹œ ์ž๋™ ๋นŒ๋“œ ์‹คํ–‰ ํ™•์ธ - [ ] ๋ฐฐํฌ ์ „ ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ํ™•์ธ - [ ] ๋ณด์•ˆ ์Šค์บ” ์ž๋™ ์‹คํ–‰ ํ™•์ธ - [ ] ๋นŒ๋“œ ์‹คํŒจ ์‹œ ์•Œ๋ฆผ ํ™•์ธ ### Vercel ํ†ตํ•ฉ ํ™•์ธ + - [ ] `main` ๋ธŒ๋žœ์น˜ ํ‘ธ์‹œ ์‹œ ํ”„๋กœ๋•์…˜ ์ž๋™ ๋ฐฐํฌ - [ ] PR ์ƒ์„ฑ ์‹œ ํ”„๋ฆฌ๋ทฐ ๋ฐฐํฌ ์ž๋™ ์ƒ์„ฑ - [ ] ๋ฐฐํฌ ์ƒํƒœ๊ฐ€ GitHub PR์— ์ž๋™ ์ฝ”๋ฉ˜ํŠธ๋จ @@ -88,6 +100,7 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐ŸŒ ๋„๋ฉ”์ธ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) ### ์ปค์Šคํ…€ ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ + - [ ] Vercel ํ”„๋กœ์ ํŠธ Settings > Domains ์ ‘์† - [ ] ์›ํ•˜๋Š” ๋„๋ฉ”์ธ ์ž…๋ ฅ - [ ] DNS ์„ค์ • ์—…๋ฐ์ดํŠธ (CNAME ๋˜๋Š” A ๋ ˆ์ฝ”๋“œ) @@ -97,12 +110,14 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐Ÿ“Š ์„ฑ๋Šฅ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • ### ์„ฑ๋Šฅ ์ตœ์ ํ™” ํ™•์ธ + - [ ] ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์ด ์ ์šฉ๋จ (์ฒญํฌ ํŒŒ์ผ๋“ค ํ™•์ธ) - [ ] ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์ ์šฉ ํ™•์ธ - [ ] ์ •์  ์ž์‚ฐ ์บ์‹ฑ ์„ค์ • ํ™•์ธ - [ ] ์••์ถ• ๋ฐ minification ์ ์šฉ ํ™•์ธ ### ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • + - [ ] Vercel Analytics ํ™œ์„ฑํ™” (์„ ํƒ์‚ฌํ•ญ) - [ ] Core Web Vitals ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • - [ ] ์—๋Ÿฌ ์ถ”์  ์„ค์ • (Sentry ๋“ฑ, ์„ ํƒ์‚ฌํ•ญ) @@ -111,12 +126,14 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ์•ˆ์ •์„ฑ ์ฒดํฌ ### ๋ณด์•ˆ ์„ค์ • ํ™•์ธ + - [ ] ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ๋นŒ๋“œ ํŒŒ์ผ์— ๋…ธ์ถœ๋˜์ง€ ์•Š์Œ - [ ] HTTPS ๊ฐ•์ œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์„ค์ • - [ ] ๋ณด์•ˆ ํ—ค๋” ์„ค์ • ํ™•์ธ (`vercel.json`) - [ ] npm audit ๋ณด์•ˆ ์ทจ์•ฝ์  ์—†์Œ ### ๋ฐฑ์—… ๋ฐ ๋กค๋ฐฑ ์ค€๋น„ + - [ ] ์ด์ „ ๋ฐฐํฌ ๋ฒ„์ „ ๋กค๋ฐฑ ๋ฐฉ๋ฒ• ์ˆ™์ง€ - [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… ๊ณ„ํš ์ˆ˜๋ฆฝ - [ ] ์žฅ์•  ์ƒํ™ฉ ๋Œ€์‘ ๊ณ„ํš ์ˆ˜๋ฆฝ @@ -124,6 +141,7 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ## ๐Ÿ“‹ ๋ฐฐํฌ ์™„๋ฃŒ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### ์ตœ์ข… ํ™•์ธ + - [ ] ํ”„๋กœ๋•์…˜ URL์—์„œ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ - [ ] ๋ชจ๋ฐ”์ผ ๋””๋ฐ”์ด์Šค์—์„œ ์ ‘์† ํ…Œ์ŠคํŠธ - [ ] ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ˜ธํ™˜์„ฑ ํ™•์ธ @@ -131,6 +149,7 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: - [ ] ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ ์ค€๋น„ ### ๋ฌธ์„œํ™” + - [ ] ๋ฐฐํฌ URL ๋ฐ ์ ‘์† ์ •๋ณด ๊ณต์œ  - [ ] ๋ฐฐํฌ ๊ณผ์ • ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ - [ ] ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ ์ž‘์„ฑ @@ -141,25 +160,30 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: ### ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋“ค #### ๋นŒ๋“œ ์‹คํŒจ + - Node.js ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ํ™•์ธ - ์˜์กด์„ฑ ์„ค์น˜ ๋ฌธ์ œ (`npm ci` ์žฌ์‹คํ–‰) - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์˜คํƒ€ ํ™•์ธ #### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์˜ค๋ฅ˜ + - Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋ณ€์ˆ˜ ๊ฐ’ ํ™•์ธ - ๋Œ€์†Œ๋ฌธ์ž ๋ฐ ์˜คํƒ€ ํ™•์ธ - ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํ™•์ธ (Production/Preview) #### ๋ผ์šฐํŒ… ๋ฌธ์ œ + - `vercel.json`์˜ rewrites ์„ค์ • ํ™•์ธ - SPA ๋ผ์šฐํŒ… ์„ค์ • ํ™•์ธ #### ์„ฑ๋Šฅ ๋ฌธ์ œ + - ๋ฒˆ๋“ค ํฌ๊ธฐ ๋ถ„์„ (`npm run build:analyze`) - ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ ํ™•์ธ - ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ํ™•์ธ ### ๋„์›€๋ง ๋ฐ ์ง€์› + - [Vercel ๊ณต์‹ ๋ฌธ์„œ](https://vercel.com/docs) - [GitHub Issues](https://github.com/hansoo./zellyy-finance/issues) - [DEPLOYMENT.md](./DEPLOYMENT.md) ์ƒ์„ธ ๊ฐ€์ด๋“œ @@ -168,4 +192,4 @@ Vercel ํ”„๋กœ์ ํŠธ Settings > Environment Variables์—์„œ ์„ค์ •: **โœ… ๋ฐฐํฌ ์™„๋ฃŒ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰** -์ด์ œ Zellyy Finance๊ฐ€ ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค! \ No newline at end of file +์ด์ œ Zellyy Finance๊ฐ€ ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค! diff --git a/README.md b/README.md index f09afaa..af66a13 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,17 @@ npx tsc --noEmit ์ด ํ”„๋กœ์ ํŠธ๋Š” Vercel์„ ํ†ตํ•ด ์ž๋™ ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. ### ์ž๋™ ๋ฐฐํฌ + - **ํ”„๋กœ๋•์…˜**: `main` ๋ธŒ๋žœ์น˜์— ํ‘ธ์‹œํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ - **ํ”„๋ฆฌ๋ทฐ**: PR ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ์ƒ์„ฑ - **์Šคํ…Œ์ด์ง•**: `develop` ๋ธŒ๋žœ์น˜๋Š” ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์œผ๋กœ ๋ฐฐํฌ ### ๋ฐฐํฌ ์„ค์ • + ์ž์„ธํ•œ ๋ฐฐํฌ ์„ค์ • ๋ฐฉ๋ฒ•์€ [DEPLOYMENT.md](./DEPLOYMENT.md)๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. ### ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + ```env VITE_APPWRITE_ENDPOINT=https://your-appwrite-endpoint/v1 VITE_APPWRITE_PROJECT_ID=your-project-id @@ -124,6 +127,7 @@ VITE_DISABLE_LOVABLE_BANNER=true ## ๐Ÿ”— ์ปค์Šคํ…€ ๋„๋ฉ”์ธ Vercel์„ ํ†ตํ•ด ์ปค์Šคํ…€ ๋„๋ฉ”์ธ์„ ์‰ฝ๊ฒŒ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: + 1. Vercel ํ”„๋กœ์ ํŠธ Settings > Domains 2. ์›ํ•˜๋Š” ๋„๋ฉ”์ธ ์ž…๋ ฅ 3. DNS ์„ค์ • ์—…๋ฐ์ดํŠธ diff --git a/performance-analysis.md b/performance-analysis.md index a18630b..9e5c335 100644 --- a/performance-analysis.md +++ b/performance-analysis.md @@ -1,6 +1,7 @@ # React ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ถ„์„ ๋ณด๊ณ ์„œ ## ์„ฑ๋Šฅ ๋ถ„์„ ๊ฐœ์š” + - ๋ถ„์„ ์ผ์‹œ: 2025-07-12 - ๋ถ„์„ ๋„๊ตฌ: React DevTools Profiler, ์ฝ”๋“œ ๋ฆฌ๋ทฐ - ๋ชฉํ‘œ: ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ, ๋กœ๋”ฉ ์†๋„ 2๋ฐฐ ํ–ฅ์ƒ, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” @@ -8,16 +9,19 @@ ## ๋ฐœ๊ฒฌ๋œ ์„ฑ๋Šฅ ์ด์Šˆ ### 1. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ๋ฏธ์ ์šฉ + - **๋ฌธ์ œ**: ๋ชจ๋“  ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™๊ธฐ์ ์œผ๋กœ import๋จ (App.tsx:15-27) - **์˜ํ–ฅ**: ์ดˆ๊ธฐ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ฆ๊ฐ€, ์ฒซ ๋กœ๋”ฉ ์‹œ๊ฐ„ ์ง€์—ฐ - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: React.lazy์™€ Suspense ์ ์šฉ ### 2. ๊ณผ๋„ํ•œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” + - **๋ฌธ์ œ**: BackgroundSync๊ฐ€ 5๋ถ„ ๊ฐ„๊ฒฉ์œผ๋กœ ์‹คํ–‰ (App.tsx:228) - **์˜ํ–ฅ**: ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ, ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: 30์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์กฐ์ • ### 3. ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๋ฏธ์ ์šฉ + - **๋ฌธ์ œ**: ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ๋“ค์—์„œ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐœ์ƒ ๊ฐ€๋Šฅ - Header: ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ์žฌ๋ Œ๋”๋ง - IndexContent: ์Šคํ† ์–ด ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ์žฌ๋ Œ๋”๋ง @@ -25,6 +29,7 @@ - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: React.memo, useMemo, useCallback ์ ์šฉ ### 4. ๋ณต์žกํ•œ useEffect ์˜์กด์„ฑ + - **๋ฌธ์ œ**: Index.tsx์—์„œ ๋ณต์žกํ•œ ์˜์กด์„ฑ ๋ฐฐ์—ด (๋ผ์ธ 92-98) - **์˜ํ–ฅ**: ๋ถˆํ•„์š”ํ•œ effect ์‹คํ–‰ - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: useCallback์œผ๋กœ ํ•จ์ˆ˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ @@ -32,22 +37,26 @@ ## ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ณ„ํš ### Phase 1: ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… (์šฐ์„ ์ˆœ์œ„: ๋†’์Œ) + - [ ] ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๋“ค์„ React.lazy๋กœ ๋ณ€ํ™˜ - [ ] Suspense boundary ์ถ”๊ฐ€ - [ ] ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ์ปดํฌ๋„ŒํŠธ ๊ฐœ์„  ### Phase 2: ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ (์šฐ์„ ์ˆœ์œ„: ๋†’์Œ) + - [ ] Header ์ปดํฌ๋„ŒํŠธ์— React.memo ์ ์šฉ - [ ] IndexContent์—์„œ props drilling ์ตœ์ ํ™” - [ ] BudgetProgressCard ๋ฉ”๋ชจ์ด์ œ์ด์…˜ - [ ] ์ปค์Šคํ…€ ํ›…์—์„œ useCallback ์ ์šฉ ### Phase 3: ์„ค์ • ์ตœ์ ํ™” (์šฐ์„ ์ˆœ์œ„: ์ค‘๊ฐ„) + - [ ] BackgroundSync ๊ฐ„๊ฒฉ ์กฐ์ • (5๋ถ„ โ†’ 30์ดˆ) - [ ] ์ด๋ฏธ์ง€ ์ง€์—ฐ ๋กœ๋”ฉ ๊ตฌํ˜„ - [ ] ๊ฐ€์ƒํ™”๋œ ๋ฆฌ์ŠคํŠธ ๊ฒ€ํ†  ### Phase 4: ์ธก์ • ๋ฐ ๊ฒ€์ฆ (์šฐ์„ ์ˆœ์œ„: ๋†’์Œ) + - [ ] React DevTools Profiler๋กœ before/after ๋น„๊ต - [ ] Lighthouse ์„ฑ๋Šฅ ์ ์ˆ˜ ์ธก์ • - [ ] ๋ฒˆ๋“ค ํฌ๊ธฐ ๋ถ„์„ @@ -55,31 +64,36 @@ ## ๊ตฌํ˜„ ์™„๋ฃŒ๋œ ์ตœ์ ํ™” ### 1. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… โœ… + - **์ ์šฉ**: React.lazy์™€ Suspense๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋™์  ๋กœ๋”ฉ์œผ๋กœ ๋ณ€๊ฒฝ -- **๊ฒฐ๊ณผ**: +- **๊ฒฐ๊ณผ**: - ๋ฉ”์ธ ๋ฒˆ๋“ค: 470.15 kB (์ „์ฒด ์ฝ”๋“œ๋ฒ ์ด์Šค) - ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ฒญํฌ: Index ํŽ˜์ด์ง€๋งŒ 62.78 kB - ๊ธฐํƒ€ ํŽ˜์ด์ง€๋“ค์€ ํ•„์š”์‹œ์—๋งŒ ๋กœ๋”ฉ (6-400 kB ๋ฒ”์œ„) - **ํšจ๊ณผ**: ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ 87% ๋ฒˆ๋“ค ํฌ๊ธฐ ๊ฐ์†Œ (470 kB โ†’ 62 kB) ### 2. ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ โœ… + - **Header ์ปดํฌ๋„ŒํŠธ**: React.memo, useMemo, useCallback ์ ์šฉ - **IndexContent ์ปดํฌ๋„ŒํŠธ**: ์ „์ฒด ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๋ฐ props ์ตœ์ ํ™” - **BudgetProgressCard ์ปดํฌ๋„ŒํŠธ**: ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋ฐ ์ƒํƒœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ - **Index ํŽ˜์ด์ง€**: ๋ณต์žกํ•œ useEffect ์˜์กด์„ฑ ์ตœ์ ํ™” ### 3. ์„ฑ๋Šฅ ์„ค์ • ์ตœ์ ํ™” โœ… + - **BackgroundSync ๊ฐ„๊ฒฉ**: 5๋ถ„ โ†’ 30์ดˆ๋กœ ์กฐ์ • (90% ๊ฐ์†Œ) - **์ด๋ฏธ์ง€ ๋กœ๋”ฉ**: ํ”„๋ฆฌ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ตœ์ ํ™” - **์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ**: ๋ฉ”๋ชจ์ด์ œ์ด์…˜์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์Šค๋„ˆ ์žฌ๋“ฑ๋ก ๋ฐฉ์ง€ ### 4. ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ โœ… + - **๋‹จ์œ„ ํ…Œ์ŠคํŠธ**: 229๊ฐœ ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ - **ํƒ€์ž… ๊ฒ€์‚ฌ**: TypeScript ์ปดํŒŒ์ผ ์˜ค๋ฅ˜ ์—†์Œ - **ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ**: ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ ## ์ธก์ •๋œ ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ + - **์ดˆ๊ธฐ ๋ฒˆ๋“ค ํฌ๊ธฐ**: 87% ๊ฐ์†Œ (470 kB โ†’ 62 kB) - **๋ฆฌ๋ Œ๋”๋ง ์ตœ์ ํ™”**: ๋ฉ”๋ชจ์ด์ œ์ด์…˜์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ - **๋™๊ธฐํ™” ํšจ์œจ์„ฑ**: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ 90% ๋‹จ์ถ• -- **๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜**: ์ฝ”๋“œ ์œ ์ง€๋ณด์ˆ˜์„ฑ ๋ฐ ๋””๋ฒ„๊น… ๊ฐœ์„  \ No newline at end of file +- **๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜**: ์ฝ”๋“œ ์œ ์ง€๋ณด์ˆ˜์„ฑ ๋ฐ ๋””๋ฒ„๊น… ๊ฐœ์„  diff --git a/scripts/setup-vercel-env.js b/scripts/setup-vercel-env.js index af3db98..3772dc5 100755 --- a/scripts/setup-vercel-env.js +++ b/scripts/setup-vercel-env.js @@ -5,11 +5,11 @@ * ์ด ์Šคํฌ๋ฆฝํŠธ๋Š” .env.example ํŒŒ์ผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ Vercel ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. */ -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); -const ENV_EXAMPLE_PATH = path.join(__dirname, '..', '.env.example'); +const ENV_EXAMPLE_PATH = path.join(__dirname, "..", ".env.example"); function parseEnvFile(filePath) { if (!fs.existsSync(filePath)) { @@ -17,15 +17,15 @@ function parseEnvFile(filePath) { process.exit(1); } - const content = fs.readFileSync(filePath, 'utf-8'); + const content = fs.readFileSync(filePath, "utf-8"); const envVars = {}; - content.split('\n').forEach(line => { + content.split("\n").forEach((line) => { line = line.trim(); - if (line && !line.startsWith('#') && line.includes('=')) { - const [key, ...values] = line.split('='); - if (key.startsWith('VITE_')) { - envVars[key.trim()] = values.join('=').trim(); + if (line && !line.startsWith("#") && line.includes("=")) { + const [key, ...values] = line.split("="); + if (key.startsWith("VITE_")) { + envVars[key.trim()] = values.join("=").trim(); } } }); @@ -34,52 +34,52 @@ function parseEnvFile(filePath) { } function setupVercelEnv() { - console.log('๐Ÿš€ Vercel ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...'); + console.log("๐Ÿš€ Vercel ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค..."); // Vercel CLI ์„ค์น˜ ํ™•์ธ try { - execSync('vercel --version', { stdio: 'ignore' }); + execSync("vercel --version", { stdio: "ignore" }); } catch (error) { - console.error('โŒ Vercel CLI๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'); - console.error('๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”: npm i -g vercel'); + console.error("โŒ Vercel CLI๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); + console.error("๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”: npm i -g vercel"); process.exit(1); } // .env.example์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์‹ฑ - console.log('๐Ÿ“‹ .env.example์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ฝ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...'); + console.log("๐Ÿ“‹ .env.example์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ฝ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..."); const envVars = parseEnvFile(ENV_EXAMPLE_PATH); if (Object.keys(envVars).length === 0) { - console.log('โš ๏ธ VITE_ ์ ‘๋‘์‚ฌ๋ฅผ ๊ฐ€์ง„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + console.log("โš ๏ธ VITE_ ์ ‘๋‘์‚ฌ๋ฅผ ๊ฐ€์ง„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return; } - console.log('๐Ÿ”ง ๋‹ค์Œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋“ค์„ Vercel์— ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:'); - Object.keys(envVars).forEach(key => { + console.log("๐Ÿ”ง ๋‹ค์Œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋“ค์„ Vercel์— ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:"); + Object.keys(envVars).forEach((key) => { console.log(` - ${key}`); }); - console.log('\\n๐Ÿ“ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•˜๊ฑฐ๋‚˜,'); - console.log('๋‹ค์Œ Vercel CLI ๋ช…๋ น์–ด๋“ค์„ ์‚ฌ์šฉํ•˜์„ธ์š”:\\n'); + console.log("\\n๐Ÿ“ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•˜๊ฑฐ๋‚˜,"); + console.log("๋‹ค์Œ Vercel CLI ๋ช…๋ น์–ด๋“ค์„ ์‚ฌ์šฉํ•˜์„ธ์š”:\\n"); // ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ช…๋ น์–ด ์ƒ์„ฑ const environments = [ - { name: 'production', flag: '--prod' }, - { name: 'preview', flag: '--preview' }, - { name: 'development', flag: '--dev' } + { name: "production", flag: "--prod" }, + { name: "preview", flag: "--preview" }, + { name: "development", flag: "--dev" }, ]; - environments.forEach(env => { + environments.forEach((env) => { console.log(`# ${env.name.toUpperCase()} ํ™˜๊ฒฝ:`); - Object.keys(envVars).forEach(key => { - const placeholder = `your-${env.name}-${key.toLowerCase().replace('vite_', '').replace(/_/g, '-')}`; + Object.keys(envVars).forEach((key) => { + const placeholder = `your-${env.name}-${key.toLowerCase().replace("vite_", "").replace(/_/g, "-")}`; console.log(`vercel env add ${key} ${env.flag} # ๊ฐ’: ${placeholder}`); }); - console.log(''); + console.log(""); }); - console.log('๐Ÿ’ก ํŒ: Vercel ๋Œ€์‹œ๋ณด๋“œ (Settings > Environment Variables)์—์„œ'); - console.log(' ๋” ์‰ฝ๊ฒŒ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'); + console.log("๐Ÿ’ก ํŒ: Vercel ๋Œ€์‹œ๋ณด๋“œ (Settings > Environment Variables)์—์„œ"); + console.log(" ๋” ์‰ฝ๊ฒŒ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); } // ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ @@ -87,4 +87,4 @@ if (require.main === module) { setupVercelEnv(); } -module.exports = { parseEnvFile, setupVercelEnv }; \ No newline at end of file +module.exports = { parseEnvFile, setupVercelEnv }; diff --git a/src/App.tsx b/src/App.tsx index 5801966..9236c00 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,9 @@ const ProfileManagement = lazy(() => import("./pages/ProfileManagement")); const NotFound = lazy(() => import("./pages/NotFound")); const PaymentMethods = lazy(() => import("./pages/PaymentMethods")); const HelpSupport = lazy(() => import("./pages/HelpSupport")); -const SecurityPrivacySettings = lazy(() => import("./pages/SecurityPrivacySettings")); +const SecurityPrivacySettings = lazy( + () => import("./pages/SecurityPrivacySettings") +); const NotificationSettings = lazy(() => import("./pages/NotificationSettings")); const ForgotPassword = lazy(() => import("./pages/ForgotPassword")); const AppwriteSettingsPage = lazy(() => import("./pages/AppwriteSettingsPage")); @@ -198,7 +200,9 @@ function App() { return ( - }> + } + > }> @@ -225,20 +229,17 @@ function App() { {/* React Query ์บ์‹œ ๊ด€๋ฆฌ */} - - + {/* ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ด€๋ฆฌ */} - - + + {/* ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™” - ์„ฑ๋Šฅ ์ตœ์ ํ™”๋กœ 30์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์กฐ์ • */} - { - it('renders button with text', () => { +describe("Button Component", () => { + it("renders button with text", () => { render(); - expect(screen.getByRole('button', { name: 'Test Button' })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Test Button" }) + ).toBeInTheDocument(); }); - it('handles click events', () => { + it("handles click events", () => { const handleClick = vi.fn(); - + render(); - - fireEvent.click(screen.getByRole('button', { name: 'Click me' })); + + fireEvent.click(screen.getByRole("button", { name: "Click me" })); expect(handleClick).toHaveBeenCalledTimes(1); }); - it('can be disabled', () => { + it("can be disabled", () => { render(); - expect(screen.getByRole('button', { name: 'Disabled Button' })).toBeDisabled(); + expect( + screen.getByRole("button", { name: "Disabled Button" }) + ).toBeDisabled(); }); - it('applies variant styles correctly', () => { + it("applies variant styles correctly", () => { render(); - const button = screen.getByRole('button', { name: 'Delete' }); - expect(button).toHaveClass('bg-destructive'); + const button = screen.getByRole("button", { name: "Delete" }); + expect(button).toHaveClass("bg-destructive"); }); -}); \ No newline at end of file +}); diff --git a/src/components/__tests__/ExpenseForm.test.tsx b/src/components/__tests__/ExpenseForm.test.tsx index 2e41d7c..b1febba 100644 --- a/src/components/__tests__/ExpenseForm.test.tsx +++ b/src/components/__tests__/ExpenseForm.test.tsx @@ -1,40 +1,40 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; -import ExpenseForm from '../expenses/ExpenseForm'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import ExpenseForm from "../expenses/ExpenseForm"; // Mock child components with proper props handling -vi.mock('../expenses/ExpenseFormFields', () => ({ +vi.mock("../expenses/ExpenseFormFields", () => ({ default: ({ form, isSubmitting }: any) => (
- {isSubmitting.toString()} - {form ? 'form-present' : 'form-missing'} + + {isSubmitting.toString()} + + + {form ? "form-present" : "form-missing"} +
- ) + ), })); -vi.mock('../expenses/ExpenseSubmitActions', () => ({ +vi.mock("../expenses/ExpenseSubmitActions", () => ({ default: ({ onCancel, isSubmitting }: any) => (
- -
- ) + ), })); -describe('ExpenseForm', () => { +describe("ExpenseForm", () => { const mockOnSubmit = vi.fn(); const mockOnCancel = vi.fn(); @@ -48,164 +48,186 @@ describe('ExpenseForm', () => { vi.clearAllMocks(); }); - describe('rendering', () => { - it('renders the form with all child components', () => { + describe("rendering", () => { + it("renders the form with all child components", () => { render(); - - expect(screen.getByTestId('expense-form')).toBeInTheDocument(); - expect(screen.getByTestId('expense-form-fields')).toBeInTheDocument(); - expect(screen.getByTestId('expense-submit-actions')).toBeInTheDocument(); + + expect(screen.getByTestId("expense-form")).toBeInTheDocument(); + expect(screen.getByTestId("expense-form-fields")).toBeInTheDocument(); + expect(screen.getByTestId("expense-submit-actions")).toBeInTheDocument(); }); - it('applies correct CSS classes to form', () => { + it("applies correct CSS classes to form", () => { render(); - - const form = screen.getByTestId('expense-form'); - expect(form).toHaveClass('space-y-4'); + + const form = screen.getByTestId("expense-form"); + expect(form).toHaveClass("space-y-4"); }); - it('passes form object to ExpenseFormFields', () => { + it("passes form object to ExpenseFormFields", () => { render(); - - expect(screen.getByTestId('form-object')).toHaveTextContent('form-present'); + + expect(screen.getByTestId("form-object")).toHaveTextContent( + "form-present" + ); }); }); - describe('isSubmitting prop handling', () => { - it('passes isSubmitting=false to child components', () => { + describe("isSubmitting prop handling", () => { + it("passes isSubmitting=false to child components", () => { render(); - - expect(screen.getByTestId('fields-submitting-state')).toHaveTextContent('false'); - expect(screen.getByTestId('submit-button')).toHaveTextContent('์ €์žฅ'); - expect(screen.getByTestId('submit-button')).not.toBeDisabled(); - expect(screen.getByTestId('cancel-button')).not.toBeDisabled(); + + expect(screen.getByTestId("fields-submitting-state")).toHaveTextContent( + "false" + ); + expect(screen.getByTestId("submit-button")).toHaveTextContent("์ €์žฅ"); + expect(screen.getByTestId("submit-button")).not.toBeDisabled(); + expect(screen.getByTestId("cancel-button")).not.toBeDisabled(); }); - it('passes isSubmitting=true to child components', () => { + it("passes isSubmitting=true to child components", () => { render(); - - expect(screen.getByTestId('fields-submitting-state')).toHaveTextContent('true'); - expect(screen.getByTestId('submit-button')).toHaveTextContent('์ €์žฅ ์ค‘...'); - expect(screen.getByTestId('submit-button')).toBeDisabled(); - expect(screen.getByTestId('cancel-button')).toBeDisabled(); + + expect(screen.getByTestId("fields-submitting-state")).toHaveTextContent( + "true" + ); + expect(screen.getByTestId("submit-button")).toHaveTextContent( + "์ €์žฅ ์ค‘..." + ); + expect(screen.getByTestId("submit-button")).toBeDisabled(); + expect(screen.getByTestId("cancel-button")).toBeDisabled(); }); - it('updates submitting state correctly when prop changes', () => { - const { rerender } = render(); - - expect(screen.getByTestId('submit-button')).toHaveTextContent('์ €์žฅ'); - + it("updates submitting state correctly when prop changes", () => { + const { rerender } = render( + + ); + + expect(screen.getByTestId("submit-button")).toHaveTextContent("์ €์žฅ"); + rerender(); - - expect(screen.getByTestId('submit-button')).toHaveTextContent('์ €์žฅ ์ค‘...'); + + expect(screen.getByTestId("submit-button")).toHaveTextContent( + "์ €์žฅ ์ค‘..." + ); }); }); - describe('form interactions', () => { - it('calls onCancel when cancel button is clicked', () => { + describe("form interactions", () => { + it("calls onCancel when cancel button is clicked", () => { render(); - - fireEvent.click(screen.getByTestId('cancel-button')); - + + fireEvent.click(screen.getByTestId("cancel-button")); + expect(mockOnCancel).toHaveBeenCalledTimes(1); expect(mockOnSubmit).not.toHaveBeenCalled(); }); - it('does not call onCancel when cancel button is disabled', () => { + it("does not call onCancel when cancel button is disabled", () => { render(); - - const cancelButton = screen.getByTestId('cancel-button'); + + const cancelButton = screen.getByTestId("cancel-button"); expect(cancelButton).toBeDisabled(); - + fireEvent.click(cancelButton); - + expect(mockOnCancel).not.toHaveBeenCalled(); }); - it('prevents form submission when submit button is disabled', () => { + it("prevents form submission when submit button is disabled", () => { render(); - - const submitButton = screen.getByTestId('submit-button'); + + const submitButton = screen.getByTestId("submit-button"); expect(submitButton).toBeDisabled(); - + fireEvent.click(submitButton); - + expect(mockOnSubmit).not.toHaveBeenCalled(); }); }); - describe('prop validation', () => { - it('handles different onCancel functions correctly', () => { + describe("prop validation", () => { + it("handles different onCancel functions correctly", () => { const customOnCancel = vi.fn(); render(); - - fireEvent.click(screen.getByTestId('cancel-button')); - + + fireEvent.click(screen.getByTestId("cancel-button")); + expect(customOnCancel).toHaveBeenCalledTimes(1); expect(mockOnCancel).not.toHaveBeenCalled(); }); - it('maintains form structure with different prop combinations', () => { + it("maintains form structure with different prop combinations", () => { const { rerender } = render(); - - expect(screen.getByTestId('expense-form')).toBeInTheDocument(); - - rerender(); - - expect(screen.getByTestId('expense-form')).toBeInTheDocument(); - expect(screen.getByTestId('expense-form-fields')).toBeInTheDocument(); - expect(screen.getByTestId('expense-submit-actions')).toBeInTheDocument(); + + expect(screen.getByTestId("expense-form")).toBeInTheDocument(); + + rerender( + + ); + + expect(screen.getByTestId("expense-form")).toBeInTheDocument(); + expect(screen.getByTestId("expense-form-fields")).toBeInTheDocument(); + expect(screen.getByTestId("expense-submit-actions")).toBeInTheDocument(); }); }); - describe('accessibility', () => { - it('maintains proper form semantics', () => { + describe("accessibility", () => { + it("maintains proper form semantics", () => { render(); - - const form = screen.getByTestId('expense-form'); - expect(form.tagName).toBe('FORM'); + + const form = screen.getByTestId("expense-form"); + expect(form.tagName).toBe("FORM"); }); - it('submit button has correct type attribute', () => { + it("submit button has correct type attribute", () => { render(); - - const submitButton = screen.getByTestId('submit-button'); - expect(submitButton).toHaveAttribute('type', 'submit'); + + const submitButton = screen.getByTestId("submit-button"); + expect(submitButton).toHaveAttribute("type", "submit"); }); - it('cancel button has correct type attribute', () => { + it("cancel button has correct type attribute", () => { render(); - - const cancelButton = screen.getByTestId('cancel-button'); - expect(cancelButton).toHaveAttribute('type', 'button'); + + const cancelButton = screen.getByTestId("cancel-button"); + expect(cancelButton).toHaveAttribute("type", "button"); }); }); - describe('edge cases', () => { - it('handles rapid state changes', () => { - const { rerender } = render(); - + describe("edge cases", () => { + it("handles rapid state changes", () => { + const { rerender } = render( + + ); + rerender(); rerender(); rerender(); - - expect(screen.getByTestId('expense-form')).toBeInTheDocument(); - expect(screen.getByTestId('submit-button')).toHaveTextContent('์ €์žฅ ์ค‘...'); + + expect(screen.getByTestId("expense-form")).toBeInTheDocument(); + expect(screen.getByTestId("submit-button")).toHaveTextContent( + "์ €์žฅ ์ค‘..." + ); }); - it('maintains component stability during prop updates', () => { + it("maintains component stability during prop updates", () => { const { rerender } = render(); - - const form = screen.getByTestId('expense-form'); - const formFields = screen.getByTestId('expense-form-fields'); - const submitActions = screen.getByTestId('expense-submit-actions'); - + + const form = screen.getByTestId("expense-form"); + const formFields = screen.getByTestId("expense-form-fields"); + const submitActions = screen.getByTestId("expense-submit-actions"); + rerender(); - + // Components should still be present after prop update expect(form).toBeInTheDocument(); expect(formFields).toBeInTheDocument(); expect(submitActions).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/__tests__/Header.test.tsx b/src/components/__tests__/Header.test.tsx index 898f228..536614b 100644 --- a/src/components/__tests__/Header.test.tsx +++ b/src/components/__tests__/Header.test.tsx @@ -1,9 +1,9 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; -import Header from '../Header'; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import Header from "../Header"; // ๋ชจ๋“  ์˜์กด์„ฑ์„ ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„์œผ๋กœ ๋ชจํ‚น -vi.mock('@/utils/logger', () => ({ +vi.mock("@/utils/logger", () => ({ logger: { info: vi.fn(), error: vi.fn(), @@ -11,19 +11,19 @@ vi.mock('@/utils/logger', () => ({ }, })); -vi.mock('@/stores', () => ({ +vi.mock("@/stores", () => ({ useAuth: vi.fn(), })); -vi.mock('@/hooks/use-mobile', () => ({ +vi.mock("@/hooks/use-mobile", () => ({ useIsMobile: vi.fn(() => false), })); -vi.mock('@/utils/platform', () => ({ +vi.mock("@/utils/platform", () => ({ isIOSPlatform: vi.fn(() => false), })); -vi.mock('@/hooks/useNotifications', () => ({ +vi.mock("@/hooks/useNotifications", () => ({ default: vi.fn(() => ({ notifications: [], clearAllNotifications: vi.fn(), @@ -31,13 +31,15 @@ vi.mock('@/hooks/useNotifications', () => ({ })), })); -vi.mock('../notification/NotificationPopover', () => ({ - default: () =>
์•Œ๋ฆผ
+vi.mock("../notification/NotificationPopover", () => ({ + default: () =>
์•Œ๋ฆผ
, })); -vi.mock('@/components/ui/avatar', () => ({ +vi.mock("@/components/ui/avatar", () => ({ Avatar: ({ children, className }: any) => ( -
{children}
+
+ {children} +
), AvatarImage: ({ src, alt }: any) => ( {alt} @@ -47,26 +49,28 @@ vi.mock('@/components/ui/avatar', () => ({ ), })); -vi.mock('@/components/ui/skeleton', () => ({ +vi.mock("@/components/ui/skeleton", () => ({ Skeleton: ({ className }: any) => ( -
Loading...
+
+ Loading... +
), })); -import { useAuth } from '@/stores'; +import { useAuth } from "@/stores"; -describe('Header', () => { +describe("Header", () => { const mockUseAuth = vi.mocked(useAuth); beforeEach(() => { vi.clearAllMocks(); - + // Image constructor ๋ชจํ‚น global.Image = class { onload: (() => void) | null = null; onerror: (() => void) | null = null; - src: string = ''; - + src: string = ""; + constructor() { setTimeout(() => { if (this.onload) this.onload(); @@ -75,154 +79,154 @@ describe('Header', () => { } as any; }); - describe('๊ธฐ๋ณธ ๋ Œ๋”๋ง', () => { - it('ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง๋œ๋‹ค', () => { + describe("๊ธฐ๋ณธ ๋ Œ๋”๋ง", () => { + it("ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - expect(screen.getByTestId('header')).toBeInTheDocument(); - expect(screen.getByTestId('avatar')).toBeInTheDocument(); - expect(screen.getByTestId('notification-popover')).toBeInTheDocument(); + expect(screen.getByTestId("header")).toBeInTheDocument(); + expect(screen.getByTestId("avatar")).toBeInTheDocument(); + expect(screen.getByTestId("notification-popover")).toBeInTheDocument(); }); - it('๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ธฐ๋ณธ ์ธ์‚ฌ๋ง์„ ํ‘œ์‹œํ•œ๋‹ค', () => { + it("๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ธฐ๋ณธ ์ธ์‚ฌ๋ง์„ ํ‘œ์‹œํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - expect(screen.getByText('๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); - expect(screen.getByText('์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ')).toBeInTheDocument(); + expect(screen.getByText("๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); + expect(screen.getByText("์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ")).toBeInTheDocument(); }); - it('๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐœ์ธํ™”๋œ ์ธ์‚ฌ๋ง์„ ํ‘œ์‹œํ•œ๋‹ค', () => { + it("๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐœ์ธํ™”๋œ ์ธ์‚ฌ๋ง์„ ํ‘œ์‹œํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: { user_metadata: { - username: '๊น€์ฒ ์ˆ˜' - } - } + username: "๊น€์ฒ ์ˆ˜", + }, + }, }); render(
); - expect(screen.getByText('๊น€์ฒ ์ˆ˜๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("๊น€์ฒ ์ˆ˜๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); it('์‚ฌ์šฉ์ž ์ด๋ฆ„์ด ์—†์„ ๋•Œ "์ต๋ช…"์œผ๋กœ ํ‘œ์‹œํ•œ๋‹ค', () => { mockUseAuth.mockReturnValue({ user: { - user_metadata: {} - } + user_metadata: {}, + }, }); render(
); - expect(screen.getByText('์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); it('user_metadata๊ฐ€ ์—†์„ ๋•Œ "์ต๋ช…"์œผ๋กœ ํ‘œ์‹œํ•œ๋‹ค', () => { mockUseAuth.mockReturnValue({ - user: {} + user: {}, }); render(
); - expect(screen.getByText('์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); }); - describe('CSS ํด๋ž˜์Šค ๋ฐ ์Šคํƒ€์ผ๋ง', () => { - it('๊ธฐ๋ณธ ํ—ค๋” ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋œ๋‹ค', () => { + describe("CSS ํด๋ž˜์Šค ๋ฐ ์Šคํƒ€์ผ๋ง", () => { + it("๊ธฐ๋ณธ ํ—ค๋” ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const header = screen.getByTestId('header'); - expect(header).toHaveClass('py-4'); + const header = screen.getByTestId("header"); + expect(header).toHaveClass("py-4"); }); - it('์•„๋ฐ”ํƒ€์— ์˜ฌ๋ฐ”๋ฅธ ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋œ๋‹ค', () => { + it("์•„๋ฐ”ํƒ€์— ์˜ฌ๋ฐ”๋ฅธ ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const avatar = screen.getByTestId('avatar'); - expect(avatar).toHaveClass('h-12', 'w-12', 'mr-3'); + const avatar = screen.getByTestId("avatar"); + expect(avatar).toHaveClass("h-12", "w-12", "mr-3"); }); - it('์ œ๋ชฉ์— ์˜ฌ๋ฐ”๋ฅธ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ๋‹ค', () => { + it("์ œ๋ชฉ์— ์˜ฌ๋ฐ”๋ฅธ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const title = screen.getByText('๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค'); - expect(title).toHaveClass('font-bold', 'neuro-text', 'text-xl'); + const title = screen.getByText("๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค"); + expect(title).toHaveClass("font-bold", "neuro-text", "text-xl"); }); - it('๋ถ€์ œ๋ชฉ์— ์˜ฌ๋ฐ”๋ฅธ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ๋‹ค', () => { + it("๋ถ€์ œ๋ชฉ์— ์˜ฌ๋ฐ”๋ฅธ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const subtitle = screen.getByText('์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ'); - expect(subtitle).toHaveClass('text-gray-500', 'text-left'); + const subtitle = screen.getByText("์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ"); + expect(subtitle).toHaveClass("text-gray-500", "text-left"); }); }); - describe('์•„๋ฐ”ํƒ€ ์ฒ˜๋ฆฌ', () => { - it('์•„๋ฐ”ํƒ€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค', () => { + describe("์•„๋ฐ”ํƒ€ ์ฒ˜๋ฆฌ", () => { + it("์•„๋ฐ”ํƒ€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - expect(screen.getByTestId('avatar')).toBeInTheDocument(); + expect(screen.getByTestId("avatar")).toBeInTheDocument(); }); - it('์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘์— ์Šค์ผˆ๋ ˆํ†ค์„ ํ‘œ์‹œํ•œ๋‹ค', () => { + it("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘์— ์Šค์ผˆ๋ ˆํ†ค์„ ํ‘œ์‹œํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - expect(screen.getByTestId('skeleton')).toBeInTheDocument(); - expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.getByTestId("skeleton")).toBeInTheDocument(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); }); }); - describe('์•Œ๋ฆผ ์‹œ์Šคํ…œ', () => { - it('์•Œ๋ฆผ ํŒ์˜ค๋ฒ„๊ฐ€ ๋ Œ๋”๋ง๋œ๋‹ค', () => { + describe("์•Œ๋ฆผ ์‹œ์Šคํ…œ", () => { + it("์•Œ๋ฆผ ํŒ์˜ค๋ฒ„๊ฐ€ ๋ Œ๋”๋ง๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - expect(screen.getByTestId('notification-popover')).toBeInTheDocument(); - expect(screen.getByText('์•Œ๋ฆผ')).toBeInTheDocument(); + expect(screen.getByTestId("notification-popover")).toBeInTheDocument(); + expect(screen.getByText("์•Œ๋ฆผ")).toBeInTheDocument(); }); }); - describe('์ ‘๊ทผ์„ฑ', () => { - it('ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์‹œ๋งจํ‹ฑ ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค', () => { + describe("์ ‘๊ทผ์„ฑ", () => { + it("ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์‹œ๋งจํ‹ฑ ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const header = screen.getByTestId('header'); - expect(header.tagName).toBe('HEADER'); + const header = screen.getByTestId("header"); + expect(header.tagName).toBe("HEADER"); }); - it('์ œ๋ชฉ์ด h1 ํƒœ๊ทธ๋กœ ๋ Œ๋”๋ง๋œ๋‹ค', () => { + it("์ œ๋ชฉ์ด h1 ํƒœ๊ทธ๋กœ ๋ Œ๋”๋ง๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const title = screen.getByRole('heading', { level: 1 }); + const title = screen.getByRole("heading", { level: 1 }); expect(title).toBeInTheDocument(); - expect(title).toHaveTextContent('๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค'); + expect(title).toHaveTextContent("๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค"); }); }); - describe('์—ฃ์ง€ ์ผ€์ด์Šค', () => { - it('user๊ฐ€ null์ผ ๋•Œ ํฌ๋ž˜์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค', () => { + describe("์—ฃ์ง€ ์ผ€์ด์Šค", () => { + it("user๊ฐ€ null์ผ ๋•Œ ํฌ๋ž˜์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); expect(() => { @@ -230,95 +234,101 @@ describe('Header', () => { }).not.toThrow(); }); - it('user_metadata๊ฐ€ ์—†์–ด๋„ ์ฒ˜๋ฆฌํ•œ๋‹ค', () => { + it("user_metadata๊ฐ€ ์—†์–ด๋„ ์ฒ˜๋ฆฌํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ - user: {} + user: {}, }); render(
); - expect(screen.getByText('์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); - it('๊ธด ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค', () => { - const longUsername = 'VeryLongUserNameThatMightCauseIssues'; + it("๊ธด ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค", () => { + const longUsername = "VeryLongUserNameThatMightCauseIssues"; mockUseAuth.mockReturnValue({ user: { user_metadata: { - username: longUsername - } - } + username: longUsername, + }, + }, }); render(
); - expect(screen.getByText(`${longUsername}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค`)).toBeInTheDocument(); + expect( + screen.getByText(`${longUsername}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค`) + ).toBeInTheDocument(); }); - it('ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค', () => { - const specialUsername = '๊น€@์ฒ #์ˆ˜$123'; + it("ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค", () => { + const specialUsername = "๊น€@์ฒ #์ˆ˜$123"; mockUseAuth.mockReturnValue({ user: { user_metadata: { - username: specialUsername - } - } + username: specialUsername, + }, + }, }); render(
); - expect(screen.getByText(`${specialUsername}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค`)).toBeInTheDocument(); + expect( + screen.getByText(`${specialUsername}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค`) + ).toBeInTheDocument(); }); - it('๋นˆ ๋ฌธ์ž์—ด ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค', () => { + it("๋นˆ ๋ฌธ์ž์—ด ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: { user_metadata: { - username: '' - } - } + username: "", + }, + }, }); render(
); - expect(screen.getByText('์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("์ต๋ช…๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); - it('undefined user๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค', () => { + it("undefined user๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: undefined }); render(
); - expect(screen.getByText('๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค')).toBeInTheDocument(); + expect(screen.getByText("๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค")).toBeInTheDocument(); }); }); - describe('๋ ˆ์ด์•„์›ƒ ๋ฐ ๊ตฌ์กฐ', () => { - it('์˜ฌ๋ฐ”๋ฅธ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค', () => { + describe("๋ ˆ์ด์•„์›ƒ ๋ฐ ๊ตฌ์กฐ", () => { + it("์˜ฌ๋ฐ”๋ฅธ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const header = screen.getByTestId('header'); - const flexContainer = header.querySelector('.flex.justify-between.items-center'); + const header = screen.getByTestId("header"); + const flexContainer = header.querySelector( + ".flex.justify-between.items-center" + ); expect(flexContainer).toBeInTheDocument(); - const leftSection = flexContainer?.querySelector('.flex.items-center'); + const leftSection = flexContainer?.querySelector(".flex.items-center"); expect(leftSection).toBeInTheDocument(); }); - it('์•„๋ฐ”ํƒ€์™€ ํ…์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐฐ์น˜๋œ๋‹ค', () => { + it("์•„๋ฐ”ํƒ€์™€ ํ…์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐฐ์น˜๋œ๋‹ค", () => { mockUseAuth.mockReturnValue({ user: null }); render(
); - const avatar = screen.getByTestId('avatar'); - const title = screen.getByText('๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค'); - const subtitle = screen.getByText('์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ'); + const avatar = screen.getByTestId("avatar"); + const title = screen.getByText("๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค"); + const subtitle = screen.getByText("์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ"); expect(avatar).toBeInTheDocument(); expect(title).toBeInTheDocument(); expect(subtitle).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/__tests__/LoginForm.test.tsx b/src/components/__tests__/LoginForm.test.tsx index 57b1e6f..b025665 100644 --- a/src/components/__tests__/LoginForm.test.tsx +++ b/src/components/__tests__/LoginForm.test.tsx @@ -1,11 +1,11 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { BrowserRouter } from 'react-router-dom'; -import LoginForm from '../auth/LoginForm'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { BrowserRouter } from "react-router-dom"; +import LoginForm from "../auth/LoginForm"; // Mock react-router-dom Link component -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); return { ...actual, Link: ({ to, children, className }: any) => ( @@ -21,16 +21,16 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); -describe('LoginForm', () => { +describe("LoginForm", () => { const mockSetEmail = vi.fn(); const mockSetPassword = vi.fn(); const mockSetShowPassword = vi.fn(); const mockHandleLogin = vi.fn(); const defaultProps = { - email: '', + email: "", setEmail: mockSetEmail, - password: '', + password: "", setPassword: mockSetPassword, showPassword: false, setShowPassword: mockSetShowPassword, @@ -44,367 +44,380 @@ describe('LoginForm', () => { vi.clearAllMocks(); }); - describe('rendering', () => { - it('renders the login form with all fields', () => { + describe("rendering", () => { + it("renders the login form with all fields", () => { render(, { wrapper: Wrapper }); - expect(screen.getByTestId('login-form')).toBeInTheDocument(); - expect(screen.getByLabelText('์ด๋ฉ”์ผ')).toBeInTheDocument(); - expect(screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ')).toBeInTheDocument(); - expect(screen.getByText('๋กœ๊ทธ์ธ')).toBeInTheDocument(); - expect(screen.getByTestId('forgot-password-link')).toBeInTheDocument(); + expect(screen.getByTestId("login-form")).toBeInTheDocument(); + expect(screen.getByLabelText("์ด๋ฉ”์ผ")).toBeInTheDocument(); + expect(screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ")).toBeInTheDocument(); + expect(screen.getByText("๋กœ๊ทธ์ธ")).toBeInTheDocument(); + expect(screen.getByTestId("forgot-password-link")).toBeInTheDocument(); }); - it('renders email field with correct attributes', () => { + it("renders email field with correct attributes", () => { render(, { wrapper: Wrapper }); - const emailInput = screen.getByLabelText('์ด๋ฉ”์ผ'); - expect(emailInput).toHaveAttribute('type', 'email'); - expect(emailInput).toHaveAttribute('id', 'email'); - expect(emailInput).toHaveAttribute('placeholder', 'your@email.com'); + const emailInput = screen.getByLabelText("์ด๋ฉ”์ผ"); + expect(emailInput).toHaveAttribute("type", "email"); + expect(emailInput).toHaveAttribute("id", "email"); + expect(emailInput).toHaveAttribute("placeholder", "your@email.com"); }); - it('renders password field with correct attributes', () => { + it("renders password field with correct attributes", () => { render(, { wrapper: Wrapper }); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); - expect(passwordInput).toHaveAttribute('type', 'password'); - expect(passwordInput).toHaveAttribute('id', 'password'); - expect(passwordInput).toHaveAttribute('placeholder', 'โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข'); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); + expect(passwordInput).toHaveAttribute("type", "password"); + expect(passwordInput).toHaveAttribute("id", "password"); + expect(passwordInput).toHaveAttribute("placeholder", "โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข"); }); - it('renders forgot password link', () => { + it("renders forgot password link", () => { render(, { wrapper: Wrapper }); - const forgotLink = screen.getByTestId('forgot-password-link'); - expect(forgotLink).toHaveAttribute('href', '/forgot-password'); - expect(forgotLink).toHaveTextContent('๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์…จ๋‚˜์š”?'); + const forgotLink = screen.getByTestId("forgot-password-link"); + expect(forgotLink).toHaveAttribute("href", "/forgot-password"); + expect(forgotLink).toHaveTextContent("๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์…จ๋‚˜์š”?"); }); }); - describe('form values', () => { - it('displays current email value', () => { - render( - , - { wrapper: Wrapper } - ); + describe("form values", () => { + it("displays current email value", () => { + render(, { + wrapper: Wrapper, + }); - expect(screen.getByDisplayValue('test@example.com')).toBeInTheDocument(); + expect(screen.getByDisplayValue("test@example.com")).toBeInTheDocument(); }); - it('displays current password value', () => { - render( - , - { wrapper: Wrapper } - ); + it("displays current password value", () => { + render(, { + wrapper: Wrapper, + }); - expect(screen.getByDisplayValue('mypassword')).toBeInTheDocument(); + expect(screen.getByDisplayValue("mypassword")).toBeInTheDocument(); }); - it('calls setEmail when email input changes', () => { + it("calls setEmail when email input changes", () => { render(, { wrapper: Wrapper }); - const emailInput = screen.getByLabelText('์ด๋ฉ”์ผ'); - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + const emailInput = screen.getByLabelText("์ด๋ฉ”์ผ"); + fireEvent.change(emailInput, { target: { value: "test@example.com" } }); - expect(mockSetEmail).toHaveBeenCalledWith('test@example.com'); + expect(mockSetEmail).toHaveBeenCalledWith("test@example.com"); }); - it('calls setPassword when password input changes', () => { + it("calls setPassword when password input changes", () => { render(, { wrapper: Wrapper }); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); - fireEvent.change(passwordInput, { target: { value: 'newpassword' } }); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); + fireEvent.change(passwordInput, { target: { value: "newpassword" } }); - expect(mockSetPassword).toHaveBeenCalledWith('newpassword'); + expect(mockSetPassword).toHaveBeenCalledWith("newpassword"); }); }); - describe('password visibility toggle', () => { - it('shows password as hidden by default', () => { + describe("password visibility toggle", () => { + it("shows password as hidden by default", () => { render(, { wrapper: Wrapper }); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); - expect(passwordInput).toHaveAttribute('type', 'password'); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); + expect(passwordInput).toHaveAttribute("type", "password"); }); - it('shows password as text when showPassword is true', () => { - render( - , - { wrapper: Wrapper } - ); + it("shows password as text when showPassword is true", () => { + render(, { + wrapper: Wrapper, + }); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); - expect(passwordInput).toHaveAttribute('type', 'text'); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); + expect(passwordInput).toHaveAttribute("type", "text"); }); - it('calls setShowPassword when visibility toggle is clicked', () => { + it("calls setShowPassword when visibility toggle is clicked", () => { render(, { wrapper: Wrapper }); // Find the password toggle button (the one that's not the submit button) - const buttons = screen.getAllByRole('button'); - const toggleButton = buttons.find(btn => btn.getAttribute('type') === 'button'); - + const buttons = screen.getAllByRole("button"); + const toggleButton = buttons.find( + (btn) => btn.getAttribute("type") === "button" + ); + fireEvent.click(toggleButton!); expect(mockSetShowPassword).toHaveBeenCalledWith(true); }); - it('calls setShowPassword with opposite value', () => { - render( - , - { wrapper: Wrapper } - ); + it("calls setShowPassword with opposite value", () => { + render(, { + wrapper: Wrapper, + }); // Find the password toggle button (the one that's not the submit button) - const buttons = screen.getAllByRole('button'); - const toggleButton = buttons.find(btn => btn.getAttribute('type') === 'button'); - + const buttons = screen.getAllByRole("button"); + const toggleButton = buttons.find( + (btn) => btn.getAttribute("type") === "button" + ); + fireEvent.click(toggleButton!); expect(mockSetShowPassword).toHaveBeenCalledWith(false); }); }); - describe('form submission', () => { - it('calls handleLogin when form is submitted', () => { + describe("form submission", () => { + it("calls handleLogin when form is submitted", () => { render(, { wrapper: Wrapper }); - const form = screen.getByTestId('login-form'); + const form = screen.getByTestId("login-form"); fireEvent.submit(form); expect(mockHandleLogin).toHaveBeenCalledTimes(1); }); - it('does not submit when form is disabled during loading', () => { - render( - , - { wrapper: Wrapper } - ); + it("does not submit when form is disabled during loading", () => { + render(, { + wrapper: Wrapper, + }); - const loginButton = screen.getByText('๋กœ๊ทธ์ธ ์ค‘...'); + const loginButton = screen.getByText("๋กœ๊ทธ์ธ ์ค‘..."); expect(loginButton).toBeDisabled(); }); - it('does not submit when form is disabled during table setup', () => { - render( - , - { wrapper: Wrapper } - ); + it("does not submit when form is disabled during table setup", () => { + render(, { + wrapper: Wrapper, + }); - const loginButton = screen.getByText('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘...'); + const loginButton = screen.getByText("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘..."); expect(loginButton).toBeDisabled(); }); }); - describe('loading states', () => { - it('shows loading text when isLoading is true', () => { - render( - , - { wrapper: Wrapper } - ); + describe("loading states", () => { + it("shows loading text when isLoading is true", () => { + render(, { + wrapper: Wrapper, + }); - expect(screen.getByText('๋กœ๊ทธ์ธ ์ค‘...')).toBeInTheDocument(); - const submitButton = screen.getByText('๋กœ๊ทธ์ธ ์ค‘...').closest('button'); + expect(screen.getByText("๋กœ๊ทธ์ธ ์ค‘...")).toBeInTheDocument(); + const submitButton = screen.getByText("๋กœ๊ทธ์ธ ์ค‘...").closest("button"); expect(submitButton).toBeDisabled(); }); - it('shows table setup text when isSettingUpTables is true', () => { - render( - , - { wrapper: Wrapper } - ); + it("shows table setup text when isSettingUpTables is true", () => { + render(, { + wrapper: Wrapper, + }); - expect(screen.getByText('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘...')).toBeInTheDocument(); - const submitButton = screen.getByText('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘...').closest('button'); + expect(screen.getByText("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘...")).toBeInTheDocument(); + const submitButton = screen + .getByText("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘...") + .closest("button"); expect(submitButton).toBeDisabled(); }); - it('shows normal text when not loading', () => { + it("shows normal text when not loading", () => { render(, { wrapper: Wrapper }); - expect(screen.getByText('๋กœ๊ทธ์ธ')).toBeInTheDocument(); - const submitButton = screen.getByText('๋กœ๊ทธ์ธ').closest('button'); + expect(screen.getByText("๋กœ๊ทธ์ธ")).toBeInTheDocument(); + const submitButton = screen.getByText("๋กœ๊ทธ์ธ").closest("button"); expect(submitButton).not.toBeDisabled(); }); - it('isLoading takes precedence over isSettingUpTables', () => { + it("isLoading takes precedence over isSettingUpTables", () => { render( - , + , { wrapper: Wrapper } ); - expect(screen.getByText('๋กœ๊ทธ์ธ ์ค‘...')).toBeInTheDocument(); + expect(screen.getByText("๋กœ๊ทธ์ธ ์ค‘...")).toBeInTheDocument(); }); }); - describe('error handling', () => { - it('does not show error message when loginError is null', () => { + describe("error handling", () => { + it("does not show error message when loginError is null", () => { render(, { wrapper: Wrapper }); expect(screen.queryByText(/์—๋Ÿฌ/)).not.toBeInTheDocument(); }); - it('shows regular error message for standard errors', () => { - const errorMessage = '์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.'; - render( - , - { wrapper: Wrapper } - ); + it("shows regular error message for standard errors", () => { + const errorMessage = "์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค."; + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); - it('shows CORS/JSON error with special styling and suggestions', () => { - const corsError = 'CORS ์ •์ฑ…์— ์˜ํ•ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'; - render( - , - { wrapper: Wrapper } - ); + it("shows CORS/JSON error with special styling and suggestions", () => { + const corsError = "CORS ์ •์ฑ…์— ์˜ํ•ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(corsError)).toBeInTheDocument(); - expect(screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/)).toBeInTheDocument(); - expect(screen.getByText(/HTTPS URL์„ ์‚ฌ์šฉํ•˜๋Š” Supabase ์ธ์Šคํ„ด์Šค๋กœ ๋ณ€๊ฒฝ/)).toBeInTheDocument(); - expect(screen.getByText(/๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”/)).toBeInTheDocument(); + expect( + screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/) + ).toBeInTheDocument(); + expect( + screen.getByText(/HTTPS URL์„ ์‚ฌ์šฉํ•˜๋Š” Supabase ์ธ์Šคํ„ด์Šค๋กœ ๋ณ€๊ฒฝ/) + ).toBeInTheDocument(); + expect( + screen.getByText(/๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”/) + ).toBeInTheDocument(); }); - it('detects JSON errors correctly', () => { - const jsonError = 'JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - render( - , - { wrapper: Wrapper } - ); + it("detects JSON errors correctly", () => { + const jsonError = "JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(jsonError)).toBeInTheDocument(); - expect(screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/)).toBeInTheDocument(); + expect( + screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/) + ).toBeInTheDocument(); }); - it('detects network 404 errors correctly', () => { - const networkError = '404 Not Found ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.'; - render( - , - { wrapper: Wrapper } - ); + it("detects network 404 errors correctly", () => { + const networkError = "404 Not Found ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค."; + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(networkError)).toBeInTheDocument(); - expect(screen.getByText(/๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”/)).toBeInTheDocument(); + expect( + screen.getByText(/๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”/) + ).toBeInTheDocument(); }); - it('detects proxy errors correctly', () => { - const proxyError = 'ํ”„๋ก์‹œ ์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.'; - render( - , - { wrapper: Wrapper } - ); + it("detects proxy errors correctly", () => { + const proxyError = "ํ”„๋ก์‹œ ์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค."; + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(proxyError)).toBeInTheDocument(); - expect(screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/)).toBeInTheDocument(); + expect( + screen.getByText(/๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”/) + ).toBeInTheDocument(); }); }); - describe('CSS classes and styling', () => { - it('applies correct CSS classes to form container', () => { + describe("CSS classes and styling", () => { + it("applies correct CSS classes to form container", () => { render(, { wrapper: Wrapper }); - const container = screen.getByTestId('login-form').parentElement; - expect(container).toHaveClass('neuro-flat', 'p-8', 'mb-6'); + const container = screen.getByTestId("login-form").parentElement; + expect(container).toHaveClass("neuro-flat", "p-8", "mb-6"); }); - it('applies correct CSS classes to email input', () => { + it("applies correct CSS classes to email input", () => { render(, { wrapper: Wrapper }); - const emailInput = screen.getByLabelText('์ด๋ฉ”์ผ'); - expect(emailInput).toHaveClass('pl-10', 'neuro-pressed'); + const emailInput = screen.getByLabelText("์ด๋ฉ”์ผ"); + expect(emailInput).toHaveClass("pl-10", "neuro-pressed"); }); - it('applies correct CSS classes to password input', () => { + it("applies correct CSS classes to password input", () => { render(, { wrapper: Wrapper }); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); - expect(passwordInput).toHaveClass('pl-10', 'neuro-pressed'); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); + expect(passwordInput).toHaveClass("pl-10", "neuro-pressed"); }); - it('applies correct CSS classes to submit button', () => { + it("applies correct CSS classes to submit button", () => { render(, { wrapper: Wrapper }); - const submitButton = screen.getByRole('button', { name: /๋กœ๊ทธ์ธ/ }); + const submitButton = screen.getByRole("button", { name: /๋กœ๊ทธ์ธ/ }); expect(submitButton).toHaveClass( - 'w-full', - 'hover:bg-neuro-income/80', - 'text-white', - 'h-auto', - 'bg-neuro-income', - 'text-lg' + "w-full", + "hover:bg-neuro-income/80", + "text-white", + "h-auto", + "bg-neuro-income", + "text-lg" ); }); }); - describe('accessibility', () => { - it('has proper form labels', () => { + describe("accessibility", () => { + it("has proper form labels", () => { render(, { wrapper: Wrapper }); - expect(screen.getByLabelText('์ด๋ฉ”์ผ')).toBeInTheDocument(); - expect(screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ')).toBeInTheDocument(); + expect(screen.getByLabelText("์ด๋ฉ”์ผ")).toBeInTheDocument(); + expect(screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ")).toBeInTheDocument(); }); - it('has proper input IDs matching labels', () => { + it("has proper input IDs matching labels", () => { render(, { wrapper: Wrapper }); - const emailInput = screen.getByLabelText('์ด๋ฉ”์ผ'); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); + const emailInput = screen.getByLabelText("์ด๋ฉ”์ผ"); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); - expect(emailInput).toHaveAttribute('id', 'email'); - expect(passwordInput).toHaveAttribute('id', 'password'); + expect(emailInput).toHaveAttribute("id", "email"); + expect(passwordInput).toHaveAttribute("id", "password"); }); - it('password toggle button has correct type', () => { + it("password toggle button has correct type", () => { render(, { wrapper: Wrapper }); // Find the eye icon button (the one that's not the submit button) - const buttons = screen.getAllByRole('button'); - const toggleButton = buttons.find(button => button.getAttribute('type') === 'button'); - - expect(toggleButton).toHaveAttribute('type', 'button'); + const buttons = screen.getAllByRole("button"); + const toggleButton = buttons.find( + (button) => button.getAttribute("type") === "button" + ); + + expect(toggleButton).toHaveAttribute("type", "button"); }); - it('submit button has correct type', () => { + it("submit button has correct type", () => { render(, { wrapper: Wrapper }); - const submitButton = screen.getByText('๋กœ๊ทธ์ธ').closest('button'); - expect(submitButton).toHaveAttribute('type', 'submit'); + const submitButton = screen.getByText("๋กœ๊ทธ์ธ").closest("button"); + expect(submitButton).toHaveAttribute("type", "submit"); }); }); - describe('edge cases', () => { - it('handles empty email and password values', () => { - render( - , - { wrapper: Wrapper } - ); + describe("edge cases", () => { + it("handles empty email and password values", () => { + render(, { + wrapper: Wrapper, + }); - const emailInput = screen.getByLabelText('์ด๋ฉ”์ผ'); - const passwordInput = screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ'); + const emailInput = screen.getByLabelText("์ด๋ฉ”์ผ"); + const passwordInput = screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ"); - expect(emailInput).toHaveValue(''); - expect(passwordInput).toHaveValue(''); + expect(emailInput).toHaveValue(""); + expect(passwordInput).toHaveValue(""); }); - it('handles very long error messages', () => { - const longError = 'A'.repeat(1000); - render( - , - { wrapper: Wrapper } - ); + it("handles very long error messages", () => { + const longError = "A".repeat(1000); + render(, { + wrapper: Wrapper, + }); expect(screen.getByText(longError)).toBeInTheDocument(); }); - it('handles special characters in email and password', () => { - const specialEmail = 'test+tag@example-domain.co.uk'; - const specialPassword = 'P@ssw0rd!#$%'; + it("handles special characters in email and password", () => { + const specialEmail = "test+tag@example-domain.co.uk"; + const specialPassword = "P@ssw0rd!#$%"; render( - , + , { wrapper: Wrapper } ); @@ -412,8 +425,10 @@ describe('LoginForm', () => { expect(screen.getByDisplayValue(specialPassword)).toBeInTheDocument(); }); - it('maintains form functionality during rapid state changes', () => { - const { rerender } = render(, { wrapper: Wrapper }); + it("maintains form functionality during rapid state changes", () => { + const { rerender } = render(, { + wrapper: Wrapper, + }); // Rapid state changes rerender(); @@ -422,9 +437,9 @@ describe('LoginForm', () => { rerender(); // Form should still be functional - expect(screen.getByTestId('login-form')).toBeInTheDocument(); - expect(screen.getByLabelText('์ด๋ฉ”์ผ')).toBeInTheDocument(); - expect(screen.getByLabelText('๋น„๋ฐ€๋ฒˆํ˜ธ')).toBeInTheDocument(); + expect(screen.getByTestId("login-form")).toBeInTheDocument(); + expect(screen.getByLabelText("์ด๋ฉ”์ผ")).toBeInTheDocument(); + expect(screen.getByLabelText("๋น„๋ฐ€๋ฒˆํ˜ธ")).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/__tests__/TransactionCard.test.tsx b/src/components/__tests__/TransactionCard.test.tsx index 47f9359..4261e82 100644 --- a/src/components/__tests__/TransactionCard.test.tsx +++ b/src/components/__tests__/TransactionCard.test.tsx @@ -1,290 +1,322 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import TransactionCard from '../TransactionCard'; -import { Transaction } from '@/contexts/budget/types'; -import { logger } from '@/utils/logger'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import TransactionCard from "../TransactionCard"; +import { Transaction } from "@/contexts/budget/types"; +import { logger } from "@/utils/logger"; // Mock child components to isolate TransactionCard testing -vi.mock('../TransactionEditDialog', () => ({ - default: ({ open, onOpenChange, transaction, onDelete }: any) => +vi.mock("../TransactionEditDialog", () => ({ + default: ({ open, onOpenChange, transaction, onDelete }: any) => open ? (
Edit Dialog for: {transaction.title}
- ) : null + ) : null, })); -vi.mock('../transaction/TransactionIcon', () => ({ +vi.mock("../transaction/TransactionIcon", () => ({ default: ({ category }: { category: string }) => (
{category} icon
- ) + ), })); -vi.mock('../transaction/TransactionDetails', () => ({ +vi.mock("../transaction/TransactionDetails", () => ({ default: ({ title, date }: { title: string; date: string }) => (
{title}
{date}
- ) + ), })); -vi.mock('../transaction/TransactionAmount', () => ({ +vi.mock("../transaction/TransactionAmount", () => ({ default: ({ amount }: { amount: number }) => (
{amount}์›
- ) + ), })); // Mock logger -vi.mock('@/utils/logger', () => ({ +vi.mock("@/utils/logger", () => ({ logger: { info: vi.fn(), error: vi.fn(), }, })); -describe('TransactionCard', () => { +describe("TransactionCard", () => { const mockTransaction: Transaction = { - id: 'test-transaction-1', - title: 'Coffee Shop', + id: "test-transaction-1", + title: "Coffee Shop", amount: 5000, - date: '2024-06-15', - category: 'Food', - type: 'expense', - paymentMethod: '์‹ ์šฉ์นด๋“œ', + date: "2024-06-15", + category: "Food", + type: "expense", + paymentMethod: "์‹ ์šฉ์นด๋“œ", }; - describe('rendering', () => { - it('renders transaction card with all components', () => { + describe("rendering", () => { + it("renders transaction card with all components", () => { render(); - expect(screen.getByTestId('transaction-icon')).toBeInTheDocument(); - expect(screen.getByTestId('transaction-details')).toBeInTheDocument(); - expect(screen.getByTestId('transaction-amount')).toBeInTheDocument(); - expect(screen.getByText('Coffee Shop')).toBeInTheDocument(); - expect(screen.getByText('2024-06-15')).toBeInTheDocument(); - expect(screen.getByText('5000์›')).toBeInTheDocument(); + expect(screen.getByTestId("transaction-icon")).toBeInTheDocument(); + expect(screen.getByTestId("transaction-details")).toBeInTheDocument(); + expect(screen.getByTestId("transaction-amount")).toBeInTheDocument(); + expect(screen.getByText("Coffee Shop")).toBeInTheDocument(); + expect(screen.getByText("2024-06-15")).toBeInTheDocument(); + expect(screen.getByText("5000์›")).toBeInTheDocument(); }); - it('passes correct props to child components', () => { + it("passes correct props to child components", () => { render(); - expect(screen.getByText('Food icon')).toBeInTheDocument(); - expect(screen.getByText('Coffee Shop')).toBeInTheDocument(); - expect(screen.getByText('2024-06-15')).toBeInTheDocument(); - expect(screen.getByText('5000์›')).toBeInTheDocument(); + expect(screen.getByText("Food icon")).toBeInTheDocument(); + expect(screen.getByText("Coffee Shop")).toBeInTheDocument(); + expect(screen.getByText("2024-06-15")).toBeInTheDocument(); + expect(screen.getByText("5000์›")).toBeInTheDocument(); }); - it('renders with different transaction data', () => { + it("renders with different transaction data", () => { const differentTransaction: Transaction = { - id: 'test-transaction-2', - title: 'Gas Station', + id: "test-transaction-2", + title: "Gas Station", amount: 50000, - date: '2024-07-01', - category: 'Transportation', - type: 'expense', - paymentMethod: 'ํ˜„๊ธˆ', + date: "2024-07-01", + category: "Transportation", + type: "expense", + paymentMethod: "ํ˜„๊ธˆ", }; render(); - expect(screen.getByText('Gas Station')).toBeInTheDocument(); - expect(screen.getByText('2024-07-01')).toBeInTheDocument(); - expect(screen.getByText('50000์›')).toBeInTheDocument(); - expect(screen.getByText('Transportation icon')).toBeInTheDocument(); + expect(screen.getByText("Gas Station")).toBeInTheDocument(); + expect(screen.getByText("2024-07-01")).toBeInTheDocument(); + expect(screen.getByText("50000์›")).toBeInTheDocument(); + expect(screen.getByText("Transportation icon")).toBeInTheDocument(); }); }); - describe('user interactions', () => { - it('opens edit dialog when card is clicked', () => { + describe("user interactions", () => { + it("opens edit dialog when card is clicked", () => { render(); - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); fireEvent.click(card); - expect(screen.getByTestId('transaction-edit-dialog')).toBeInTheDocument(); - expect(screen.getByText('Edit Dialog for: Coffee Shop')).toBeInTheDocument(); + expect(screen.getByTestId("transaction-edit-dialog")).toBeInTheDocument(); + expect( + screen.getByText("Edit Dialog for: Coffee Shop") + ).toBeInTheDocument(); }); - it('closes edit dialog when close button is clicked', () => { + it("closes edit dialog when close button is clicked", () => { render(); // Open dialog - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); fireEvent.click(card); - expect(screen.getByTestId('transaction-edit-dialog')).toBeInTheDocument(); + expect(screen.getByTestId("transaction-edit-dialog")).toBeInTheDocument(); // Close dialog - const closeButton = screen.getByText('Close'); + const closeButton = screen.getByText("Close"); fireEvent.click(closeButton); - expect(screen.queryByTestId('transaction-edit-dialog')).not.toBeInTheDocument(); + expect( + screen.queryByTestId("transaction-edit-dialog") + ).not.toBeInTheDocument(); }); - it('initially does not show edit dialog', () => { + it("initially does not show edit dialog", () => { render(); - expect(screen.queryByTestId('transaction-edit-dialog')).not.toBeInTheDocument(); + expect( + screen.queryByTestId("transaction-edit-dialog") + ).not.toBeInTheDocument(); }); }); - describe('delete functionality', () => { - it('calls onDelete when delete button is clicked in dialog', async () => { + describe("delete functionality", () => { + it("calls onDelete when delete button is clicked in dialog", async () => { const mockOnDelete = vi.fn().mockResolvedValue(true); - render(); + render( + + ); // Open dialog - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); fireEvent.click(card); // Click delete - const deleteButton = screen.getByText('Delete'); + const deleteButton = screen.getByText("Delete"); fireEvent.click(deleteButton); - expect(mockOnDelete).toHaveBeenCalledWith('test-transaction-1'); + expect(mockOnDelete).toHaveBeenCalledWith("test-transaction-1"); }); - it('handles delete when no onDelete prop is provided', async () => { + it("handles delete when no onDelete prop is provided", async () => { render(); // Open dialog - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); fireEvent.click(card); // Click delete (should not crash) - const deleteButton = screen.getByText('Delete'); + const deleteButton = screen.getByText("Delete"); fireEvent.click(deleteButton); // Should not crash and should log expect(vi.mocked(logger.info)).toHaveBeenCalledWith( - '์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค' + "์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค" ); }); - it('handles delete error gracefully', async () => { - const mockOnDelete = vi.fn().mockRejectedValue(new Error('Delete failed')); - render(); + it("handles delete error gracefully", async () => { + const mockOnDelete = vi + .fn() + .mockRejectedValue(new Error("Delete failed")); + render( + + ); // Open dialog - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); fireEvent.click(card); // Click delete - const deleteButton = screen.getByText('Delete'); + const deleteButton = screen.getByText("Delete"); fireEvent.click(deleteButton); - expect(mockOnDelete).toHaveBeenCalledWith('test-transaction-1'); - + expect(mockOnDelete).toHaveBeenCalledWith("test-transaction-1"); + // Wait for the promise to be resolved/rejected - await vi.waitFor(() => { - expect(vi.mocked(logger.error)).toHaveBeenCalledWith( - 'ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', - expect.any(Error) - ); - }, { timeout: 1000 }); + await vi.waitFor( + () => { + expect(vi.mocked(logger.error)).toHaveBeenCalledWith( + "ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", + expect.any(Error) + ); + }, + { timeout: 1000 } + ); }); - it('handles both sync and async onDelete functions', async () => { + it("handles both sync and async onDelete functions", async () => { // Test sync function const syncOnDelete = vi.fn().mockReturnValue(true); const { rerender } = render( - + ); - let card = screen.getByTestId('transaction-card'); + let card = screen.getByTestId("transaction-card"); fireEvent.click(card); - let deleteButton = screen.getByText('Delete'); + let deleteButton = screen.getByText("Delete"); fireEvent.click(deleteButton); - expect(syncOnDelete).toHaveBeenCalledWith('test-transaction-1'); + expect(syncOnDelete).toHaveBeenCalledWith("test-transaction-1"); // Test async function const asyncOnDelete = vi.fn().mockResolvedValue(true); - rerender(); + rerender( + + ); - card = screen.getByTestId('transaction-card'); + card = screen.getByTestId("transaction-card"); fireEvent.click(card); - deleteButton = screen.getByText('Delete'); + deleteButton = screen.getByText("Delete"); fireEvent.click(deleteButton); - expect(asyncOnDelete).toHaveBeenCalledWith('test-transaction-1'); + expect(asyncOnDelete).toHaveBeenCalledWith("test-transaction-1"); }); }); - describe('CSS classes and styling', () => { - it('applies correct CSS classes to the card', () => { + describe("CSS classes and styling", () => { + it("applies correct CSS classes to the card", () => { render(); - const card = screen.getByTestId('transaction-card'); + const card = screen.getByTestId("transaction-card"); expect(card).toHaveClass( - 'neuro-flat', - 'p-4', - 'transition-all', - 'duration-300', - 'hover:shadow-neuro-convex', - 'animate-scale-in', - 'cursor-pointer' + "neuro-flat", + "p-4", + "transition-all", + "duration-300", + "hover:shadow-neuro-convex", + "animate-scale-in", + "cursor-pointer" ); }); - it('has correct layout structure', () => { + it("has correct layout structure", () => { render(); - const card = screen.getByTestId('transaction-card'); - const flexContainer = card.querySelector('.flex.items-center.justify-between'); + const card = screen.getByTestId("transaction-card"); + const flexContainer = card.querySelector( + ".flex.items-center.justify-between" + ); expect(flexContainer).toBeInTheDocument(); - const leftSection = card.querySelector('.flex.items-center.gap-3'); + const leftSection = card.querySelector(".flex.items-center.gap-3"); expect(leftSection).toBeInTheDocument(); }); }); - describe('accessibility', () => { - it('is keyboard accessible', () => { + describe("accessibility", () => { + it("is keyboard accessible", () => { render(); - const card = screen.getByTestId('transaction-card'); - expect(card).toHaveClass('cursor-pointer'); - + const card = screen.getByTestId("transaction-card"); + expect(card).toHaveClass("cursor-pointer"); + // Should be clickable fireEvent.click(card); - expect(screen.getByTestId('transaction-edit-dialog')).toBeInTheDocument(); + expect(screen.getByTestId("transaction-edit-dialog")).toBeInTheDocument(); }); - it('provides semantic content for screen readers', () => { + it("provides semantic content for screen readers", () => { render(); // All important information should be accessible to screen readers - expect(screen.getByText('Coffee Shop')).toBeInTheDocument(); - expect(screen.getByText('2024-06-15')).toBeInTheDocument(); - expect(screen.getByText('5000์›')).toBeInTheDocument(); - expect(screen.getByText('Food icon')).toBeInTheDocument(); + expect(screen.getByText("Coffee Shop")).toBeInTheDocument(); + expect(screen.getByText("2024-06-15")).toBeInTheDocument(); + expect(screen.getByText("5000์›")).toBeInTheDocument(); + expect(screen.getByText("Food icon")).toBeInTheDocument(); }); }); - describe('edge cases', () => { - it('handles missing optional transaction fields', () => { + describe("edge cases", () => { + it("handles missing optional transaction fields", () => { const minimalTransaction: Transaction = { - id: 'minimal-transaction', - title: 'Minimal', + id: "minimal-transaction", + title: "Minimal", amount: 1000, - date: '2024-01-01', - category: 'Other', - type: 'expense', + date: "2024-01-01", + category: "Other", + type: "expense", // paymentMethod is optional }; render(); - expect(screen.getByText('Minimal')).toBeInTheDocument(); - expect(screen.getByText('2024-01-01')).toBeInTheDocument(); - expect(screen.getByText('1000์›')).toBeInTheDocument(); + expect(screen.getByText("Minimal")).toBeInTheDocument(); + expect(screen.getByText("2024-01-01")).toBeInTheDocument(); + expect(screen.getByText("1000์›")).toBeInTheDocument(); }); - it('handles very long transaction titles', () => { + it("handles very long transaction titles", () => { const longTitleTransaction: Transaction = { ...mockTransaction, - title: 'This is a very long transaction title that might overflow the container and cause layout issues', + title: + "This is a very long transaction title that might overflow the container and cause layout issues", }; render(); @@ -292,7 +324,7 @@ describe('TransactionCard', () => { expect(screen.getByText(longTitleTransaction.title)).toBeInTheDocument(); }); - it('handles zero and negative amounts', () => { + it("handles zero and negative amounts", () => { const zeroAmountTransaction: Transaction = { ...mockTransaction, amount: 0, @@ -303,11 +335,13 @@ describe('TransactionCard', () => { amount: -5000, }; - const { rerender } = render(); - expect(screen.getByText('0์›')).toBeInTheDocument(); + const { rerender } = render( + + ); + expect(screen.getByText("0์›")).toBeInTheDocument(); rerender(); - expect(screen.getByText('-5000์›')).toBeInTheDocument(); + expect(screen.getByText("-5000์›")).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/home/IndexContent.tsx b/src/components/home/IndexContent.tsx index a32419d..a7afd5d 100644 --- a/src/components/home/IndexContent.tsx +++ b/src/components/home/IndexContent.tsx @@ -48,25 +48,33 @@ const IndexContent: React.FC = memo(() => { }, [budgetData]); // ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋“ค ๋ฉ”๋ชจ์ด์ œ์ด์…˜ - const handleTabChange = useCallback((tab: string) => { - setSelectedTab(tab); - }, [setSelectedTab]); + const handleTabChange = useCallback( + (tab: string) => { + setSelectedTab(tab); + }, + [setSelectedTab] + ); - const handleBudgetUpdate = useCallback(( - type: any, - amount: number, - categoryBudgets?: Record - ) => { - handleBudgetGoalUpdate(type, amount, categoryBudgets); - }, [handleBudgetGoalUpdate]); + const handleBudgetUpdate = useCallback( + (type: any, amount: number, categoryBudgets?: Record) => { + handleBudgetGoalUpdate(type, amount, categoryBudgets); + }, + [handleBudgetGoalUpdate] + ); - const handleTransactionUpdate = useCallback((transaction: any) => { - updateTransaction(transaction); - }, [updateTransaction]); + const handleTransactionUpdate = useCallback( + (transaction: any) => { + updateTransaction(transaction); + }, + [updateTransaction] + ); - const handleCategorySpending = useCallback((category: string) => { - return getCategorySpending(category); - }, [getCategorySpending]); + const handleCategorySpending = useCallback( + (category: string) => { + return getCategorySpending(category); + }, + [getCategorySpending] + ); return (
@@ -85,6 +93,6 @@ const IndexContent: React.FC = memo(() => { ); }); -IndexContent.displayName = 'IndexContent'; +IndexContent.displayName = "IndexContent"; export default IndexContent; diff --git a/src/components/offline/OfflineManager.tsx b/src/components/offline/OfflineManager.tsx index 7646d3a..a40c64d 100644 --- a/src/components/offline/OfflineManager.tsx +++ b/src/components/offline/OfflineManager.tsx @@ -1,15 +1,15 @@ /** * ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ - * + * * ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์˜คํ”„๋ผ์ธ ์‹œ ์ ์ ˆํ•œ ๋Œ€์‘์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. */ -import { useState, useEffect } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; -import { toast } from '@/hooks/useToast.wrapper'; -import { syncLogger } from '@/utils/logger'; -import { offlineStrategies } from '@/lib/query/cacheStrategies'; -import { useAppStore } from '@/stores'; +import { useState, useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "@/hooks/useToast.wrapper"; +import { syncLogger } from "@/utils/logger"; +import { offlineStrategies } from "@/lib/query/cacheStrategies"; +import { useAppStore } from "@/stores"; interface OfflineManagerProps { /** ์˜คํ”„๋ผ์ธ ์ƒํƒœ ์•Œ๋ฆผ ํ‘œ์‹œ ์—ฌ๋ถ€ */ @@ -23,7 +23,7 @@ interface OfflineManagerProps { */ export const OfflineManager = ({ showOfflineToast = true, - autoSyncOnReconnect = true + autoSyncOnReconnect = true, }: OfflineManagerProps) => { const [isOnline, setIsOnline] = useState(navigator.onLine); const [wasOffline, setWasOffline] = useState(false); @@ -35,24 +35,25 @@ export const OfflineManager = ({ const handleOnline = () => { setIsOnline(true); setOnlineStatus(true); - - syncLogger.info('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ณต๊ตฌ๋จ'); - + + syncLogger.info("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ณต๊ตฌ๋จ"); + if (wasOffline) { // ์˜คํ”„๋ผ์ธ์—์„œ ์˜จ๋ผ์ธ์œผ๋กœ ๋ณต๊ตฌ๋œ ๊ฒฝ์šฐ if (showOfflineToast) { toast({ title: "์—ฐ๊ฒฐ ๋ณต๊ตฌ", - description: "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋ณต๊ตฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ์ค‘...", + description: + "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋ณต๊ตฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ์ค‘...", }); } if (autoSyncOnReconnect) { // ์—ฐ๊ฒฐ ๋ณต๊ตฌ ์‹œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” setTimeout(() => { - queryClient.refetchQueries({ - type: 'active', - stale: true + queryClient.refetchQueries({ + type: "active", + stale: true, }); }, 1000); // 1์ดˆ ํ›„ ๋ฆฌํŽ˜์น˜ (๋„คํŠธ์›Œํฌ ์•ˆ์ •ํ™” ๋Œ€๊ธฐ) } @@ -65,13 +66,14 @@ export const OfflineManager = ({ setIsOnline(false); setOnlineStatus(false); setWasOffline(true); - - syncLogger.warn('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋Š์–ด์ง'); - + + syncLogger.warn("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋Š์–ด์ง"); + if (showOfflineToast) { toast({ title: "์—ฐ๊ฒฐ ๋Š์–ด์ง", - description: "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์Šต๋‹ˆ๋‹ค. ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.", + description: + "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์Šต๋‹ˆ๋‹ค. ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.", variant: "destructive", }); } @@ -81,44 +83,50 @@ export const OfflineManager = ({ }; // ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ์ง€ ์„ค์ • - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); + window.addEventListener("online", handleOnline); + window.addEventListener("offline", handleOffline); // ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ • setOnlineStatus(navigator.onLine); return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", handleOffline); }; - }, [wasOffline, showOfflineToast, autoSyncOnReconnect, queryClient, setOnlineStatus]); + }, [ + wasOffline, + showOfflineToast, + autoSyncOnReconnect, + queryClient, + setOnlineStatus, + ]); // ์ฃผ๊ธฐ์  ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ (๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฒคํŠธ ๋ณด์™„) useEffect(() => { const checkConnection = async () => { try { // ๊ฐ„๋‹จํ•œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์œผ๋กœ ์‹ค์ œ ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ - const response = await fetch('/api/health', { - method: 'HEAD', - mode: 'no-cors', - cache: 'no-cache' + const response = await fetch("/api/health", { + method: "HEAD", + mode: "no-cors", + cache: "no-cache", }); - - const actuallyOnline = response.ok || response.type === 'opaque'; - + + const actuallyOnline = response.ok || response.type === "opaque"; + if (actuallyOnline !== isOnline) { - syncLogger.info('์‹ค์ œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ์™€ ๊ฐ์ง€๋œ ์ƒํƒœ๊ฐ€ ๋‹ค๋ฆ„', { + syncLogger.info("์‹ค์ œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ์™€ ๊ฐ์ง€๋œ ์ƒํƒœ๊ฐ€ ๋‹ค๋ฆ„", { detected: isOnline, - actual: actuallyOnline + actual: actuallyOnline, }); - + setIsOnline(actuallyOnline); setOnlineStatus(actuallyOnline); } } catch (error) { // ์š”์ฒญ ์‹คํŒจ ์‹œ ์˜คํ”„๋ผ์ธ์œผ๋กœ ๊ฐ„์ฃผ if (isOnline) { - syncLogger.warn('๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ - ์˜คํ”„๋ผ์ธ์œผ๋กœ ๊ฐ„์ฃผ'); + syncLogger.warn("๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ - ์˜คํ”„๋ผ์ธ์œผ๋กœ ๊ฐ„์ฃผ"); setIsOnline(false); setOnlineStatus(false); setWasOffline(true); @@ -151,7 +159,7 @@ export const OfflineManager = ({ queryClient.setDefaultOptions({ queries: { retry: (failureCount, error: any) => { - if (error?.code === 'NETWORK_ERROR' || error?.status >= 500) { + if (error?.code === "NETWORK_ERROR" || error?.status >= 500) { return failureCount < 3; } return false; @@ -161,7 +169,7 @@ export const OfflineManager = ({ }, mutations: { retry: (failureCount, error: any) => { - if (error?.code === 'NETWORK_ERROR') { + if (error?.code === "NETWORK_ERROR") { return failureCount < 2; } return false; @@ -174,17 +182,21 @@ export const OfflineManager = ({ // ์žฅ์‹œ๊ฐ„ ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ฐ์ง€ useEffect(() => { if (!isOnline) { - const longOfflineTimer = setTimeout(() => { - syncLogger.warn('์žฅ์‹œ๊ฐ„ ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ฐ์ง€'); - - if (showOfflineToast) { - toast({ - title: "์žฅ์‹œ๊ฐ„ ์˜คํ”„๋ผ์ธ", - description: "์—ฐ๊ฒฐ์ด ์˜ค๋žซ๋™์•ˆ ๋Š์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive", - }); - } - }, 5 * 60 * 1000); // 5๋ถ„ ํ›„ + const longOfflineTimer = setTimeout( + () => { + syncLogger.warn("์žฅ์‹œ๊ฐ„ ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ฐ์ง€"); + + if (showOfflineToast) { + toast({ + title: "์žฅ์‹œ๊ฐ„ ์˜คํ”„๋ผ์ธ", + description: + "์—ฐ๊ฒฐ์ด ์˜ค๋žซ๋™์•ˆ ๋Š์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + }, + 5 * 60 * 1000 + ); // 5๋ถ„ ํ›„ return () => clearTimeout(longOfflineTimer); } @@ -194,4 +206,4 @@ export const OfflineManager = ({ return null; }; -export default OfflineManager; \ No newline at end of file +export default OfflineManager; diff --git a/src/components/query/QueryCacheManager.tsx b/src/components/query/QueryCacheManager.tsx index 436cb38..9ecb26f 100644 --- a/src/components/query/QueryCacheManager.tsx +++ b/src/components/query/QueryCacheManager.tsx @@ -1,14 +1,18 @@ /** * React Query ์บ์‹œ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ - * + * * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์˜ ์บ์‹œ ์ „๋žต์„ ๊ด€๋ฆฌํ•˜๊ณ  ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. */ -import { useEffect } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; -import { autoCacheManagement, offlineStrategies, cacheOptimization } from '@/lib/query/cacheStrategies'; -import { useAuthStore } from '@/stores'; -import { syncLogger } from '@/utils/logger'; +import { useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + autoCacheManagement, + offlineStrategies, + cacheOptimization, +} from "@/lib/query/cacheStrategies"; +import { useAuthStore } from "@/stores"; +import { syncLogger } from "@/utils/logger"; interface QueryCacheManagerProps { /** ์ฃผ๊ธฐ์  ์บ์‹œ ์ •๋ฆฌ ๊ฐ„๊ฒฉ (๋ถ„) */ @@ -25,14 +29,14 @@ interface QueryCacheManagerProps { export const QueryCacheManager = ({ cleanupIntervalMinutes = 30, enableOfflineCache = true, - enableCacheAnalysis = import.meta.env.DEV + enableCacheAnalysis = import.meta.env.DEV, }: QueryCacheManagerProps) => { const queryClient = useQueryClient(); const { user, session } = useAuthStore(); // ์บ์‹œ ๊ด€๋ฆฌ ์ดˆ๊ธฐํ™” useEffect(() => { - syncLogger.info('React Query ์บ์‹œ ๊ด€๋ฆฌ ์ดˆ๊ธฐํ™” ์‹œ์ž‘'); + syncLogger.info("React Query ์บ์‹œ ๊ด€๋ฆฌ ์ดˆ๊ธฐํ™” ์‹œ์ž‘"); // ๋ธŒ๋ผ์šฐ์ € ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์„ค์ • autoCacheManagement.setupBrowserEventHandlers(); @@ -43,20 +47,25 @@ export const QueryCacheManager = ({ } // ์ฃผ๊ธฐ์  ์บ์‹œ ์ •๋ฆฌ ์‹œ์ž‘ - const cleanupInterval = autoCacheManagement.startPeriodicCleanup(cleanupIntervalMinutes); + const cleanupInterval = autoCacheManagement.startPeriodicCleanup( + cleanupIntervalMinutes + ); // ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์บ์‹œ ๋ถ„์„ let analysisInterval: NodeJS.Timeout | null = null; if (enableCacheAnalysis) { - analysisInterval = setInterval(() => { - cacheOptimization.analyzeCacheHitRate(); - }, 5 * 60 * 1000); // 5๋ถ„๋งˆ๋‹ค ๋ถ„์„ + analysisInterval = setInterval( + () => { + cacheOptimization.analyzeCacheHitRate(); + }, + 5 * 60 * 1000 + ); // 5๋ถ„๋งˆ๋‹ค ๋ถ„์„ } - syncLogger.info('React Query ์บ์‹œ ๊ด€๋ฆฌ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ', { + syncLogger.info("React Query ์บ์‹œ ๊ด€๋ฆฌ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ", { cleanupIntervalMinutes, enableOfflineCache, - enableCacheAnalysis + enableCacheAnalysis, }); // ์ •๋ฆฌ ํ•จ์ˆ˜ @@ -65,13 +74,13 @@ export const QueryCacheManager = ({ if (analysisInterval) { clearInterval(analysisInterval); } - + // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ ์‹œ ์ตœ์ข… ์˜คํ”„๋ผ์ธ ์บ์‹œ ์ €์žฅ if (enableOfflineCache) { offlineStrategies.cacheForOffline(); } - - syncLogger.info('React Query ์บ์‹œ ๊ด€๋ฆฌ ์ •๋ฆฌ ์™„๋ฃŒ'); + + syncLogger.info("React Query ์บ์‹œ ๊ด€๋ฆฌ ์ •๋ฆฌ ์™„๋ฃŒ"); }; }, [cleanupIntervalMinutes, enableOfflineCache, enableCacheAnalysis]); @@ -80,38 +89,41 @@ export const QueryCacheManager = ({ if (!user || !session) { // ๋กœ๊ทธ์•„์›ƒ ์‹œ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ์บ์‹œ ์ •๋ฆฌ queryClient.clear(); - syncLogger.info('๋กœ๊ทธ์•„์›ƒ์œผ๋กœ ์ธํ•œ ์บ์‹œ ์ „์ฒด ์ •๋ฆฌ'); + syncLogger.info("๋กœ๊ทธ์•„์›ƒ์œผ๋กœ ์ธํ•œ ์บ์‹œ ์ „์ฒด ์ •๋ฆฌ"); } }, [user, session, queryClient]); // ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ• ์ƒํ™ฉ ๊ฐ์ง€ ๋ฐ ๋Œ€์‘ useEffect(() => { const handleMemoryPressure = () => { - syncLogger.warn('๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ• ๊ฐ์ง€ - ์บ์‹œ ์ตœ์ ํ™” ์‹คํ–‰'); + syncLogger.warn("๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ• ๊ฐ์ง€ - ์บ์‹œ ์ตœ์ ํ™” ์‹คํ–‰"); cacheOptimization.optimizeMemoryUsage(); }; // Performance Observer๋ฅผ ํ†ตํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋‹ˆํ„ฐ๋ง (์ง€์›๋˜๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ) - if ('PerformanceObserver' in window) { + if ("PerformanceObserver" in window) { try { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach((entry) => { // ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ จ ์„ฑ๋Šฅ ์ง€ํ‘œ ํ™•์ธ - if (entry.entryType === 'memory') { + if (entry.entryType === "memory") { const memoryEntry = entry as any; - if (memoryEntry.usedJSHeapSize > memoryEntry.totalJSHeapSize * 0.9) { + if ( + memoryEntry.usedJSHeapSize > + memoryEntry.totalJSHeapSize * 0.9 + ) { handleMemoryPressure(); } } }); }); - - observer.observe({ entryTypes: ['memory'] }); - + + observer.observe({ entryTypes: ["memory"] }); + return () => observer.disconnect(); } catch (error) { - syncLogger.warn('Performance Observer ์„ค์ • ์‹คํŒจ', error); + syncLogger.warn("Performance Observer ์„ค์ • ์‹คํŒจ", error); } } }, []); @@ -120,28 +132,28 @@ export const QueryCacheManager = ({ useEffect(() => { const updateCacheStrategy = () => { const isOnline = navigator.onLine; - + if (isOnline) { // ์˜จ๋ผ์ธ ์ƒํƒœ: ์ ๊ทน์ ์ธ ์บ์‹œ ๋ฌดํšจํ™” - syncLogger.info('์˜จ๋ผ์ธ ์ƒํƒœ - ์ ๊ทน์  ์บ์‹œ ์ „๋žต ํ™œ์„ฑํ™”'); + syncLogger.info("์˜จ๋ผ์ธ ์ƒํƒœ - ์ ๊ทน์  ์บ์‹œ ์ „๋žต ํ™œ์„ฑํ™”"); } else { // ์˜คํ”„๋ผ์ธ ์ƒํƒœ: ๋ณด์ˆ˜์ ์ธ ์บ์‹œ ์ „๋žต - syncLogger.info('์˜คํ”„๋ผ์ธ ์ƒํƒœ - ๋ณด์ˆ˜์  ์บ์‹œ ์ „๋žต ํ™œ์„ฑํ™”'); + syncLogger.info("์˜คํ”„๋ผ์ธ ์ƒํƒœ - ๋ณด์ˆ˜์  ์บ์‹œ ์ „๋žต ํ™œ์„ฑํ™”"); if (enableOfflineCache) { offlineStrategies.cacheForOffline(); } } }; - window.addEventListener('online', updateCacheStrategy); - window.addEventListener('offline', updateCacheStrategy); + window.addEventListener("online", updateCacheStrategy); + window.addEventListener("offline", updateCacheStrategy); // ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ • updateCacheStrategy(); return () => { - window.removeEventListener('online', updateCacheStrategy); - window.removeEventListener('offline', updateCacheStrategy); + window.removeEventListener("online", updateCacheStrategy); + window.removeEventListener("offline", updateCacheStrategy); }; }, [enableOfflineCache]); @@ -149,4 +161,4 @@ export const QueryCacheManager = ({ return null; }; -export default QueryCacheManager; \ No newline at end of file +export default QueryCacheManager; diff --git a/src/components/sync/BackgroundSync.tsx b/src/components/sync/BackgroundSync.tsx index 5ac4d81..c7804c4 100644 --- a/src/components/sync/BackgroundSync.tsx +++ b/src/components/sync/BackgroundSync.tsx @@ -1,13 +1,13 @@ /** * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™” ์ปดํฌ๋„ŒํŠธ - * + * * React Query์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜์—ฌ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. */ -import { useEffect } from 'react'; -import { useAutoSyncQuery, useSync } from '@/hooks/query'; -import { useAuthStore } from '@/stores'; -import { syncLogger } from '@/utils/logger'; +import { useEffect } from "react"; +import { useAutoSyncQuery, useSync } from "@/hooks/query"; +import { useAuthStore } from "@/stores"; +import { syncLogger } from "@/utils/logger"; interface BackgroundSyncProps { /** ์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ (๋ถ„) */ @@ -21,14 +21,14 @@ interface BackgroundSyncProps { /** * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™” ์ปดํฌ๋„ŒํŠธ */ -export const BackgroundSync = ({ +export const BackgroundSync = ({ intervalMinutes = 5, syncOnFocus = true, - syncOnOnline = true + syncOnOnline = true, }: BackgroundSyncProps) => { const { user, session } = useAuthStore(); const { triggerBackgroundSync } = useSync(); - + // ์ฃผ๊ธฐ์  ์ž๋™ ๋™๊ธฐํ™” ์„ค์ • useAutoSyncQuery(intervalMinutes); @@ -37,23 +37,23 @@ export const BackgroundSync = ({ if (!syncOnFocus || !user?.id) return; const handleFocus = () => { - syncLogger.info('์œˆ๋„์šฐ ํฌ์ปค์Šค ๊ฐ์ง€ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰'); + syncLogger.info("์œˆ๋„์šฐ ํฌ์ปค์Šค ๊ฐ์ง€ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰"); triggerBackgroundSync(); }; const handleVisibilityChange = () => { if (!document.hidden) { - syncLogger.info('ํŽ˜์ด์ง€ ๊ฐ€์‹œ์„ฑ ๋ณต๊ตฌ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰'); + syncLogger.info("ํŽ˜์ด์ง€ ๊ฐ€์‹œ์„ฑ ๋ณต๊ตฌ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰"); triggerBackgroundSync(); } }; - window.addEventListener('focus', handleFocus); - document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener("focus", handleFocus); + document.addEventListener("visibilitychange", handleVisibilityChange); return () => { - window.removeEventListener('focus', handleFocus); - document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener("focus", handleFocus); + document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [user?.id, syncOnFocus, triggerBackgroundSync]); @@ -62,21 +62,21 @@ export const BackgroundSync = ({ if (!syncOnOnline || !user?.id) return; const handleOnline = () => { - syncLogger.info('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ณต๊ตฌ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰'); + syncLogger.info("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ณต๊ตฌ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰"); triggerBackgroundSync(); }; - window.addEventListener('online', handleOnline); + window.addEventListener("online", handleOnline); return () => { - window.removeEventListener('online', handleOnline); + window.removeEventListener("online", handleOnline); }; }, [user?.id, syncOnOnline, triggerBackgroundSync]); // ์„ธ์…˜ ๋ณ€๊ฒฝ ์‹œ ๋™๊ธฐํ™” useEffect(() => { if (session && user?.id) { - syncLogger.info('์„ธ์…˜ ๋ณ€๊ฒฝ ๊ฐ์ง€ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰'); + syncLogger.info("์„ธ์…˜ ๋ณ€๊ฒฝ ๊ฐ์ง€ - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰"); triggerBackgroundSync(); } }, [session, user?.id, triggerBackgroundSync]); @@ -85,4 +85,4 @@ export const BackgroundSync = ({ return null; }; -export default BackgroundSync; \ No newline at end of file +export default BackgroundSync; diff --git a/src/contexts/budget/utils/__tests__/budgetCalculation.test.ts b/src/contexts/budget/utils/__tests__/budgetCalculation.test.ts index e836bec..8c92372 100644 --- a/src/contexts/budget/utils/__tests__/budgetCalculation.test.ts +++ b/src/contexts/budget/utils/__tests__/budgetCalculation.test.ts @@ -1,10 +1,10 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { calculateUpdatedBudgetData } from '../budgetCalculation'; -import { BudgetData, BudgetPeriod } from '../../types'; -import { getInitialBudgetData } from '../constants'; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { calculateUpdatedBudgetData } from "../budgetCalculation"; +import { BudgetData, BudgetPeriod } from "../../types"; +import { getInitialBudgetData } from "../constants"; // Mock logger to prevent console output during tests -vi.mock('@/utils/logger', () => ({ +vi.mock("@/utils/logger", () => ({ logger: { info: vi.fn(), error: vi.fn(), @@ -12,7 +12,7 @@ vi.mock('@/utils/logger', () => ({ })); // Mock constants -vi.mock('../constants', () => ({ +vi.mock("../constants", () => ({ getInitialBudgetData: vi.fn(() => ({ daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, @@ -20,88 +20,142 @@ vi.mock('../constants', () => ({ })), })); -describe('budgetCalculation', () => { +describe("budgetCalculation", () => { let mockPrevBudgetData: BudgetData; beforeEach(() => { vi.clearAllMocks(); - + mockPrevBudgetData = { daily: { targetAmount: 10000, spentAmount: 5000, remainingAmount: 5000 }, - weekly: { targetAmount: 70000, spentAmount: 30000, remainingAmount: 40000 }, - monthly: { targetAmount: 300000, spentAmount: 100000, remainingAmount: 200000 }, + weekly: { + targetAmount: 70000, + spentAmount: 30000, + remainingAmount: 40000, + }, + monthly: { + targetAmount: 300000, + spentAmount: 100000, + remainingAmount: 200000, + }, }; }); - describe('calculateUpdatedBudgetData', () => { - describe('monthly budget input', () => { - it('calculates weekly and daily budgets from monthly amount', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 300000); + describe("calculateUpdatedBudgetData", () => { + describe("monthly budget input", () => { + it("calculates weekly and daily budgets from monthly amount", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 300000 + ); expect(result.monthly.targetAmount).toBe(300000); expect(result.weekly.targetAmount).toBe(Math.round(300000 / 4.345)); // ~69043 expect(result.daily.targetAmount).toBe(Math.round(300000 / 30)); // 10000 }); - it('preserves existing spent amounts', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 600000); + it("preserves existing spent amounts", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 600000 + ); expect(result.daily.spentAmount).toBe(5000); expect(result.weekly.spentAmount).toBe(30000); expect(result.monthly.spentAmount).toBe(100000); }); - it('calculates remaining amounts correctly', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 500000); + it("calculates remaining amounts correctly", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 500000 + ); const expectedWeekly = Math.round(500000 / 4.345); const expectedDaily = Math.round(500000 / 30); - expect(result.daily.remainingAmount).toBe(Math.max(0, expectedDaily - 5000)); - expect(result.weekly.remainingAmount).toBe(Math.max(0, expectedWeekly - 30000)); - expect(result.monthly.remainingAmount).toBe(Math.max(0, 500000 - 100000)); + expect(result.daily.remainingAmount).toBe( + Math.max(0, expectedDaily - 5000) + ); + expect(result.weekly.remainingAmount).toBe( + Math.max(0, expectedWeekly - 30000) + ); + expect(result.monthly.remainingAmount).toBe( + Math.max(0, 500000 - 100000) + ); }); }); - describe('weekly budget input', () => { - it('converts weekly amount to monthly and calculates others', () => { + describe("weekly budget input", () => { + it("converts weekly amount to monthly and calculates others", () => { const weeklyAmount = 80000; const expectedMonthly = Math.round(weeklyAmount * 4.345); // ~347600 - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'weekly', weeklyAmount); + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "weekly", + weeklyAmount + ); expect(result.monthly.targetAmount).toBe(expectedMonthly); - expect(result.weekly.targetAmount).toBe(Math.round(expectedMonthly / 4.345)); - expect(result.daily.targetAmount).toBe(Math.round(expectedMonthly / 30)); + expect(result.weekly.targetAmount).toBe( + Math.round(expectedMonthly / 4.345) + ); + expect(result.daily.targetAmount).toBe( + Math.round(expectedMonthly / 30) + ); }); - it('handles edge case weekly amounts', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'weekly', 1); + it("handles edge case weekly amounts", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "weekly", + 1 + ); expect(result.monthly.targetAmount).toBe(Math.round(1 * 4.345)); }); }); - describe('daily budget input', () => { - it('converts daily amount to monthly and calculates others', () => { + describe("daily budget input", () => { + it("converts daily amount to monthly and calculates others", () => { const dailyAmount = 15000; const expectedMonthly = Math.round(dailyAmount * 30); // 450000 - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'daily', dailyAmount); + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "daily", + dailyAmount + ); expect(result.monthly.targetAmount).toBe(expectedMonthly); - expect(result.weekly.targetAmount).toBe(Math.round(expectedMonthly / 4.345)); - expect(result.daily.targetAmount).toBe(Math.round(expectedMonthly / 30)); + expect(result.weekly.targetAmount).toBe( + Math.round(expectedMonthly / 4.345) + ); + expect(result.daily.targetAmount).toBe( + Math.round(expectedMonthly / 30) + ); }); - it('handles edge case daily amounts', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'daily', 1); + it("handles edge case daily amounts", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "daily", + 1 + ); expect(result.monthly.targetAmount).toBe(30); }); }); - describe('edge cases and error handling', () => { - it('handles null/undefined previous budget data', () => { - const result = calculateUpdatedBudgetData(null as any, 'monthly', 300000); + describe("edge cases and error handling", () => { + it("handles null/undefined previous budget data", () => { + const result = calculateUpdatedBudgetData( + null as any, + "monthly", + 300000 + ); expect(getInitialBudgetData).toHaveBeenCalled(); expect(result.monthly.targetAmount).toBe(300000); @@ -110,8 +164,12 @@ describe('budgetCalculation', () => { expect(result.monthly.spentAmount).toBe(0); }); - it('handles zero amount input', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 0); + it("handles zero amount input", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 0 + ); expect(result.monthly.targetAmount).toBe(0); expect(result.weekly.targetAmount).toBe(0); @@ -121,14 +179,30 @@ describe('budgetCalculation', () => { expect(result.monthly.remainingAmount).toBe(0); }); - it('handles negative remaining amounts (when spent > target)', () => { + it("handles negative remaining amounts (when spent > target)", () => { const highSpentBudgetData: BudgetData = { - daily: { targetAmount: 10000, spentAmount: 15000, remainingAmount: -5000 }, - weekly: { targetAmount: 70000, spentAmount: 80000, remainingAmount: -10000 }, - monthly: { targetAmount: 300000, spentAmount: 350000, remainingAmount: -50000 }, + daily: { + targetAmount: 10000, + spentAmount: 15000, + remainingAmount: -5000, + }, + weekly: { + targetAmount: 70000, + spentAmount: 80000, + remainingAmount: -10000, + }, + monthly: { + targetAmount: 300000, + spentAmount: 350000, + remainingAmount: -50000, + }, }; - const result = calculateUpdatedBudgetData(highSpentBudgetData, 'monthly', 100000); + const result = calculateUpdatedBudgetData( + highSpentBudgetData, + "monthly", + 100000 + ); // remainingAmount should never be negative (Math.max with 0) expect(result.daily.remainingAmount).toBe(0); @@ -136,23 +210,33 @@ describe('budgetCalculation', () => { expect(result.monthly.remainingAmount).toBe(0); }); - it('handles very large amounts', () => { + it("handles very large amounts", () => { const largeAmount = 10000000; // 10 million - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', largeAmount); + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + largeAmount + ); expect(result.monthly.targetAmount).toBe(largeAmount); - expect(result.weekly.targetAmount).toBe(Math.round(largeAmount / 4.345)); + expect(result.weekly.targetAmount).toBe( + Math.round(largeAmount / 4.345) + ); expect(result.daily.targetAmount).toBe(Math.round(largeAmount / 30)); }); - it('handles missing spent amounts in previous data', () => { + it("handles missing spent amounts in previous data", () => { const incompleteBudgetData = { daily: { targetAmount: 10000 } as any, weekly: { targetAmount: 70000, spentAmount: undefined } as any, monthly: { targetAmount: 300000, spentAmount: null } as any, }; - const result = calculateUpdatedBudgetData(incompleteBudgetData, 'monthly', 400000); + const result = calculateUpdatedBudgetData( + incompleteBudgetData, + "monthly", + 400000 + ); expect(result.daily.spentAmount).toBe(0); expect(result.weekly.spentAmount).toBe(0); @@ -160,10 +244,14 @@ describe('budgetCalculation', () => { }); }); - describe('calculation accuracy', () => { - it('maintains reasonable accuracy in conversions', () => { + describe("calculation accuracy", () => { + it("maintains reasonable accuracy in conversions", () => { const monthlyAmount = 435000; // Amount that should convert cleanly - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', monthlyAmount); + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + monthlyAmount + ); const expectedWeekly = Math.round(monthlyAmount / 4.345); const expectedDaily = Math.round(monthlyAmount / 30); @@ -172,13 +260,21 @@ describe('budgetCalculation', () => { const backToMonthlyFromWeekly = Math.round(expectedWeekly * 4.345); const backToMonthlyFromDaily = Math.round(expectedDaily * 30); - expect(Math.abs(backToMonthlyFromWeekly - monthlyAmount)).toBeLessThan(100); - expect(Math.abs(backToMonthlyFromDaily - monthlyAmount)).toBeLessThan(100); + expect(Math.abs(backToMonthlyFromWeekly - monthlyAmount)).toBeLessThan( + 100 + ); + expect(Math.abs(backToMonthlyFromDaily - monthlyAmount)).toBeLessThan( + 100 + ); }); - it('handles rounding consistently', () => { + it("handles rounding consistently", () => { // Test with amount that would create decimals - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'weekly', 77777); + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "weekly", + 77777 + ); expect(Number.isInteger(result.monthly.targetAmount)).toBe(true); expect(Number.isInteger(result.weekly.targetAmount)).toBe(true); @@ -186,47 +282,81 @@ describe('budgetCalculation', () => { }); }); - describe('budget period conversion consistency', () => { - it('maintains consistency across different input types for same monthly equivalent', () => { + describe("budget period conversion consistency", () => { + it("maintains consistency across different input types for same monthly equivalent", () => { const monthlyAmount = 300000; const weeklyEquivalent = Math.round(monthlyAmount / 4.345); // ~69043 const dailyEquivalent = Math.round(monthlyAmount / 30); // 10000 - const fromMonthly = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', monthlyAmount); - const fromWeekly = calculateUpdatedBudgetData(mockPrevBudgetData, 'weekly', weeklyEquivalent); - const fromDaily = calculateUpdatedBudgetData(mockPrevBudgetData, 'daily', dailyEquivalent); + const fromMonthly = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + monthlyAmount + ); + const fromWeekly = calculateUpdatedBudgetData( + mockPrevBudgetData, + "weekly", + weeklyEquivalent + ); + const fromDaily = calculateUpdatedBudgetData( + mockPrevBudgetData, + "daily", + dailyEquivalent + ); // All should result in similar monthly amounts (within rounding tolerance) - expect(Math.abs(fromMonthly.monthly.targetAmount - fromWeekly.monthly.targetAmount)).toBeLessThan(100); - expect(Math.abs(fromMonthly.monthly.targetAmount - fromDaily.monthly.targetAmount)).toBeLessThan(100); + expect( + Math.abs( + fromMonthly.monthly.targetAmount - fromWeekly.monthly.targetAmount + ) + ).toBeLessThan(100); + expect( + Math.abs( + fromMonthly.monthly.targetAmount - fromDaily.monthly.targetAmount + ) + ).toBeLessThan(100); }); }); - describe('data structure integrity', () => { - it('returns complete budget data structure', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 300000); + describe("data structure integrity", () => { + it("returns complete budget data structure", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 300000 + ); - expect(result).toHaveProperty('daily'); - expect(result).toHaveProperty('weekly'); - expect(result).toHaveProperty('monthly'); + expect(result).toHaveProperty("daily"); + expect(result).toHaveProperty("weekly"); + expect(result).toHaveProperty("monthly"); - ['daily', 'weekly', 'monthly'].forEach(period => { - expect(result[period as keyof BudgetData]).toHaveProperty('targetAmount'); - expect(result[period as keyof BudgetData]).toHaveProperty('spentAmount'); - expect(result[period as keyof BudgetData]).toHaveProperty('remainingAmount'); + ["daily", "weekly", "monthly"].forEach((period) => { + expect(result[period as keyof BudgetData]).toHaveProperty( + "targetAmount" + ); + expect(result[period as keyof BudgetData]).toHaveProperty( + "spentAmount" + ); + expect(result[period as keyof BudgetData]).toHaveProperty( + "remainingAmount" + ); }); }); - it('preserves data types correctly', () => { - const result = calculateUpdatedBudgetData(mockPrevBudgetData, 'monthly', 300000); + it("preserves data types correctly", () => { + const result = calculateUpdatedBudgetData( + mockPrevBudgetData, + "monthly", + 300000 + ); - ['daily', 'weekly', 'monthly'].forEach(period => { + ["daily", "weekly", "monthly"].forEach((period) => { const periodData = result[period as keyof BudgetData]; - expect(typeof periodData.targetAmount).toBe('number'); - expect(typeof periodData.spentAmount).toBe('number'); - expect(typeof periodData.remainingAmount).toBe('number'); + expect(typeof periodData.targetAmount).toBe("number"); + expect(typeof periodData.spentAmount).toBe("number"); + expect(typeof periodData.remainingAmount).toBe("number"); }); }); }); }); -}); \ No newline at end of file +}); diff --git a/src/hooks/query/index.ts b/src/hooks/query/index.ts index 3cdbfa5..87d2af3 100644 --- a/src/hooks/query/index.ts +++ b/src/hooks/query/index.ts @@ -1,6 +1,6 @@ /** * React Query ํ›… ํ†ตํ•ฉ ๋‚ด๋ณด๋‚ด๊ธฐ - * + * * ๋ชจ๋“  React Query ํ›…๋“ค์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. */ @@ -13,7 +13,7 @@ export { useSignOutMutation, useResetPasswordMutation, useAuth, -} from './useAuthQueries'; +} from "./useAuthQueries"; // ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ํ›…๋“ค export { @@ -24,7 +24,7 @@ export { useDeleteTransactionMutation, useTransactions, useTransactionStatsQuery, -} from './useTransactionQueries'; +} from "./useTransactionQueries"; // ๋™๊ธฐํ™” ๊ด€๋ จ ํ›…๋“ค export { @@ -35,7 +35,7 @@ export { useAutoSyncQuery, useSync, useSyncSettings, -} from './useSyncQueries'; +} from "./useSyncQueries"; // ์ฟผ๋ฆฌ ํด๋ผ์ด์–ธํŠธ ์„ค์ • (์žฌ๋‚ด๋ณด๋‚ด๊ธฐ) export { @@ -46,4 +46,4 @@ export { invalidateQueries, prefetchQueries, isDevMode, -} from '@/lib/query/queryClient'; \ No newline at end of file +} from "@/lib/query/queryClient"; diff --git a/src/hooks/query/useAuthQueries.ts b/src/hooks/query/useAuthQueries.ts index 9b572c2..121f33d 100644 --- a/src/hooks/query/useAuthQueries.ts +++ b/src/hooks/query/useAuthQueries.ts @@ -1,60 +1,71 @@ /** * ์ธ์ฆ ๊ด€๋ จ React Query ํ›…๋“ค - * + * * ๊ธฐ์กด Zustand ์Šคํ† ์–ด์˜ ์ธ์ฆ ๋กœ์ง์„ React Query๋กœ ์ „ํ™˜ํ•˜์—ฌ * ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. */ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { - getCurrentUser, - createSession, - createAccount, - deleteCurrentSession, - sendPasswordRecoveryEmail -} from '@/lib/appwrite/setup'; -import { queryKeys, queryConfigs, handleQueryError } from '@/lib/query/queryClient'; -import { authLogger } from '@/utils/logger'; -import { useAuthStore } from '@/stores'; -import type { AuthResponse, SignUpResponse, ResetPasswordResponse } from '@/contexts/auth/types'; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + getCurrentUser, + createSession, + createAccount, + deleteCurrentSession, + sendPasswordRecoveryEmail, +} from "@/lib/appwrite/setup"; +import { + queryKeys, + queryConfigs, + handleQueryError, +} from "@/lib/query/queryClient"; +import { authLogger } from "@/utils/logger"; +import { useAuthStore } from "@/stores"; +import type { + AuthResponse, + SignUpResponse, + ResetPasswordResponse, +} from "@/contexts/auth/types"; /** * ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์ฟผ๋ฆฌ - * + * * - ์ž๋™ ์บ์‹ฑ ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” * - ์œˆ๋„์šฐ ํฌ์ปค์Šค ์‹œ ์ž๋™ refetch * - ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ */ export const useUserQuery = () => { const { session } = useAuthStore(); - + return useQuery({ queryKey: queryKeys.auth.user(), queryFn: async () => { - authLogger.info('์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘'); + authLogger.info("์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘"); const result = await getCurrentUser(); - + if (result.error) { throw new Error(result.error.message); } - - authLogger.info('์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต', { userId: result.user?.$id }); + + authLogger.info("์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต", { userId: result.user?.$id }); return result; }, ...queryConfigs.userInfo, - + // ์„ธ์…˜์ด ์žˆ์„ ๋•Œ๋งŒ ์ฟผ๋ฆฌ ํ™œ์„ฑํ™” enabled: !!session, - + // ์—๋Ÿฌ ์‹œ ๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ๋กœ ์ „ํ™˜ retry: (failureCount, error: any) => { - if (error?.message?.includes('401') || error?.message?.includes('Unauthorized')) { + if ( + error?.message?.includes("401") || + error?.message?.includes("Unauthorized") + ) { // ์ธ์ฆ ์—๋Ÿฌ๋Š” ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Œ return false; } return failureCount < 2; }, - + // ์„ฑ๊ณต ์‹œ Zustand ์Šคํ† ์–ด ์—…๋ฐ์ดํŠธ onSuccess: (data) => { if (data.user) { @@ -64,11 +75,11 @@ export const useUserQuery = () => { useAuthStore.getState().setSession(data.session); } }, - + // ์—๋Ÿฌ ์‹œ ์Šคํ† ์–ด ์ •๋ฆฌ onError: (error: any) => { - authLogger.error('์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:', error); - if (error?.message?.includes('401')) { + authLogger.error("์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:", error); + if (error?.message?.includes("401")) { // 401 ์—๋Ÿฌ ์‹œ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ useAuthStore.getState().setUser(null); useAuthStore.getState().setSession(null); @@ -79,7 +90,7 @@ export const useUserQuery = () => { /** * ์„ธ์…˜ ์ƒํƒœ ์กฐํšŒ ์ฟผ๋ฆฌ (๊ฐ€๋ณ๊ฒŒ ์‚ฌ์šฉ) - * + * * ์‚ฌ์šฉ์ž ์ •๋ณด ์—†์ด ์„ธ์…˜ ์ƒํƒœ๋งŒ ํ™•์ธํ•  ๋•Œ ์‚ฌ์šฉ */ export const useSessionQuery = () => { @@ -90,8 +101,8 @@ export const useSessionQuery = () => { return result.session; }, staleTime: 1 * 60 * 1000, // 1๋ถ„ - gcTime: 5 * 60 * 1000, // 5๋ถ„ - + gcTime: 5 * 60 * 1000, // 5๋ถ„ + // ์—๋Ÿฌ ๋ฌด์‹œ (์„ธ์…˜ ์ฒดํฌ์šฉ) retry: false, refetchOnWindowFocus: false, @@ -100,21 +111,27 @@ export const useSessionQuery = () => { /** * ๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ - * + * * - ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ์ฟผ๋ฆฌ ๋ฌดํšจํ™” * - Zustand ์Šคํ† ์–ด์™€ ๋™๊ธฐํ™” * - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ํ† ์ŠคํŠธ ์•Œ๋ฆผ */ export const useSignInMutation = () => { const queryClient = useQueryClient(); - + return useMutation({ - mutationFn: async ({ email, password }: { email: string; password: string }): Promise => { - authLogger.info('๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { email }); - + mutationFn: async ({ + email, + password, + }: { + email: string; + password: string; + }): Promise => { + authLogger.info("๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { email }); + try { const sessionResult = await createSession(email, password); - + if (sessionResult.error) { return { error: sessionResult.error }; } @@ -122,39 +139,47 @@ export const useSignInMutation = () => { if (sessionResult.session) { // ์„ธ์…˜ ์ƒ์„ฑ ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ const userResult = await getCurrentUser(); - + if (userResult.user && userResult.session) { - authLogger.info('๋กœ๊ทธ์ธ ์„ฑ๊ณต', { userId: userResult.user.$id }); + authLogger.info("๋กœ๊ทธ์ธ ์„ฑ๊ณต", { userId: userResult.user.$id }); return { user: userResult.user, error: null }; } } - return { error: { message: '์„ธ์…˜ ๋˜๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค', code: 'AUTH_ERROR' } }; + return { + error: { + message: "์„ธ์…˜ ๋˜๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", + code: "AUTH_ERROR", + }, + }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : '๋กœ๊ทธ์ธ ์ค‘ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'; - authLogger.error('๋กœ๊ทธ์ธ ์—๋Ÿฌ:', error); - return { error: { message: errorMessage, code: 'UNKNOWN_ERROR' } }; + const errorMessage = + error instanceof Error + ? error.message + : "๋กœ๊ทธ์ธ ์ค‘ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"; + authLogger.error("๋กœ๊ทธ์ธ ์—๋Ÿฌ:", error); + return { error: { message: errorMessage, code: "UNKNOWN_ERROR" } }; } }, - + // ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ onSuccess: (data) => { if (data.user && !data.error) { // Zustand ์Šคํ† ์–ด ์—…๋ฐ์ดํŠธ useAuthStore.getState().setUser(data.user); - + // ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋กœ๋“œ queryClient.invalidateQueries({ queryKey: queryKeys.auth.user() }); queryClient.invalidateQueries({ queryKey: queryKeys.auth.session() }); - - authLogger.info('๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต - ์ฟผ๋ฆฌ ๋ฌดํšจํ™” ์™„๋ฃŒ'); + + authLogger.info("๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต - ์ฟผ๋ฆฌ ๋ฌดํšจํ™” ์™„๋ฃŒ"); } }, - + // ์—๋Ÿฌ ์‹œ ์ฒ˜๋ฆฌ onError: (error: any) => { - const friendlyMessage = handleQueryError(error, '๋กœ๊ทธ์ธ'); - authLogger.error('๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); + const friendlyMessage = handleQueryError(error, "๋กœ๊ทธ์ธ"); + authLogger.error("๋กœ๊ทธ์ธ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); useAuthStore.getState().setError(new Error(friendlyMessage)); }, }); @@ -165,36 +190,42 @@ export const useSignInMutation = () => { */ export const useSignUpMutation = () => { return useMutation({ - mutationFn: async ({ - email, - password, - username - }: { - email: string; - password: string; - username: string; + mutationFn: async ({ + email, + password, + username, + }: { + email: string; + password: string; + username: string; }): Promise => { - authLogger.info('ํšŒ์›๊ฐ€์ž… ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { email, username }); - + authLogger.info("ํšŒ์›๊ฐ€์ž… ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { email, username }); + try { const result = await createAccount(email, password, username); - + if (result.error) { return { error: result.error, user: null }; } - authLogger.info('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', { userId: result.user?.$id }); + authLogger.info("ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", { userId: result.user?.$id }); return { error: null, user: result.user }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'ํšŒ์›๊ฐ€์ž… ์ค‘ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'; - authLogger.error('ํšŒ์›๊ฐ€์ž… ์—๋Ÿฌ:', error); - return { error: { message: errorMessage, code: 'UNKNOWN_ERROR' }, user: null }; + const errorMessage = + error instanceof Error + ? error.message + : "ํšŒ์›๊ฐ€์ž… ์ค‘ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"; + authLogger.error("ํšŒ์›๊ฐ€์ž… ์—๋Ÿฌ:", error); + return { + error: { message: errorMessage, code: "UNKNOWN_ERROR" }, + user: null, + }; } }, - + onError: (error: any) => { - const friendlyMessage = handleQueryError(error, 'ํšŒ์›๊ฐ€์ž…'); - authLogger.error('ํšŒ์›๊ฐ€์ž… ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); + const friendlyMessage = handleQueryError(error, "ํšŒ์›๊ฐ€์ž…"); + authLogger.error("ํšŒ์›๊ฐ€์ž… ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); useAuthStore.getState().setError(new Error(friendlyMessage)); }, }); @@ -205,35 +236,35 @@ export const useSignUpMutation = () => { */ export const useSignOutMutation = () => { const queryClient = useQueryClient(); - + return useMutation({ mutationFn: async (): Promise => { - authLogger.info('๋กœ๊ทธ์•„์›ƒ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘'); + authLogger.info("๋กœ๊ทธ์•„์›ƒ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘"); await deleteCurrentSession(); }, - + // ์„ฑ๊ณต ์‹œ ๋ชจ๋“  ์ธ์ฆ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ onSuccess: () => { // Zustand ์Šคํ† ์–ด ์ •๋ฆฌ useAuthStore.getState().setSession(null); useAuthStore.getState().setUser(null); useAuthStore.getState().setError(null); - + // ๋ชจ๋“  ์ฟผ๋ฆฌ ์บ์‹œ ์ •๋ฆฌ (๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ) queryClient.clear(); - - authLogger.info('๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต - ๋ชจ๋“  ์บ์‹œ ์ •๋ฆฌ ์™„๋ฃŒ'); + + authLogger.info("๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต - ๋ชจ๋“  ์บ์‹œ ์ •๋ฆฌ ์™„๋ฃŒ"); }, - + // ์—๋Ÿฌ ์‹œ์—๋„ ๋กœ์ปฌ ์ƒํƒœ๋Š” ์ •๋ฆฌ onError: (error: any) => { - authLogger.error('๋กœ๊ทธ์•„์›ƒ ์—๋Ÿฌ:', error); - + authLogger.error("๋กœ๊ทธ์•„์›ƒ ์—๋Ÿฌ:", error); + // ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋กœ์ปฌ ์ƒํƒœ๋Š” ์ •๋ฆฌ useAuthStore.getState().setSession(null); useAuthStore.getState().setUser(null); - - const friendlyMessage = handleQueryError(error, '๋กœ๊ทธ์•„์›ƒ'); + + const friendlyMessage = handleQueryError(error, "๋กœ๊ทธ์•„์›ƒ"); useAuthStore.getState().setError(new Error(friendlyMessage)); }, }); @@ -244,28 +275,35 @@ export const useSignOutMutation = () => { */ export const useResetPasswordMutation = () => { return useMutation({ - mutationFn: async ({ email }: { email: string }): Promise => { - authLogger.info('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { email }); - + mutationFn: async ({ + email, + }: { + email: string; + }): Promise => { + authLogger.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { email }); + try { const result = await sendPasswordRecoveryEmail(email); - + if (result.error) { return { error: result.error }; } - authLogger.info('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก ์„ฑ๊ณต'); + authLogger.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก ์„ฑ๊ณต"); return { error: null }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : '๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'; - authLogger.error('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์—๋Ÿฌ:', error); - return { error: { message: errorMessage, code: 'UNKNOWN_ERROR' } }; + const errorMessage = + error instanceof Error + ? error.message + : "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"; + authLogger.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์—๋Ÿฌ:", error); + return { error: { message: errorMessage, code: "UNKNOWN_ERROR" } }; } }, - + onError: (error: any) => { - const friendlyMessage = handleQueryError(error, '๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •'); - authLogger.error('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); + const friendlyMessage = handleQueryError(error, "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •"); + authLogger.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); useAuthStore.getState().setError(new Error(friendlyMessage)); }, }); @@ -273,8 +311,8 @@ export const useResetPasswordMutation = () => { /** * ํ†ตํ•ฉ ์ธ์ฆ ํ›… (๊ธฐ์กด useAuth์™€ ํ˜ธํ™˜์„ฑ ์œ ์ง€) - * - * React Query์™€ Zustand๋ฅผ ์กฐํ•ฉํ•˜์—ฌ + * + * React Query์™€ Zustand๋ฅผ ์กฐํ•ฉํ•˜์—ฌ * ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋“ค์ด ํฐ ๋ณ€๊ฒฝ ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ */ export const useAuth = () => { @@ -284,22 +322,22 @@ export const useAuth = () => { const signUpMutation = useSignUpMutation(); const signOutMutation = useSignOutMutation(); const resetPasswordMutation = useResetPasswordMutation(); - + return { // ์ƒํƒœ (Zustand + React Query ์กฐํ•ฉ) user, session, loading: loading || userQuery.isLoading, error: error || userQuery.error, - appwriteInitialized: useAuthStore(state => state.appwriteInitialized), - + appwriteInitialized: useAuthStore((state) => state.appwriteInitialized), + // ์•ก์…˜ (React Query ๋ฎคํ…Œ์ด์…˜) signIn: signInMutation.mutate, signUp: signUpMutation.mutate, signOut: signOutMutation.mutate, resetPassword: resetPasswordMutation.mutate, reinitializeAppwrite: useAuthStore.getState().reinitializeAppwrite, - + // React Query ์ƒํƒœ (ํ•„์š”์‹œ ์ ‘๊ทผ) queries: { user: userQuery, @@ -309,4 +347,4 @@ export const useAuth = () => { isResettingPassword: resetPasswordMutation.isPending, }, }; -}; \ No newline at end of file +}; diff --git a/src/hooks/query/useSyncQueries.ts b/src/hooks/query/useSyncQueries.ts index d8d6239..8a27f9b 100644 --- a/src/hooks/query/useSyncQueries.ts +++ b/src/hooks/query/useSyncQueries.ts @@ -1,17 +1,26 @@ /** * ๋™๊ธฐํ™” ๊ด€๋ จ React Query ํ›…๋“ค - * + * * ๊ธฐ์กด ๋™๊ธฐํ™” ๋กœ์ง์„ React Query๋กœ ์ „ํ™˜ํ•˜์—ฌ * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”์™€ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. */ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { trySyncAllData, getLastSyncTime, setLastSyncTime } from '@/utils/syncUtils'; -import { queryKeys, queryConfigs, handleQueryError, invalidateQueries } from '@/lib/query/queryClient'; -import { syncLogger } from '@/utils/logger'; -import { useAuthStore } from '@/stores'; -import { toast } from '@/hooks/useToast.wrapper'; -import useNotifications from '@/hooks/useNotifications'; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + trySyncAllData, + getLastSyncTime, + setLastSyncTime, +} from "@/utils/syncUtils"; +import { + queryKeys, + queryConfigs, + handleQueryError, + invalidateQueries, +} from "@/lib/query/queryClient"; +import { syncLogger } from "@/utils/logger"; +import { useAuthStore } from "@/stores"; +import { toast } from "@/hooks/useToast.wrapper"; +import useNotifications from "@/hooks/useNotifications"; /** * ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์กฐํšŒ ์ฟผ๋ฆฌ @@ -21,7 +30,7 @@ export const useLastSyncTimeQuery = () => { queryKey: queryKeys.sync.lastSync(), queryFn: async () => { const lastSyncTime = getLastSyncTime(); - syncLogger.info('๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์กฐํšŒ', { lastSyncTime }); + syncLogger.info("๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์กฐํšŒ", { lastSyncTime }); return lastSyncTime; }, staleTime: 30 * 1000, // 30์ดˆ @@ -34,27 +43,27 @@ export const useLastSyncTimeQuery = () => { */ export const useSyncStatusQuery = () => { const { user } = useAuthStore(); - + return useQuery({ queryKey: queryKeys.sync.status(), queryFn: async () => { if (!user?.id) { return { canSync: false, - reason: '์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.', + reason: "์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", lastSyncTime: null, }; } - + const lastSyncTime = getLastSyncTime(); const now = new Date(); const lastSync = lastSyncTime ? new Date(lastSyncTime) : null; - + // ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™”๋กœ๋ถ€ํ„ฐ ์–ผ๋งˆ๋‚˜ ์‹œ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ๊ณ„์‚ฐ - const timeSinceLastSync = lastSync + const timeSinceLastSync = lastSync ? Math.floor((now.getTime() - lastSync.getTime()) / 1000 / 60) // ๋ถ„ ๋‹จ์œ„ : null; - + return { canSync: true, reason: null, @@ -70,7 +79,7 @@ export const useSyncStatusQuery = () => { /** * ์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ - * + * * - ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ๋™๊ธฐํ™”๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•  ๋•Œ ์‚ฌ์šฉ * - ์„ฑ๊ณต ์‹œ ๋ชจ๋“  ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” * - ์•Œ๋ฆผ ๋ฐ ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ์ œ๊ณต @@ -79,87 +88,81 @@ export const useManualSyncMutation = () => { const queryClient = useQueryClient(); const { user } = useAuthStore(); const { addNotification } = useNotifications(); - + return useMutation({ mutationFn: async (): Promise => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { userId: user.id }); - + + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { userId: user.id }); + // ๋™๊ธฐํ™” ์‹คํ–‰ const result = await trySyncAllData(user.id); - + if (!result.success) { - throw new Error(result.error || '๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error(result.error || "๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - + // ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ const currentTime = new Date().toISOString(); setLastSyncTime(currentTime); - - syncLogger.info('์ˆ˜๋™ ๋™๊ธฐํ™” ์„ฑ๊ณต', { + + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ์„ฑ๊ณต", { syncTime: currentTime, - result + result, }); - + return { ...result, syncTime: currentTime }; }, - + // ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘ ์‹œ onMutate: () => { - syncLogger.info('๋™๊ธฐํ™” ์‹œ์ž‘ ์•Œ๋ฆผ'); - addNotification( - "๋™๊ธฐํ™” ์‹œ์ž‘", - "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค." - ); + syncLogger.info("๋™๊ธฐํ™” ์‹œ์ž‘ ์•Œ๋ฆผ"); + addNotification("๋™๊ธฐํ™” ์‹œ์ž‘", "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); }, - + // ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ onSuccess: (result) => { // ๋ชจ๋“  ์ฟผ๋ฆฌ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋กœ๋“œ invalidateQueries.all(); - + // ๋™๊ธฐํ™” ๊ด€๋ จ ์ฟผ๋ฆฌ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ queryClient.setQueryData(queryKeys.sync.lastSync(), result.syncTime); - + // ์„ฑ๊ณต ์•Œ๋ฆผ toast({ title: "๋™๊ธฐํ™” ์™„๋ฃŒ", description: "๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋™๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); - + addNotification( "๋™๊ธฐํ™” ์™„๋ฃŒ", "๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋™๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค." ); - - syncLogger.info('์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ', result); + + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ", result); }, - + // ์—๋Ÿฌ ์‹œ ์ฒ˜๋ฆฌ onError: (error: any) => { - const friendlyMessage = handleQueryError(error, '๋™๊ธฐํ™”'); - syncLogger.error('์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); - + const friendlyMessage = handleQueryError(error, "๋™๊ธฐํ™”"); + syncLogger.error("์ˆ˜๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); + toast({ title: "๋™๊ธฐํ™” ์‹คํŒจ", description: friendlyMessage, variant: "destructive", }); - - addNotification( - "๋™๊ธฐํ™” ์‹คํŒจ", - friendlyMessage - ); + + addNotification("๋™๊ธฐํ™” ์‹คํŒจ", friendlyMessage); }, }); }; /** * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™” ๋ฎคํ…Œ์ด์…˜ - * + * * - ์กฐ์šฉํ•œ ๋™๊ธฐํ™” (์•Œ๋ฆผ ์—†์Œ) * - ์—๋Ÿฌ ์‹œ์—๋„ ์‚ฌ์šฉ์ž๋ฅผ ๋ฐฉํ•ดํ•˜์ง€ ์•Š์Œ * - ์„ฑ๊ณต ์‹œ์—๋งŒ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ @@ -167,72 +170,75 @@ export const useManualSyncMutation = () => { export const useBackgroundSyncMutation = () => { const queryClient = useQueryClient(); const { user } = useAuthStore(); - + return useMutation({ mutationFn: async (): Promise => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹œ์ž‘', { userId: user.id }); - + + syncLogger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹œ์ž‘", { userId: user.id }); + const result = await trySyncAllData(user.id); - + if (!result.success) { - throw new Error(result.error || '๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error(result.error || "๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - + const currentTime = new Date().toISOString(); setLastSyncTime(currentTime); - - syncLogger.info('๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์„ฑ๊ณต', { - syncTime: currentTime + + syncLogger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์„ฑ๊ณต", { + syncTime: currentTime, }); - + return { ...result, syncTime: currentTime }; }, - + // ์„ฑ๊ณต ์‹œ ์กฐ์šฉํžˆ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ onSuccess: (result) => { // ํŠธ๋žœ์žญ์…˜๊ณผ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋งŒ ์กฐ์šฉํžˆ ์—…๋ฐ์ดํŠธ invalidateQueries.transactions(); invalidateQueries.budget(); - + // ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ queryClient.setQueryData(queryKeys.sync.lastSync(), result.syncTime); - - syncLogger.info('๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์™„๋ฃŒ - ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๋จ'); + + syncLogger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์™„๋ฃŒ - ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๋จ"); }, - + // ์—๋Ÿฌ ์‹œ ์กฐ์šฉํžˆ ๋กœ๊ทธ๋งŒ ๋‚จ๊น€ onError: (error: any) => { - syncLogger.warn('๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํŒจ (์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ๋จ):', error?.message); + syncLogger.warn( + "๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํŒจ (์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ๋จ):", + error?.message + ); }, }); }; /** * ์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ ์„ค์ •์„ ์œ„ํ•œ ์ฟผ๋ฆฌ - * + * * - ์„ค์ •๋œ ๊ฐ„๊ฒฉ์— ๋”ฐ๋ผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰ * - ๋„คํŠธ์›Œํฌ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋™์  ์กฐ์ • */ export const useAutoSyncQuery = (intervalMinutes: number = 5) => { const { user } = useAuthStore(); const backgroundSyncMutation = useBackgroundSyncMutation(); - + return useQuery({ - queryKey: ['auto-sync', intervalMinutes], + queryKey: ["auto-sync", intervalMinutes], queryFn: async () => { if (!user?.id) { return null; } - + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ์‹คํ–‰ if (!backgroundSyncMutation.isPending) { backgroundSyncMutation.mutate(); } - + return new Date().toISOString(); }, enabled: !!user?.id, @@ -245,7 +251,7 @@ export const useAutoSyncQuery = (intervalMinutes: number = 5) => { /** * ํ†ตํ•ฉ ๋™๊ธฐํ™” ํ›… (๊ธฐ์กด useManualSync์™€ ํ˜ธํ™˜์„ฑ ์œ ์ง€) - * + * * React Query ๋ฎคํ…Œ์ด์…˜๊ณผ ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฒฐํ•ฉ */ export const useSync = () => { @@ -254,23 +260,23 @@ export const useSync = () => { const syncStatusQuery = useSyncStatusQuery(); const manualSyncMutation = useManualSyncMutation(); const backgroundSyncMutation = useBackgroundSyncMutation(); - + return { // ๋™๊ธฐํ™” ์ƒํƒœ lastSyncTime: lastSyncQuery.data, syncStatus: syncStatusQuery.data, - + // ์ˆ˜๋™ ๋™๊ธฐํ™” (๊ธฐ์กด handleManualSync์™€ ๋™์ผํ•œ ์ธํ„ฐํŽ˜์ด์Šค) syncing: manualSyncMutation.isPending, handleManualSync: manualSyncMutation.mutate, - + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” backgroundSyncing: backgroundSyncMutation.isPending, triggerBackgroundSync: backgroundSyncMutation.mutate, - + // ๋™๊ธฐํ™” ๊ฐ€๋Šฅ ์—ฌ๋ถ€ canSync: !!user?.id && syncStatusQuery.data?.canSync, - + // ์ฟผ๋ฆฌ ์ œ์–ด refetchSyncStatus: syncStatusQuery.refetch, refetchLastSyncTime: lastSyncQuery.refetch, @@ -282,27 +288,27 @@ export const useSync = () => { */ export const useSyncSettings = () => { const queryClient = useQueryClient(); - + // ์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ ์„ค์ • (localStorage ๊ธฐ๋ฐ˜) const setAutoSyncInterval = (intervalMinutes: number) => { - localStorage.setItem('auto-sync-interval', intervalMinutes.toString()); - + localStorage.setItem("auto-sync-interval", intervalMinutes.toString()); + // ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” - queryClient.invalidateQueries({ - queryKey: ['auto-sync'] + queryClient.invalidateQueries({ + queryKey: ["auto-sync"], }); - - syncLogger.info('์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ ์„ค์ •๋จ', { intervalMinutes }); + + syncLogger.info("์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ ์„ค์ •๋จ", { intervalMinutes }); }; - + const getAutoSyncInterval = (): number => { - const stored = localStorage.getItem('auto-sync-interval'); + const stored = localStorage.getItem("auto-sync-interval"); return stored ? parseInt(stored, 10) : 5; // ๊ธฐ๋ณธ๊ฐ’ 5๋ถ„ }; - + return { setAutoSyncInterval, getAutoSyncInterval, currentInterval: getAutoSyncInterval(), }; -}; \ No newline at end of file +}; diff --git a/src/hooks/query/useTransactionQueries.ts b/src/hooks/query/useTransactionQueries.ts index 297a745..580757d 100644 --- a/src/hooks/query/useTransactionQueries.ts +++ b/src/hooks/query/useTransactionQueries.ts @@ -1,69 +1,74 @@ /** * ๊ฑฐ๋ž˜ ๊ด€๋ จ React Query ํ›…๋“ค - * + * * ๊ธฐ์กด Zustand ์Šคํ† ์–ด์˜ ํŠธ๋žœ์žญ์…˜ ๋กœ์ง์„ React Query๋กœ ์ „ํ™˜ํ•˜์—ฌ * ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ์ตœ์ ํ™”๋œ ์บ์‹ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. */ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { getAllTransactions, saveTransaction, updateExistingTransaction, - deleteTransactionById -} from '@/lib/appwrite/setup'; -import { queryKeys, queryConfigs, handleQueryError, invalidateQueries } from '@/lib/query/queryClient'; -import { syncLogger } from '@/utils/logger'; -import { useAuthStore, useBudgetStore } from '@/stores'; -import type { Transaction } from '@/contexts/budget/types'; -import { toast } from '@/hooks/useToast.wrapper'; + deleteTransactionById, +} from "@/lib/appwrite/setup"; +import { + queryKeys, + queryConfigs, + handleQueryError, + invalidateQueries, +} from "@/lib/query/queryClient"; +import { syncLogger } from "@/utils/logger"; +import { useAuthStore, useBudgetStore } from "@/stores"; +import type { Transaction } from "@/contexts/budget/types"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์ฟผ๋ฆฌ - * + * * - ์‹ค์‹œ๊ฐ„ ์บ์‹ฑ ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” * - ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ ์ง€์› * - ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ */ export const useTransactionsQuery = (filters?: Record) => { const { user } = useAuthStore(); - + return useQuery({ queryKey: queryKeys.transactions.list(filters), queryFn: async () => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘', { userId: user.id, filters }); + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘", { userId: user.id, filters }); const result = await getAllTransactions(user.id); - + if (result.error) { throw new Error(result.error.message); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต', { - count: result.transactions?.length || 0 + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", { + count: result.transactions?.length || 0, }); - + return result.transactions || []; }, ...queryConfigs.transactions, - + // ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ฟผ๋ฆฌ ํ™œ์„ฑํ™” enabled: !!user?.id, - + // ์„ฑ๊ณต ์‹œ Zustand ์Šคํ† ์–ด ๋™๊ธฐํ™” onSuccess: (transactions) => { useBudgetStore.getState().setTransactions(transactions); - syncLogger.info('Zustand ์Šคํ† ์–ด ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™” ์™„๋ฃŒ', { - count: transactions.length + syncLogger.info("Zustand ์Šคํ† ์–ด ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™” ์™„๋ฃŒ", { + count: transactions.length, }); }, - + // ์—๋Ÿฌ ์‹œ ์ฒ˜๋ฆฌ onError: (error: any) => { - syncLogger.error('ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:', error); + syncLogger.error("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); }, }); }; @@ -73,26 +78,28 @@ export const useTransactionsQuery = (filters?: Record) => { */ export const useTransactionQuery = (transactionId: string) => { const { user } = useAuthStore(); - + return useQuery({ queryKey: queryKeys.transactions.detail(transactionId), queryFn: async () => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - + // ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์„ ๊ฐ€์ ธ์™€์„œ ํŠน์ • ID ์ฐพ๊ธฐ const result = await getAllTransactions(user.id); - + if (result.error) { throw new Error(result.error.message); } - - const transaction = result.transactions?.find(t => t.id === transactionId); + + const transaction = result.transactions?.find( + (t) => t.id === transactionId + ); if (!transaction) { - throw new Error('ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); + throw new Error("ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - + return transaction; }, ...queryConfigs.transactions, @@ -102,7 +109,7 @@ export const useTransactionQuery = (transactionId: string) => { /** * ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ - * + * * - ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ง€์› * - ์„ฑ๊ณต ์‹œ ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” * - Zustand ์Šคํ† ์–ด ๋™๊ธฐํ™” @@ -110,48 +117,54 @@ export const useTransactionQuery = (transactionId: string) => { export const useCreateTransactionMutation = () => { const queryClient = useQueryClient(); const { user } = useAuthStore(); - + return useMutation({ - mutationFn: async (transactionData: Omit): Promise => { + mutationFn: async ( + transactionData: Omit + ): Promise => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { amount: transactionData.amount, category: transactionData.category, - type: transactionData.type + type: transactionData.type, }); - + const result = await saveTransaction({ ...transactionData, userId: user.id, }); - + if (result.error) { throw new Error(result.error.message); } - + if (!result.transaction) { - throw new Error('ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error("ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ์„ฑ๊ณต', { + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ์„ฑ๊ณต", { id: result.transaction.id, - amount: result.transaction.amount + amount: result.transaction.amount, }); - + return result.transaction; }, - + // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ onMutate: async (newTransaction) => { // ์ง„ํ–‰ ์ค‘์ธ ์ฟผ๋ฆฌ ์ทจ์†Œ - await queryClient.cancelQueries({ queryKey: queryKeys.transactions.all() }); - + await queryClient.cancelQueries({ + queryKey: queryKeys.transactions.all(), + }); + // ์ด์ „ ๋ฐ์ดํ„ฐ ๋ฐฑ์—… - const previousTransactions = queryClient.getQueryData(queryKeys.transactions.list()) as Transaction[] | undefined; - + const previousTransactions = queryClient.getQueryData( + queryKeys.transactions.list() + ) as Transaction[] | undefined; + // ๋‚™๊ด€์ ์œผ๋กœ ์ƒˆ ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ if (previousTransactions) { const optimisticTransaction: Transaction = { @@ -159,43 +172,46 @@ export const useCreateTransactionMutation = () => { id: `temp-${Date.now()}`, localTimestamp: new Date().toISOString(), }; - - queryClient.setQueryData( - queryKeys.transactions.list(), - [...previousTransactions, optimisticTransaction] - ); - + + queryClient.setQueryData(queryKeys.transactions.list(), [ + ...previousTransactions, + optimisticTransaction, + ]); + // Zustand ์Šคํ† ์–ด์—๋„ ์ฆ‰์‹œ ๋ฐ˜์˜ useBudgetStore.getState().addTransaction(newTransaction); } - + return { previousTransactions }; }, - + // ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ onSuccess: (newTransaction) => { // ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” invalidateQueries.transactions(); - + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ์™„๋ฃŒ", - description: `${newTransaction.type === 'expense' ? '์ง€์ถœ' : '์ˆ˜์ž…'} ${newTransaction.amount.toLocaleString()}์›์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + description: `${newTransaction.type === "expense" ? "์ง€์ถœ" : "์ˆ˜์ž…"} ${newTransaction.amount.toLocaleString()}์›์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, }); - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ'); + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ"); }, - + // ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ onError: (error: any, newTransaction, context) => { // ์ด์ „ ๋ฐ์ดํ„ฐ๋กœ ๋กค๋ฐฑ if (context?.previousTransactions) { - queryClient.setQueryData(queryKeys.transactions.list(), context.previousTransactions); + queryClient.setQueryData( + queryKeys.transactions.list(), + context.previousTransactions + ); } - - const friendlyMessage = handleQueryError(error, 'ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ'); - syncLogger.error('ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); - + + const friendlyMessage = handleQueryError(error, "ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ"); + syncLogger.error("ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); + toast({ title: "ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ ์‹คํŒจ", description: friendlyMessage, @@ -211,80 +227,95 @@ export const useCreateTransactionMutation = () => { export const useUpdateTransactionMutation = () => { const queryClient = useQueryClient(); const { user } = useAuthStore(); - + return useMutation({ - mutationFn: async (updatedTransaction: Transaction): Promise => { + mutationFn: async ( + updatedTransaction: Transaction + ): Promise => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { id: updatedTransaction.id, - amount: updatedTransaction.amount + amount: updatedTransaction.amount, }); - + const result = await updateExistingTransaction(updatedTransaction); - + if (result.error) { throw new Error(result.error.message); } - + if (!result.transaction) { - throw new Error('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต', { - id: result.transaction.id + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต", { + id: result.transaction.id, }); - + return result.transaction; }, - + // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ onMutate: async (updatedTransaction) => { - await queryClient.cancelQueries({ queryKey: queryKeys.transactions.all() }); - - const previousTransactions = queryClient.getQueryData(queryKeys.transactions.list()) as Transaction[] | undefined; - + await queryClient.cancelQueries({ + queryKey: queryKeys.transactions.all(), + }); + + const previousTransactions = queryClient.getQueryData( + queryKeys.transactions.list() + ) as Transaction[] | undefined; + if (previousTransactions) { - const optimisticTransactions = previousTransactions.map(t => - t.id === updatedTransaction.id - ? { ...updatedTransaction, localTimestamp: new Date().toISOString() } + const optimisticTransactions = previousTransactions.map((t) => + t.id === updatedTransaction.id + ? { + ...updatedTransaction, + localTimestamp: new Date().toISOString(), + } : t ); - - queryClient.setQueryData(queryKeys.transactions.list(), optimisticTransactions); - + + queryClient.setQueryData( + queryKeys.transactions.list(), + optimisticTransactions + ); + // Zustand ์Šคํ† ์–ด์—๋„ ์ฆ‰์‹œ ๋ฐ˜์˜ useBudgetStore.getState().updateTransaction(updatedTransaction); } - + return { previousTransactions }; }, - + // ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ onSuccess: (updatedTransaction) => { // ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” invalidateQueries.transactions(); invalidateQueries.transaction(updatedTransaction.id); - + toast({ title: "ํŠธ๋žœ์žญ์…˜ ์ˆ˜์ • ์™„๋ฃŒ", description: "ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ'); + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ"); }, - + // ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ onError: (error: any, updatedTransaction, context) => { if (context?.previousTransactions) { - queryClient.setQueryData(queryKeys.transactions.list(), context.previousTransactions); + queryClient.setQueryData( + queryKeys.transactions.list(), + context.previousTransactions + ); } - - const friendlyMessage = handleQueryError(error, 'ํŠธ๋žœ์žญ์…˜ ์ˆ˜์ •'); - syncLogger.error('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); - + + const friendlyMessage = handleQueryError(error, "ํŠธ๋žœ์žญ์…˜ ์ˆ˜์ •"); + syncLogger.error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); + toast({ title: "ํŠธ๋žœ์žญ์…˜ ์ˆ˜์ • ์‹คํŒจ", description: friendlyMessage, @@ -300,63 +331,75 @@ export const useUpdateTransactionMutation = () => { export const useDeleteTransactionMutation = () => { const queryClient = useQueryClient(); const { user } = useAuthStore(); - + return useMutation({ mutationFn: async (transactionId: string): Promise => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘', { id: transactionId }); - + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์‹œ์ž‘", { id: transactionId }); + const result = await deleteTransactionById(transactionId); - + if (result.error) { throw new Error(result.error.message); } - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์„ฑ๊ณต', { id: transactionId }); + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์„ฑ๊ณต", { id: transactionId }); }, - + // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ onMutate: async (transactionId) => { - await queryClient.cancelQueries({ queryKey: queryKeys.transactions.all() }); - - const previousTransactions = queryClient.getQueryData(queryKeys.transactions.list()) as Transaction[] | undefined; - + await queryClient.cancelQueries({ + queryKey: queryKeys.transactions.all(), + }); + + const previousTransactions = queryClient.getQueryData( + queryKeys.transactions.list() + ) as Transaction[] | undefined; + if (previousTransactions) { - const optimisticTransactions = previousTransactions.filter(t => t.id !== transactionId); - queryClient.setQueryData(queryKeys.transactions.list(), optimisticTransactions); - + const optimisticTransactions = previousTransactions.filter( + (t) => t.id !== transactionId + ); + queryClient.setQueryData( + queryKeys.transactions.list(), + optimisticTransactions + ); + // Zustand ์Šคํ† ์–ด์—๋„ ์ฆ‰์‹œ ๋ฐ˜์˜ useBudgetStore.getState().deleteTransaction(transactionId); } - + return { previousTransactions }; }, - + // ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌ onSuccess: (_, transactionId) => { // ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” invalidateQueries.transactions(); - + toast({ title: "ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์™„๋ฃŒ", description: "ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); - - syncLogger.info('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ'); + + syncLogger.info("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ์™„๋ฃŒ"); }, - + // ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ onError: (error: any, transactionId, context) => { if (context?.previousTransactions) { - queryClient.setQueryData(queryKeys.transactions.list(), context.previousTransactions); + queryClient.setQueryData( + queryKeys.transactions.list(), + context.previousTransactions + ); } - - const friendlyMessage = handleQueryError(error, 'ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ'); - syncLogger.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:', friendlyMessage); - + + const friendlyMessage = handleQueryError(error, "ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ"); + syncLogger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ:", friendlyMessage); + toast({ title: "ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์‹คํŒจ", description: friendlyMessage, @@ -368,8 +411,8 @@ export const useDeleteTransactionMutation = () => { /** * ํ†ตํ•ฉ ํŠธ๋žœ์žญ์…˜ ํ›… (๊ธฐ์กด Zustand ํ›…๊ณผ ํ˜ธํ™˜์„ฑ ์œ ์ง€) - * - * React Query์™€ Zustand๋ฅผ ์กฐํ•ฉํ•˜์—ฌ + * + * React Query์™€ Zustand๋ฅผ ์กฐํ•ฉํ•˜์—ฌ * ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋“ค์ด ํฐ ๋ณ€๊ฒฝ ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ */ export const useTransactions = (filters?: Record) => { @@ -377,30 +420,30 @@ export const useTransactions = (filters?: Record) => { const createMutation = useCreateTransactionMutation(); const updateMutation = useUpdateTransactionMutation(); const deleteMutation = useDeleteTransactionMutation(); - + // Zustand ์Šคํ† ์–ด์˜ ๊ณ„์‚ฐ ํ•จ์ˆ˜๋“ค๋„ ํ•จ๊ป˜ ์ œ๊ณต const { getCategorySpending, getPaymentMethodStats } = useBudgetStore(); - + return { // ๋ฐ์ดํ„ฐ ์ƒํƒœ (React Query) transactions: transactionsQuery.data || [], loading: transactionsQuery.isLoading, error: transactionsQuery.error, - + // CRUD ์•ก์…˜ (React Query ๋ฎคํ…Œ์ด์…˜) addTransaction: createMutation.mutate, updateTransaction: updateMutation.mutate, deleteTransaction: deleteMutation.mutate, - + // ๋ฎคํ…Œ์ด์…˜ ์ƒํƒœ isAdding: createMutation.isPending, isUpdating: updateMutation.isPending, isDeleting: deleteMutation.isPending, - + // ๋ถ„์„ ํ•จ์ˆ˜ (Zustand ์Šคํ† ์–ด) getCategorySpending, getPaymentMethodStats, - + // React Query ์ œ์–ด refetch: transactionsQuery.refetch, isFetching: transactionsQuery.isFetching, @@ -412,33 +455,33 @@ export const useTransactions = (filters?: Record) => { */ export const useTransactionStatsQuery = () => { const { user } = useAuthStore(); - + return useQuery({ queryKey: queryKeys.budget.stats(), queryFn: async () => { if (!user?.id) { - throw new Error('์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'); + throw new Error("์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); } - + const result = await getAllTransactions(user.id); - + if (result.error) { throw new Error(result.error.message); } - + const transactions = result.transactions || []; - + // ํ†ต๊ณ„ ๊ณ„์‚ฐ const totalExpenses = transactions - .filter(t => t.type === 'expense') + .filter((t) => t.type === "expense") .reduce((sum, t) => sum + t.amount, 0); - + const totalIncome = transactions - .filter(t => t.type === 'income') + .filter((t) => t.type === "income") .reduce((sum, t) => sum + t.amount, 0); - + const balance = totalIncome - totalExpenses; - + return { totalExpenses, totalIncome, @@ -449,4 +492,4 @@ export const useTransactionStatsQuery = () => { ...queryConfigs.statistics, enabled: !!user?.id, }); -}; \ No newline at end of file +}; diff --git a/src/hooks/sync/useManualSync.ts b/src/hooks/sync/useManualSync.ts index 750d5f2..a5cebe7 100644 --- a/src/hooks/sync/useManualSync.ts +++ b/src/hooks/sync/useManualSync.ts @@ -3,14 +3,14 @@ import { useSync } from "@/hooks/query"; /** * ์ˆ˜๋™ ๋™๊ธฐํ™” ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… (React Query ๊ธฐ๋ฐ˜) - * + * * ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋‚ด๋ถ€์ ์œผ๋กœ React Query๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. */ export const useManualSync = (user: Models.User | null) => { const { syncing, handleManualSync } = useSync(); - return { - syncing, - handleManualSync + return { + syncing, + handleManualSync, }; }; diff --git a/src/hooks/transactions/__tests__/dateUtils.test.ts b/src/hooks/transactions/__tests__/dateUtils.test.ts index b21fd02..49fca9c 100644 --- a/src/hooks/transactions/__tests__/dateUtils.test.ts +++ b/src/hooks/transactions/__tests__/dateUtils.test.ts @@ -1,25 +1,25 @@ -import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest'; -import { - MONTHS_KR, - isValidMonth, - getCurrentMonth, - getPrevMonth, - getNextMonth, - formatMonthForDisplay -} from '../dateUtils'; +import { describe, expect, it, vi, beforeAll, afterAll } from "vitest"; +import { + MONTHS_KR, + isValidMonth, + getCurrentMonth, + getPrevMonth, + getNextMonth, + formatMonthForDisplay, +} from "../dateUtils"; // Mock logger to prevent console output during tests -vi.mock('@/utils/logger', () => ({ +vi.mock("@/utils/logger", () => ({ logger: { warn: vi.fn(), error: vi.fn(), }, })); -describe('dateUtils', () => { +describe("dateUtils", () => { // Mock current date for consistent testing - const mockDate = new Date('2024-06-15T12:00:00.000Z'); - + const mockDate = new Date("2024-06-15T12:00:00.000Z"); + beforeAll(() => { vi.useFakeTimers(); vi.setSystemTime(mockDate); @@ -29,170 +29,180 @@ describe('dateUtils', () => { vi.useRealTimers(); }); - describe('MONTHS_KR', () => { - it('contains all 12 months in Korean', () => { + describe("MONTHS_KR", () => { + it("contains all 12 months in Korean", () => { expect(MONTHS_KR).toHaveLength(12); - expect(MONTHS_KR[0]).toBe('1์›”'); - expect(MONTHS_KR[11]).toBe('12์›”'); + expect(MONTHS_KR[0]).toBe("1์›”"); + expect(MONTHS_KR[11]).toBe("12์›”"); }); - it('has correct month names', () => { + it("has correct month names", () => { const expectedMonths = [ - '1์›”', '2์›”', '3์›”', '4์›”', '5์›”', '6์›”', - '7์›”', '8์›”', '9์›”', '10์›”', '11์›”', '12์›”' + "1์›”", + "2์›”", + "3์›”", + "4์›”", + "5์›”", + "6์›”", + "7์›”", + "8์›”", + "9์›”", + "10์›”", + "11์›”", + "12์›”", ]; expect(MONTHS_KR).toEqual(expectedMonths); }); }); - describe('isValidMonth', () => { - it('validates correct YYYY-MM format', () => { - expect(isValidMonth('2024-01')).toBe(true); - expect(isValidMonth('2024-12')).toBe(true); - expect(isValidMonth('2023-06')).toBe(true); - expect(isValidMonth('2025-09')).toBe(true); + describe("isValidMonth", () => { + it("validates correct YYYY-MM format", () => { + expect(isValidMonth("2024-01")).toBe(true); + expect(isValidMonth("2024-12")).toBe(true); + expect(isValidMonth("2023-06")).toBe(true); + expect(isValidMonth("2025-09")).toBe(true); }); - it('rejects invalid month numbers', () => { - expect(isValidMonth('2024-00')).toBe(false); - expect(isValidMonth('2024-13')).toBe(false); - expect(isValidMonth('2024-99')).toBe(false); + it("rejects invalid month numbers", () => { + expect(isValidMonth("2024-00")).toBe(false); + expect(isValidMonth("2024-13")).toBe(false); + expect(isValidMonth("2024-99")).toBe(false); }); - it('rejects invalid formats', () => { - expect(isValidMonth('24-01')).toBe(false); - expect(isValidMonth('2024-1')).toBe(false); - expect(isValidMonth('2024/01')).toBe(false); - expect(isValidMonth('2024.01')).toBe(false); - expect(isValidMonth('2024-01-01')).toBe(false); - expect(isValidMonth('')).toBe(false); - expect(isValidMonth('invalid')).toBe(false); + it("rejects invalid formats", () => { + expect(isValidMonth("24-01")).toBe(false); + expect(isValidMonth("2024-1")).toBe(false); + expect(isValidMonth("2024/01")).toBe(false); + expect(isValidMonth("2024.01")).toBe(false); + expect(isValidMonth("2024-01-01")).toBe(false); + expect(isValidMonth("")).toBe(false); + expect(isValidMonth("invalid")).toBe(false); }); - it('handles edge cases', () => { - expect(isValidMonth('0000-01')).toBe(true); // ๊ธฐ์ˆ ์ ์œผ๋กœ valid - expect(isValidMonth('9999-12')).toBe(true); + it("handles edge cases", () => { + expect(isValidMonth("0000-01")).toBe(true); // ๊ธฐ์ˆ ์ ์œผ๋กœ valid + expect(isValidMonth("9999-12")).toBe(true); }); }); - describe('getCurrentMonth', () => { - it('returns current month in YYYY-MM format', () => { - expect(getCurrentMonth()).toBe('2024-06'); + describe("getCurrentMonth", () => { + it("returns current month in YYYY-MM format", () => { + expect(getCurrentMonth()).toBe("2024-06"); }); }); - describe('getPrevMonth', () => { - it('calculates previous month correctly', () => { - expect(getPrevMonth('2024-06')).toBe('2024-05'); - expect(getPrevMonth('2024-03')).toBe('2024-02'); - expect(getPrevMonth('2024-12')).toBe('2024-11'); + describe("getPrevMonth", () => { + it("calculates previous month correctly", () => { + expect(getPrevMonth("2024-06")).toBe("2024-05"); + expect(getPrevMonth("2024-03")).toBe("2024-02"); + expect(getPrevMonth("2024-12")).toBe("2024-11"); }); - it('handles year boundary correctly', () => { - expect(getPrevMonth('2024-01')).toBe('2023-12'); - expect(getPrevMonth('2025-01')).toBe('2024-12'); + it("handles year boundary correctly", () => { + expect(getPrevMonth("2024-01")).toBe("2023-12"); + expect(getPrevMonth("2025-01")).toBe("2024-12"); }); - it('handles invalid input gracefully', () => { - expect(getPrevMonth('invalid')).toBe('2024-06'); // current month fallback - expect(getPrevMonth('')).toBe('2024-06'); - expect(getPrevMonth('2024-13')).toBe('2024-06'); - expect(getPrevMonth('24-01')).toBe('2024-06'); + it("handles invalid input gracefully", () => { + expect(getPrevMonth("invalid")).toBe("2024-06"); // current month fallback + expect(getPrevMonth("")).toBe("2024-06"); + expect(getPrevMonth("2024-13")).toBe("2024-06"); + expect(getPrevMonth("24-01")).toBe("2024-06"); }); - it('handles edge cases', () => { - expect(getPrevMonth('0001-01')).toBe('0001-12'); // date-fns handles year 0 differently - expect(getPrevMonth('2024-00')).toBe('2024-06'); // invalid, returns current + it("handles edge cases", () => { + expect(getPrevMonth("0001-01")).toBe("0001-12"); // date-fns handles year 0 differently + expect(getPrevMonth("2024-00")).toBe("2024-06"); // invalid, returns current }); }); - describe('getNextMonth', () => { - it('calculates next month correctly', () => { - expect(getNextMonth('2024-06')).toBe('2024-07'); - expect(getNextMonth('2024-03')).toBe('2024-04'); - expect(getNextMonth('2024-11')).toBe('2024-12'); + describe("getNextMonth", () => { + it("calculates next month correctly", () => { + expect(getNextMonth("2024-06")).toBe("2024-07"); + expect(getNextMonth("2024-03")).toBe("2024-04"); + expect(getNextMonth("2024-11")).toBe("2024-12"); }); - it('handles year boundary correctly', () => { - expect(getNextMonth('2024-12')).toBe('2025-01'); - expect(getNextMonth('2023-12')).toBe('2024-01'); + it("handles year boundary correctly", () => { + expect(getNextMonth("2024-12")).toBe("2025-01"); + expect(getNextMonth("2023-12")).toBe("2024-01"); }); - it('handles invalid input gracefully', () => { - expect(getNextMonth('invalid')).toBe('2024-06'); // current month fallback - expect(getNextMonth('')).toBe('2024-06'); - expect(getNextMonth('2024-13')).toBe('2024-06'); - expect(getNextMonth('24-01')).toBe('2024-06'); + it("handles invalid input gracefully", () => { + expect(getNextMonth("invalid")).toBe("2024-06"); // current month fallback + expect(getNextMonth("")).toBe("2024-06"); + expect(getNextMonth("2024-13")).toBe("2024-06"); + expect(getNextMonth("24-01")).toBe("2024-06"); }); - it('handles edge cases', () => { - expect(getNextMonth('9999-12')).toBe('10000-01'); // theoretically valid - expect(getNextMonth('2024-00')).toBe('2024-06'); // invalid, returns current + it("handles edge cases", () => { + expect(getNextMonth("9999-12")).toBe("10000-01"); // theoretically valid + expect(getNextMonth("2024-00")).toBe("2024-06"); // invalid, returns current }); }); - describe('formatMonthForDisplay', () => { - it('formats valid months correctly', () => { - expect(formatMonthForDisplay('2024-01')).toBe('2024๋…„ 01์›”'); - expect(formatMonthForDisplay('2024-06')).toBe('2024๋…„ 06์›”'); - expect(formatMonthForDisplay('2024-12')).toBe('2024๋…„ 12์›”'); + describe("formatMonthForDisplay", () => { + it("formats valid months correctly", () => { + expect(formatMonthForDisplay("2024-01")).toBe("2024๋…„ 01์›”"); + expect(formatMonthForDisplay("2024-06")).toBe("2024๋…„ 06์›”"); + expect(formatMonthForDisplay("2024-12")).toBe("2024๋…„ 12์›”"); }); - it('handles different years', () => { - expect(formatMonthForDisplay('2023-03')).toBe('2023๋…„ 03์›”'); - expect(formatMonthForDisplay('2025-09')).toBe('2025๋…„ 09์›”'); + it("handles different years", () => { + expect(formatMonthForDisplay("2023-03")).toBe("2023๋…„ 03์›”"); + expect(formatMonthForDisplay("2025-09")).toBe("2025๋…„ 09์›”"); }); - it('handles invalid input gracefully', () => { + it("handles invalid input gracefully", () => { // Should return current date formatted when invalid input - expect(formatMonthForDisplay('invalid')).toBe('2024๋…„ 06์›”'); - expect(formatMonthForDisplay('')).toBe('2024๋…„ 06์›”'); - expect(formatMonthForDisplay('2024-13')).toBe('2024๋…„ 06์›”'); + expect(formatMonthForDisplay("invalid")).toBe("2024๋…„ 06์›”"); + expect(formatMonthForDisplay("")).toBe("2024๋…„ 06์›”"); + expect(formatMonthForDisplay("2024-13")).toBe("2024๋…„ 06์›”"); }); - it('preserves original format on error', () => { + it("preserves original format on error", () => { // For some edge cases, it might return the original string - const result = formatMonthForDisplay('completely-invalid-format'); + const result = formatMonthForDisplay("completely-invalid-format"); // Could be either the fallback format or the original string - expect(typeof result).toBe('string'); + expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); - it('handles edge case years', () => { - expect(formatMonthForDisplay('0001-01')).toBe('0001๋…„ 01์›”'); - expect(formatMonthForDisplay('9999-12')).toBe('9999๋…„ 12์›”'); + it("handles edge case years", () => { + expect(formatMonthForDisplay("0001-01")).toBe("0001๋…„ 01์›”"); + expect(formatMonthForDisplay("9999-12")).toBe("9999๋…„ 12์›”"); }); }); - describe('month navigation sequences', () => { - it('maintains consistency in forward/backward navigation', () => { - const startMonth = '2024-06'; - + describe("month navigation sequences", () => { + it("maintains consistency in forward/backward navigation", () => { + const startMonth = "2024-06"; + // Forward then backward should return to original const nextMonth = getNextMonth(startMonth); const backToPrev = getPrevMonth(nextMonth); expect(backToPrev).toBe(startMonth); - + // Backward then forward should return to original const prevMonth = getPrevMonth(startMonth); const backToNext = getNextMonth(prevMonth); expect(backToNext).toBe(startMonth); }); - it('handles multiple month navigation', () => { - let month = '2024-01'; - + it("handles multiple month navigation", () => { + let month = "2024-01"; + // Navigate forward 12 months for (let i = 0; i < 12; i++) { month = getNextMonth(month); } - expect(month).toBe('2025-01'); - + expect(month).toBe("2025-01"); + // Navigate backward 12 months for (let i = 0; i < 12; i++) { month = getPrevMonth(month); } - expect(month).toBe('2024-01'); + expect(month).toBe("2024-01"); }); }); -}); \ No newline at end of file +}); diff --git a/src/lib/appwrite/setup.ts b/src/lib/appwrite/setup.ts index 8bacc5e..941af84 100644 --- a/src/lib/appwrite/setup.ts +++ b/src/lib/appwrite/setup.ts @@ -1,6 +1,11 @@ import { ID, Query, Permission, Role, Models } from "appwrite"; import { appwriteLogger } from "@/utils/logger"; -import { databases, account, getInitializationStatus, reinitializeAppwriteClient } from "./client"; +import { + databases, + account, + getInitializationStatus, + reinitializeAppwriteClient, +} from "./client"; import { config } from "./config"; import type { ApiError } from "@/types/common"; @@ -195,12 +200,12 @@ export const createSession = async (email: string, password: string) => { return { session, error: null }; } catch (error: any) { appwriteLogger.error("์„ธ์…˜ ์ƒ์„ฑ ์‹คํŒจ:", error); - return { - session: null, + return { + session: null, error: { message: error.message || "๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "AUTH_ERROR" - } as ApiError + code: error.code || "AUTH_ERROR", + } as ApiError, }; } }; @@ -208,18 +213,22 @@ export const createSession = async (email: string, password: string) => { /** * ๊ณ„์ • ์ƒ์„ฑ (ํšŒ์›๊ฐ€์ž…) */ -export const createAccount = async (email: string, password: string, username: string) => { +export const createAccount = async ( + email: string, + password: string, + username: string +) => { try { const user = await account.create(ID.unique(), email, password, username); return { user, error: null }; } catch (error: any) { appwriteLogger.error("๊ณ„์ • ์ƒ์„ฑ ์‹คํŒจ:", error); - return { - user: null, + return { + user: null, error: { message: error.message || "ํšŒ์›๊ฐ€์ž…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "SIGNUP_ERROR" - } as ApiError + code: error.code || "SIGNUP_ERROR", + } as ApiError, }; } }; @@ -229,7 +238,7 @@ export const createAccount = async (email: string, password: string, username: s */ export const deleteCurrentSession = async () => { try { - await account.deleteSession('current'); + await account.deleteSession("current"); appwriteLogger.info("๋กœ๊ทธ์•„์›ƒ ์™„๋ฃŒ"); } catch (error: any) { appwriteLogger.error("๋กœ๊ทธ์•„์›ƒ ์‹คํŒจ:", error); @@ -243,17 +252,17 @@ export const deleteCurrentSession = async () => { export const getCurrentUser = async () => { try { const user = await account.get(); - const session = await account.getSession('current'); + const session = await account.getSession("current"); return { user, session, error: null }; } catch (error: any) { appwriteLogger.debug("์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", error); - return { - user: null, - session: null, + return { + user: null, + session: null, error: { message: error.message || "์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - code: error.code || "USER_ERROR" - } as ApiError + code: error.code || "USER_ERROR", + } as ApiError, }; } }; @@ -263,15 +272,18 @@ export const getCurrentUser = async () => { */ export const sendPasswordRecoveryEmail = async (email: string) => { try { - await account.createRecovery(email, window.location.origin + "/reset-password"); + await account.createRecovery( + email, + window.location.origin + "/reset-password" + ); return { error: null }; } catch (error: any) { appwriteLogger.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ:", error); - return { + return { error: { message: error.message || "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "RECOVERY_ERROR" - } as ApiError + code: error.code || "RECOVERY_ERROR", + } as ApiError, }; } }; @@ -309,22 +321,22 @@ export const getAllTransactions = async (userId: string) => { userId: doc.user_id, })); - appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", { - count: transactions.length + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", { + count: transactions.length, }); - return { - transactions, - error: null + return { + transactions, + error: null, }; } catch (error: any) { appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); - return { + return { transactions: null, error: { message: error.message || "ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - code: error.code || "FETCH_ERROR" - } as ApiError + code: error.code || "FETCH_ERROR", + } as ApiError, }; } }; @@ -337,9 +349,9 @@ export const saveTransaction = async (transactionData: any) => { const databaseId = config.databaseId; const transactionsCollectionId = config.transactionsCollectionId; - appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์‹œ์ž‘", { + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์‹œ์ž‘", { amount: transactionData.amount, - type: transactionData.type + type: transactionData.type, }); const documentData = { @@ -374,22 +386,22 @@ export const saveTransaction = async (transactionData: any) => { userId: response.user_id, }; - appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์„ฑ๊ณต", { - id: transaction.id + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์„ฑ๊ณต", { + id: transaction.id, }); - return { - transaction, - error: null + return { + transaction, + error: null, }; } catch (error: any) { appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์‹คํŒจ:", error); - return { + return { transaction: null, error: { message: error.message || "ํŠธ๋žœ์žญ์…˜ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "SAVE_ERROR" - } as ApiError + code: error.code || "SAVE_ERROR", + } as ApiError, }; } }; @@ -402,18 +414,15 @@ export const updateExistingTransaction = async (transactionData: any) => { const databaseId = config.databaseId; const transactionsCollectionId = config.transactionsCollectionId; - appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘", { - id: transactionData.id + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘", { + id: transactionData.id, }); // ๋จผ์ € ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜ ๋ฌธ์„œ ์ฐพ๊ธฐ const existingResponse = await databases.listDocuments( databaseId, transactionsCollectionId, - [ - Query.equal("transaction_id", transactionData.id), - Query.limit(1), - ] + [Query.equal("transaction_id", transactionData.id), Query.limit(1)] ); if (existingResponse.documents.length === 0) { @@ -452,22 +461,22 @@ export const updateExistingTransaction = async (transactionData: any) => { userId: response.user_id, }; - appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต", { - id: transaction.id + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต", { + id: transaction.id, }); - return { - transaction, - error: null + return { + transaction, + error: null, }; } catch (error: any) { appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", error); - return { + return { transaction: null, error: { message: error.message || "ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "UPDATE_ERROR" - } as ApiError + code: error.code || "UPDATE_ERROR", + } as ApiError, }; } }; @@ -486,10 +495,7 @@ export const deleteTransactionById = async (transactionId: string) => { const existingResponse = await databases.listDocuments( databaseId, transactionsCollectionId, - [ - Query.equal("transaction_id", transactionId), - Query.limit(1), - ] + [Query.equal("transaction_id", transactionId), Query.limit(1)] ); if (existingResponse.documents.length === 0) { @@ -506,16 +512,16 @@ export const deleteTransactionById = async (transactionId: string) => { appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์„ฑ๊ณต", { id: transactionId }); - return { - error: null + return { + error: null, }; } catch (error: any) { appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์‹คํŒจ:", error); - return { + return { error: { message: error.message || "ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - code: error.code || "DELETE_ERROR" - } as ApiError + code: error.code || "DELETE_ERROR", + } as ApiError, }; } }; diff --git a/src/lib/query/queryClient.ts b/src/lib/query/queryClient.ts index 40506e0..f147c07 100644 --- a/src/lib/query/queryClient.ts +++ b/src/lib/query/queryClient.ts @@ -1,15 +1,15 @@ /** * TanStack Query ์„ค์ • - * + * * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ์‚ฌ์šฉํ•  QueryClient ์„ค์ • ๋ฐ ๊ธฐ๋ณธ ์˜ต์…˜์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. */ -import { QueryClient } from '@tanstack/react-query'; -import { syncLogger } from '@/utils/logger'; +import { QueryClient } from "@tanstack/react-query"; +import { syncLogger } from "@/utils/logger"; /** * QueryClient ๊ธฐ๋ณธ ์„ค์ • - * + * * staleTime: ๋ฐ์ดํ„ฐ๊ฐ€ 'stale' ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜๊ธฐ๊นŒ์ง€์˜ ์‹œ๊ฐ„ * cacheTime: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋œ ํ›„ ์บ์‹œ๊ฐ€ ์œ ์ง€๋˜๋Š” ์‹œ๊ฐ„ * refetchOnWindowFocus: ์œˆ๋„์šฐ ํฌ์ปค์Šค ์‹œ ์ž๋™ refetch ์—ฌ๋ถ€ @@ -21,47 +21,47 @@ export const queryClient = new QueryClient({ queries: { // 5๋ถ„๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ fresh ์ƒํƒœ๋กœ ์œ ์ง€ (์ผ๋ฐ˜์ ์ธ ๊ฑฐ๋ž˜/์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ) staleTime: 5 * 60 * 1000, // 5๋ถ„ - + // 30๋ถ„๊ฐ„ ์บ์‹œ ์œ ์ง€ (๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ๊นŒ์ง€์˜ ์‹œ๊ฐ„) gcTime: 30 * 60 * 1000, // 30๋ถ„ (v5์—์„œ cacheTime โ†’ gcTime์œผ๋กœ ๋ณ€๊ฒฝ) - + // ์œˆ๋„์šฐ ํฌ์ปค์Šค ์‹œ ์ž๋™ refetch (์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ํƒญ์—์„œ ๋Œ์•„์˜ฌ ๋•Œ) refetchOnWindowFocus: true, - + // ๋„คํŠธ์›Œํฌ ์žฌ์—ฐ๊ฒฐ ์‹œ ์ž๋™ refetch refetchOnReconnect: true, - + // ๋งˆ์šดํŠธ ์‹œ stale ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด refetch refetchOnMount: true, - + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch ๊ฐ„๊ฒฉ (5๋ถ„) refetchInterval: 5 * 60 * 1000, - + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ๋„ refetch ๊ณ„์† ์‹คํ–‰ (ํƒญ์ด ๋ณด์ด์ง€ ์•Š์„ ๋•Œ๋„) refetchIntervalInBackground: false, - + // ์žฌ์‹œ๋„ ์„ค์ • (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์‚ฌ์šฉ) retry: (failureCount, error: any) => { // ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ๋‚˜ ์„œ๋ฒ„ ์—๋Ÿฌ์ธ ๊ฒฝ์šฐ์—๋งŒ ์žฌ์‹œ๋„ - if (error?.code === 'NETWORK_ERROR' || error?.status >= 500) { + if (error?.code === "NETWORK_ERROR" || error?.status >= 500) { return failureCount < 3; } // ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ (400๋ฒˆ๋Œ€)๋Š” ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Œ return false; }, - + // ์žฌ์‹œ๋„ ์ง€์—ฐ ์‹œ๊ฐ„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, mutations: { // ๋ฎคํ…Œ์ด์…˜ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ (๋„คํŠธ์›Œํฌ ์—๋Ÿฌ์ธ ๊ฒฝ์šฐ๋งŒ) retry: (failureCount, error: any) => { - if (error?.code === 'NETWORK_ERROR') { + if (error?.code === "NETWORK_ERROR") { return failureCount < 2; } return false; }, - + // ๋ฎคํ…Œ์ด์…˜ ์žฌ์‹œ๋„ ์ง€์—ฐ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), }, @@ -70,39 +70,41 @@ export const queryClient = new QueryClient({ /** * ์ฟผ๋ฆฌ ํ‚ค ํŒฉํ† ๋ฆฌ - * + * * ์ผ๊ด€๋œ ์ฟผ๋ฆฌ ํ‚ค ๋„ค์ด๋ฐ์„ ์œ„ํ•œ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜๋“ค */ export const queryKeys = { // ์ธ์ฆ ๊ด€๋ จ auth: { - user: () => ['auth', 'user'] as const, - session: () => ['auth', 'session'] as const, + user: () => ["auth", "user"] as const, + session: () => ["auth", "session"] as const, }, - + // ๊ฑฐ๋ž˜ ๊ด€๋ จ transactions: { - all: () => ['transactions'] as const, - lists: () => [...queryKeys.transactions.all(), 'list'] as const, - list: (filters?: Record) => [...queryKeys.transactions.lists(), filters] as const, - details: () => [...queryKeys.transactions.all(), 'detail'] as const, + all: () => ["transactions"] as const, + lists: () => [...queryKeys.transactions.all(), "list"] as const, + list: (filters?: Record) => + [...queryKeys.transactions.lists(), filters] as const, + details: () => [...queryKeys.transactions.all(), "detail"] as const, detail: (id: string) => [...queryKeys.transactions.details(), id] as const, }, - + // ์˜ˆ์‚ฐ ๊ด€๋ จ budget: { - all: () => ['budget'] as const, - data: () => [...queryKeys.budget.all(), 'data'] as const, - categories: () => [...queryKeys.budget.all(), 'categories'] as const, - stats: () => [...queryKeys.budget.all(), 'stats'] as const, - paymentMethods: () => [...queryKeys.budget.all(), 'paymentMethods'] as const, + all: () => ["budget"] as const, + data: () => [...queryKeys.budget.all(), "data"] as const, + categories: () => [...queryKeys.budget.all(), "categories"] as const, + stats: () => [...queryKeys.budget.all(), "stats"] as const, + paymentMethods: () => + [...queryKeys.budget.all(), "paymentMethods"] as const, }, - + // ๋™๊ธฐํ™” ๊ด€๋ จ sync: { - all: () => ['sync'] as const, - status: () => [...queryKeys.sync.all(), 'status'] as const, - lastSync: () => [...queryKeys.sync.all(), 'lastSync'] as const, + all: () => ["sync"] as const, + status: () => [...queryKeys.sync.all(), "status"] as const, + lastSync: () => [...queryKeys.sync.all(), "lastSync"] as const, }, } as const; @@ -113,25 +115,25 @@ export const queryConfigs = { // ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด (30๋ถ„ ์บ์‹œ) userInfo: { staleTime: 30 * 60 * 1000, // 30๋ถ„ - gcTime: 60 * 60 * 1000, // 1์‹œ๊ฐ„ + gcTime: 60 * 60 * 1000, // 1์‹œ๊ฐ„ }, - + // ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•œ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ (1๋ถ„ ์บ์‹œ) transactions: { - staleTime: 1 * 60 * 1000, // 1๋ถ„ - gcTime: 10 * 60 * 1000, // 10๋ถ„ + staleTime: 1 * 60 * 1000, // 1๋ถ„ + gcTime: 10 * 60 * 1000, // 10๋ถ„ }, - + // ์ƒ๋Œ€์ ์œผ๋กœ ์ •์ ์ธ ์˜ˆ์‚ฐ ์„ค์ • (10๋ถ„ ์บ์‹œ) budgetSettings: { staleTime: 10 * 60 * 1000, // 10๋ถ„ - gcTime: 30 * 60 * 1000, // 30๋ถ„ + gcTime: 30 * 60 * 1000, // 30๋ถ„ }, - + // ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ (5๋ถ„ ์บ์‹œ, ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์„ ์ˆ˜ ์žˆ์Œ) statistics: { - staleTime: 5 * 60 * 1000, // 5๋ถ„ - gcTime: 15 * 60 * 1000, // 15๋ถ„ + staleTime: 5 * 60 * 1000, // 5๋ถ„ + gcTime: 15 * 60 * 1000, // 15๋ถ„ }, } as const; @@ -139,27 +141,27 @@ export const queryConfigs = { * ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์œ ํ‹ธ๋ฆฌํ‹ฐ */ export const handleQueryError = (error: any, context?: string) => { - const errorMessage = error?.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - const errorCode = error?.code || 'UNKNOWN_ERROR'; - - syncLogger.error(`Query ์—๋Ÿฌ ${context ? `(${context})` : ''}:`, { + const errorMessage = error?.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + const errorCode = error?.code || "UNKNOWN_ERROR"; + + syncLogger.error(`Query ์—๋Ÿฌ ${context ? `(${context})` : ""}:`, { message: errorMessage, code: errorCode, stack: error?.stack, }); - + // ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œํ•  ์นœํ™”์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ switch (errorCode) { - case 'NETWORK_ERROR': - return '๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.'; - case 'AUTH_ERROR': - return '์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.'; - case 'FORBIDDEN': - return '์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.'; - case 'NOT_FOUND': - return '์š”์ฒญํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'; - case 'SERVER_ERROR': - return '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'; + case "NETWORK_ERROR": + return "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; + case "AUTH_ERROR": + return "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”."; + case "FORBIDDEN": + return "์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."; + case "NOT_FOUND": + return "์š”์ฒญํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + case "SERVER_ERROR": + return "์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; default: return errorMessage; } @@ -173,23 +175,25 @@ export const invalidateQueries = { transactions: () => { queryClient.invalidateQueries({ queryKey: queryKeys.transactions.all() }); }, - + // ํŠน์ • ๊ฑฐ๋ž˜ ๋ฌดํšจํ™” transaction: (id: string) => { - queryClient.invalidateQueries({ queryKey: queryKeys.transactions.detail(id) }); + queryClient.invalidateQueries({ + queryKey: queryKeys.transactions.detail(id), + }); }, - + // ๋ชจ๋“  ์˜ˆ์‚ฐ ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” budget: () => { queryClient.invalidateQueries({ queryKey: queryKeys.budget.all() }); }, - + // ์ธ์ฆ ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” auth: () => { queryClient.invalidateQueries({ queryKey: queryKeys.auth.user() }); queryClient.invalidateQueries({ queryKey: queryKeys.auth.session() }); }, - + // ๋ชจ๋“  ์ฟผ๋ฆฌ ๋ฌดํšจํ™” (๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์‹œ ์‚ฌ์šฉ) all: () => { queryClient.invalidateQueries(); @@ -221,9 +225,9 @@ export const prefetchQueries = { */ export const isDevMode = import.meta.env.DEV; -syncLogger.info('TanStack Query ์„ค์ • ์™„๋ฃŒ', { - staleTime: '5๋ถ„', - gcTime: '30๋ถ„', +syncLogger.info("TanStack Query ์„ค์ • ์™„๋ฃŒ", { + staleTime: "5๋ถ„", + gcTime: "30๋ถ„", retryEnabled: true, devMode: isDevMode, -}); \ No newline at end of file +}); diff --git a/src/setupTests.ts b/src/setupTests.ts index 985ef48..5af2c76 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,14 +1,14 @@ /** * ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ - * + * * ๋ชจ๋“  ํ…Œ์ŠคํŠธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์„ค์ •๊ณผ ๋ชจํ‚น์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. */ -import '@testing-library/jest-dom'; -import { vi } from 'vitest'; +import "@testing-library/jest-dom"; +import { vi } from "vitest"; // React Query ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient } from "@tanstack/react-query"; // ์ „์—ญ ๋ชจํ‚น ์„ค์ • global.ResizeObserver = vi.fn().mockImplementation(() => ({ @@ -25,7 +25,7 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({ })); // matchMedia ๋ชจํ‚น (Radix UI ํ˜ธํ™˜์„ฑ) -Object.defineProperty(window, 'matchMedia', { +Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query) => ({ matches: false, @@ -48,17 +48,17 @@ const localStorageMock = { length: 0, key: vi.fn(), }; -Object.defineProperty(window, 'localStorage', { +Object.defineProperty(window, "localStorage", { value: localStorageMock, }); // sessionStorage ๋ชจํ‚น -Object.defineProperty(window, 'sessionStorage', { +Object.defineProperty(window, "sessionStorage", { value: localStorageMock, }); // ๋„ค๋น„๊ฒŒ์ด์…˜ API ๋ชจํ‚น -Object.defineProperty(navigator, 'onLine', { +Object.defineProperty(navigator, "onLine", { writable: true, value: true, }); @@ -67,12 +67,12 @@ Object.defineProperty(navigator, 'onLine', { global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, - statusText: 'OK', + statusText: "OK", json: vi.fn().mockResolvedValue({}), - text: vi.fn().mockResolvedValue(''), + text: vi.fn().mockResolvedValue(""), headers: new Headers(), - url: '', - type: 'basic', + url: "", + type: "basic", redirected: false, bodyUsed: false, body: null, @@ -80,25 +80,25 @@ global.fetch = vi.fn().mockResolvedValue({ } as any); // Appwrite SDK ๋ชจํ‚น -vi.mock('appwrite', () => ({ +vi.mock("appwrite", () => ({ Client: vi.fn().mockImplementation(() => ({ setEndpoint: vi.fn().mockReturnThis(), setProject: vi.fn().mockReturnThis(), })), Account: vi.fn().mockImplementation(() => ({ get: vi.fn().mockResolvedValue({ - $id: 'test-user-id', - email: 'test@example.com', - name: 'Test User', + $id: "test-user-id", + email: "test@example.com", + name: "Test User", }), createEmailPasswordSession: vi.fn().mockResolvedValue({ - $id: 'test-session-id', - userId: 'test-user-id', + $id: "test-session-id", + userId: "test-user-id", }), deleteSession: vi.fn().mockResolvedValue({}), createAccount: vi.fn().mockResolvedValue({ - $id: 'test-user-id', - email: 'test@example.com', + $id: "test-user-id", + email: "test@example.com", }), createRecovery: vi.fn().mockResolvedValue({}), })), @@ -108,30 +108,30 @@ vi.mock('appwrite', () => ({ total: 0, }), createDocument: vi.fn().mockResolvedValue({ - $id: 'test-document-id', + $id: "test-document-id", $createdAt: new Date().toISOString(), $updatedAt: new Date().toISOString(), }), updateDocument: vi.fn().mockResolvedValue({ - $id: 'test-document-id', + $id: "test-document-id", $updatedAt: new Date().toISOString(), }), deleteDocument: vi.fn().mockResolvedValue({}), getDatabase: vi.fn().mockResolvedValue({ - $id: 'test-database-id', - name: 'Test Database', + $id: "test-database-id", + name: "Test Database", }), createDatabase: vi.fn().mockResolvedValue({ - $id: 'test-database-id', - name: 'Test Database', + $id: "test-database-id", + name: "Test Database", }), getCollection: vi.fn().mockResolvedValue({ - $id: 'test-collection-id', - name: 'Test Collection', + $id: "test-collection-id", + name: "Test Collection", }), createCollection: vi.fn().mockResolvedValue({ - $id: 'test-collection-id', - name: 'Test Collection', + $id: "test-collection-id", + name: "Test Collection", }), createStringAttribute: vi.fn().mockResolvedValue({}), createFloatAttribute: vi.fn().mockResolvedValue({}), @@ -157,29 +157,29 @@ vi.mock('appwrite', () => ({ }, Role: { user: vi.fn((userId) => `user:${userId}`), - any: vi.fn(() => 'any'), + any: vi.fn(() => "any"), }, })); // React Router ๋ชจํ‚น -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); return { ...actual, useNavigate: () => vi.fn(), useLocation: () => ({ - pathname: '/', - search: '', - hash: '', + pathname: "/", + search: "", + hash: "", state: null, - key: 'test', + key: "test", }), useParams: () => ({}), }; }); // Logger ๋ชจํ‚น (์ฝ˜์†” ์ถœ๋ ฅ ๋ฐฉ์ง€) -vi.mock('@/utils/logger', () => ({ +vi.mock("@/utils/logger", () => ({ logger: { info: vi.fn(), warn: vi.fn(), @@ -207,7 +207,7 @@ vi.mock('@/utils/logger', () => ({ })); // Toast ์•Œ๋ฆผ ๋ชจํ‚น -vi.mock('@/hooks/useToast.wrapper', () => ({ +vi.mock("@/hooks/useToast.wrapper", () => ({ toast: vi.fn(), })); @@ -226,7 +226,7 @@ export const createTestQueryClient = () => }); // Date ๊ฐ์ฒด ๋ชจํ‚น (์ผ๊ด€๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด) -const mockDate = new Date('2024-01-01T12:00:00.000Z'); +const mockDate = new Date("2024-01-01T12:00:00.000Z"); beforeAll(() => { vi.useFakeTimers(); @@ -249,8 +249,8 @@ const originalConsoleError = console.error; beforeAll(() => { console.error = (...args) => { if ( - typeof args[0] === 'string' && - args[0].includes('Warning: ReactDOM.render is no longer supported') + typeof args[0] === "string" && + args[0].includes("Warning: ReactDOM.render is no longer supported") ) { return; } @@ -260,4 +260,4 @@ beforeAll(() => { afterAll(() => { console.error = originalConsoleError; -}); \ No newline at end of file +}); diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index a465909..2bc19b4 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -9,23 +9,23 @@ interface AppState { theme: "light" | "dark" | "system"; sidebarOpen: boolean; globalLoading: boolean; - + // ์—๋Ÿฌ ์ฒ˜๋ฆฌ globalError: string | null; - + // ์•Œ๋ฆผ ๋ฐ ํ† ์ŠคํŠธ notifications: Notification[]; - + // ์•ฑ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ lastSyncTime: string | null; isOnline: boolean; - + // ์•ก์…˜ setTheme: (theme: "light" | "dark" | "system") => void; setSidebarOpen: (open: boolean) => void; setGlobalLoading: (loading: boolean) => void; setGlobalError: (error: string | null) => void; - addNotification: (notification: Omit) => void; + addNotification: (notification: Omit) => void; removeNotification: (id: string) => void; clearNotifications: () => void; setLastSyncTime: (time: string) => void; @@ -46,7 +46,7 @@ interface Notification { /** * ์•ฑ ์ „์ฒด ์ƒํƒœ ์Šคํ† ์–ด - * + * * ์ „์—ญ UI ์ƒํƒœ, ํ…Œ๋งˆ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์•Œ๋ฆผ ๋“ฑ์„ ๊ด€๋ฆฌ */ export const useAppStore = create()( @@ -83,7 +83,7 @@ export const useAppStore = create()( }, // ์•Œ๋ฆผ ์ถ”๊ฐ€ - addNotification: (notificationData: Omit) => { + addNotification: (notificationData: Omit) => { const notification: Notification = { ...notificationData, id: crypto.randomUUID(), @@ -169,23 +169,24 @@ export const useGlobalError = () => { }; export const useNotifications = () => { - const { - notifications, - addNotification, - removeNotification, - clearNotifications + const { + notifications, + addNotification, + removeNotification, + clearNotifications, } = useAppStore(); - - return { - notifications, - addNotification, - removeNotification, - clearNotifications + + return { + notifications, + addNotification, + removeNotification, + clearNotifications, }; }; export const useSyncStatus = () => { - const { lastSyncTime, setLastSyncTime, isOnline, setOnlineStatus } = useAppStore(); + const { lastSyncTime, setLastSyncTime, isOnline, setOnlineStatus } = + useAppStore(); return { lastSyncTime, setLastSyncTime, isOnline, setOnlineStatus }; }; @@ -216,4 +217,4 @@ export const cleanupOnlineStatusListener = () => { onlineStatusListener(); onlineStatusListener = null; } -}; \ No newline at end of file +}; diff --git a/src/stores/index.ts b/src/stores/index.ts index 648942a..24f7aaf 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,6 +1,6 @@ /** * Zustand ์Šคํ† ์–ด ํ†ตํ•ฉ export - * + * * ๋ชจ๋“  ์Šคํ† ์–ด์™€ ๊ด€๋ จ ํ›…์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌ */ @@ -49,4 +49,4 @@ export type { SignUpResponse, ResetPasswordResponse, AppwriteInitializationStatus, -} from "@/contexts/auth/types"; \ No newline at end of file +} from "@/contexts/auth/types"; diff --git a/src/stores/storeInitializer.ts b/src/stores/storeInitializer.ts index 8287053..3dea7a7 100644 --- a/src/stores/storeInitializer.ts +++ b/src/stores/storeInitializer.ts @@ -1,15 +1,15 @@ /** * Zustand ์Šคํ† ์–ด ์ดˆ๊ธฐํ™” ์œ ํ‹ธ๋ฆฌํ‹ฐ - * + * * ์•ฑ ์‹œ์ž‘์‹œ ํ•„์š”ํ•œ ์Šคํ† ์–ด ์ดˆ๊ธฐํ™” ์ž‘์—…์„ ์ฒ˜๋ฆฌ */ -import { - useAuthStore, - startSessionValidation, +import { + useAuthStore, + startSessionValidation, stopSessionValidation, setupOnlineStatusListener, - cleanupOnlineStatusListener + cleanupOnlineStatusListener, } from "./index"; import { authLogger } from "@/utils/logger"; @@ -45,9 +45,9 @@ export const cleanupStores = (): void => { try { stopSessionValidation(); cleanupOnlineStatusListener(); - + authLogger.info("์Šคํ† ์–ด ์ •๋ฆฌ ์™„๋ฃŒ"); } catch (error) { authLogger.error("์Šคํ† ์–ด ์ •๋ฆฌ ์‹คํŒจ", error); } -}; \ No newline at end of file +}; diff --git a/src/utils/__tests__/categoryColorUtils.test.ts b/src/utils/__tests__/categoryColorUtils.test.ts index 4e96877..de9fa13 100644 --- a/src/utils/__tests__/categoryColorUtils.test.ts +++ b/src/utils/__tests__/categoryColorUtils.test.ts @@ -1,172 +1,167 @@ -import { describe, expect, it } from 'vitest'; -import { getCategoryColor } from '../categoryColorUtils'; +import { describe, expect, it } from "vitest"; +import { getCategoryColor } from "../categoryColorUtils"; -describe('categoryColorUtils', () => { - describe('getCategoryColor', () => { - describe('food category colors', () => { - it('returns correct color for food-related categories', () => { - expect(getCategoryColor('์Œ์‹')).toBe('#81c784'); - expect(getCategoryColor('์‹๋น„')).toBe('#81c784'); +describe("categoryColorUtils", () => { + describe("getCategoryColor", () => { + describe("food category colors", () => { + it("returns correct color for food-related categories", () => { + expect(getCategoryColor("์Œ์‹")).toBe("#81c784"); + expect(getCategoryColor("์‹๋น„")).toBe("#81c784"); }); - it('handles case insensitive food categories', () => { - expect(getCategoryColor('์Œ์‹')).toBe('#81c784'); - expect(getCategoryColor('์Œ์‹')).toBe('#81c784'); - expect(getCategoryColor('์‹๋น„')).toBe('#81c784'); - expect(getCategoryColor('์‹๋น„')).toBe('#81c784'); + it("handles case insensitive food categories", () => { + expect(getCategoryColor("์Œ์‹")).toBe("#81c784"); + expect(getCategoryColor("์Œ์‹")).toBe("#81c784"); + expect(getCategoryColor("์‹๋น„")).toBe("#81c784"); + expect(getCategoryColor("์‹๋น„")).toBe("#81c784"); }); - it('handles food categories with extra text', () => { - expect(getCategoryColor('์™ธ์‹ ์Œ์‹')).toBe('#81c784'); - expect(getCategoryColor('์ผ๋ฐ˜ ์‹๋น„')).toBe('#81c784'); - expect(getCategoryColor('ํšŒ์‚ฌ ์‹๋น„ ์ง€์›')).toBe('#81c784'); + it("handles food categories with extra text", () => { + expect(getCategoryColor("์™ธ์‹ ์Œ์‹")).toBe("#81c784"); + expect(getCategoryColor("์ผ๋ฐ˜ ์‹๋น„")).toBe("#81c784"); + expect(getCategoryColor("ํšŒ์‚ฌ ์‹๋น„ ์ง€์›")).toBe("#81c784"); }); }); - describe('shopping category colors', () => { - it('returns correct color for shopping-related categories', () => { - expect(getCategoryColor('์‡ผํ•‘')).toBe('#AED581'); - expect(getCategoryColor('์ƒํ™œ๋น„')).toBe('#AED581'); + describe("shopping category colors", () => { + it("returns correct color for shopping-related categories", () => { + expect(getCategoryColor("์‡ผํ•‘")).toBe("#AED581"); + expect(getCategoryColor("์ƒํ™œ๋น„")).toBe("#AED581"); }); - it('handles case insensitive shopping categories', () => { - expect(getCategoryColor('์‡ผํ•‘')).toBe('#AED581'); - expect(getCategoryColor('์‡ผํ•‘')).toBe('#AED581'); - expect(getCategoryColor('์ƒํ™œ๋น„')).toBe('#AED581'); - expect(getCategoryColor('์ƒํ™œ๋น„')).toBe('#AED581'); + it("handles case insensitive shopping categories", () => { + expect(getCategoryColor("์‡ผํ•‘")).toBe("#AED581"); + expect(getCategoryColor("์‡ผํ•‘")).toBe("#AED581"); + expect(getCategoryColor("์ƒํ™œ๋น„")).toBe("#AED581"); + expect(getCategoryColor("์ƒํ™œ๋น„")).toBe("#AED581"); }); - it('handles shopping categories with extra text', () => { - expect(getCategoryColor('์˜จ๋ผ์ธ ์‡ผํ•‘')).toBe('#AED581'); - expect(getCategoryColor('์›” ์ƒํ™œ๋น„')).toBe('#AED581'); - expect(getCategoryColor('ํ•„์ˆ˜ ์ƒํ™œ๋น„ ์ง€์ถœ')).toBe('#AED581'); + it("handles shopping categories with extra text", () => { + expect(getCategoryColor("์˜จ๋ผ์ธ ์‡ผํ•‘")).toBe("#AED581"); + expect(getCategoryColor("์›” ์ƒํ™œ๋น„")).toBe("#AED581"); + expect(getCategoryColor("ํ•„์ˆ˜ ์ƒํ™œ๋น„ ์ง€์ถœ")).toBe("#AED581"); }); }); - describe('transportation category colors', () => { - it('returns correct color for transportation categories', () => { - expect(getCategoryColor('๊ตํ†ต')).toBe('#2E7D32'); + describe("transportation category colors", () => { + it("returns correct color for transportation categories", () => { + expect(getCategoryColor("๊ตํ†ต")).toBe("#2E7D32"); }); - it('handles case insensitive transportation categories', () => { - expect(getCategoryColor('๊ตํ†ต')).toBe('#2E7D32'); - expect(getCategoryColor('๊ตํ†ต')).toBe('#2E7D32'); + it("handles case insensitive transportation categories", () => { + expect(getCategoryColor("๊ตํ†ต")).toBe("#2E7D32"); + expect(getCategoryColor("๊ตํ†ต")).toBe("#2E7D32"); }); - it('handles transportation categories with extra text', () => { - expect(getCategoryColor('๋Œ€์ค‘๊ตํ†ต')).toBe('#2E7D32'); - expect(getCategoryColor('๊ตํ†ต๋น„')).toBe('#2E7D32'); - expect(getCategoryColor('๋ฒ„์Šค ๊ตํ†ต ์š”๊ธˆ')).toBe('#2E7D32'); + it("handles transportation categories with extra text", () => { + expect(getCategoryColor("๋Œ€์ค‘๊ตํ†ต")).toBe("#2E7D32"); + expect(getCategoryColor("๊ตํ†ต๋น„")).toBe("#2E7D32"); + expect(getCategoryColor("๋ฒ„์Šค ๊ตํ†ต ์š”๊ธˆ")).toBe("#2E7D32"); }); }); - describe('other category colors', () => { - it('returns correct color for other categories', () => { - expect(getCategoryColor('๊ธฐํƒ€')).toBe('#9E9E9E'); + describe("other category colors", () => { + it("returns correct color for other categories", () => { + expect(getCategoryColor("๊ธฐํƒ€")).toBe("#9E9E9E"); }); - it('handles case insensitive other categories', () => { - expect(getCategoryColor('๊ธฐํƒ€')).toBe('#9E9E9E'); - expect(getCategoryColor('๊ธฐํƒ€')).toBe('#9E9E9E'); + it("handles case insensitive other categories", () => { + expect(getCategoryColor("๊ธฐํƒ€")).toBe("#9E9E9E"); + expect(getCategoryColor("๊ธฐํƒ€")).toBe("#9E9E9E"); }); - it('handles other categories with extra text', () => { - expect(getCategoryColor('๊ธฐํƒ€ ์ง€์ถœ')).toBe('#9E9E9E'); - expect(getCategoryColor('๊ธฐํƒ€ ๋น„์šฉ')).toBe('#9E9E9E'); - expect(getCategoryColor('์—ฌ๋Ÿฌ ๊ธฐํƒ€ ํ•ญ๋ชฉ')).toBe('#9E9E9E'); + it("handles other categories with extra text", () => { + expect(getCategoryColor("๊ธฐํƒ€ ์ง€์ถœ")).toBe("#9E9E9E"); + expect(getCategoryColor("๊ธฐํƒ€ ๋น„์šฉ")).toBe("#9E9E9E"); + expect(getCategoryColor("์—ฌ๋Ÿฌ ๊ธฐํƒ€ ํ•ญ๋ชฉ")).toBe("#9E9E9E"); }); }); - describe('default category color', () => { - it('returns default color for unrecognized categories', () => { - expect(getCategoryColor('์˜๋ฃŒ')).toBe('#4CAF50'); - expect(getCategoryColor('์ทจ๋ฏธ')).toBe('#4CAF50'); - expect(getCategoryColor('๊ต์œก')).toBe('#4CAF50'); - expect(getCategoryColor('์—ฌํ–‰')).toBe('#4CAF50'); - expect(getCategoryColor('์šด๋™')).toBe('#4CAF50'); + describe("default category color", () => { + it("returns default color for unrecognized categories", () => { + expect(getCategoryColor("์˜๋ฃŒ")).toBe("#4CAF50"); + expect(getCategoryColor("์ทจ๋ฏธ")).toBe("#4CAF50"); + expect(getCategoryColor("๊ต์œก")).toBe("#4CAF50"); + expect(getCategoryColor("์—ฌํ–‰")).toBe("#4CAF50"); + expect(getCategoryColor("์šด๋™")).toBe("#4CAF50"); }); - it('returns default color for empty or random strings', () => { - expect(getCategoryColor('')).toBe('#4CAF50'); - expect(getCategoryColor('random123')).toBe('#4CAF50'); - expect(getCategoryColor('xyz')).toBe('#4CAF50'); - expect(getCategoryColor('unknown category')).toBe('#4CAF50'); + it("returns default color for empty or random strings", () => { + expect(getCategoryColor("")).toBe("#4CAF50"); + expect(getCategoryColor("random123")).toBe("#4CAF50"); + expect(getCategoryColor("xyz")).toBe("#4CAF50"); + expect(getCategoryColor("unknown category")).toBe("#4CAF50"); }); }); - describe('edge cases and input handling', () => { - it('handles whitespace correctly', () => { - expect(getCategoryColor(' ์Œ์‹ ')).toBe('#81c784'); - expect(getCategoryColor('\t์‡ผํ•‘\n')).toBe('#AED581'); - expect(getCategoryColor(' ๊ตํ†ต ')).toBe('#2E7D32'); - expect(getCategoryColor(' ๊ธฐํƒ€ ')).toBe('#9E9E9E'); + describe("edge cases and input handling", () => { + it("handles whitespace correctly", () => { + expect(getCategoryColor(" ์Œ์‹ ")).toBe("#81c784"); + expect(getCategoryColor("\t์‡ผํ•‘\n")).toBe("#AED581"); + expect(getCategoryColor(" ๊ตํ†ต ")).toBe("#2E7D32"); + expect(getCategoryColor(" ๊ธฐํƒ€ ")).toBe("#9E9E9E"); }); - it('handles mixed case with whitespace', () => { - expect(getCategoryColor(' ์Œ์‹ ')).toBe('#81c784'); - expect(getCategoryColor(' ShOpPiNg ')).toBe('#4CAF50'); // English, so default - expect(getCategoryColor(' ๊ตํ†ต ')).toBe('#2E7D32'); + it("handles mixed case with whitespace", () => { + expect(getCategoryColor(" ์Œ์‹ ")).toBe("#81c784"); + expect(getCategoryColor(" ShOpPiNg ")).toBe("#4CAF50"); // English, so default + expect(getCategoryColor(" ๊ตํ†ต ")).toBe("#2E7D32"); }); - it('handles special characters', () => { - expect(getCategoryColor('์Œ์‹!')).toBe('#81c784'); - expect(getCategoryColor('์‡ผํ•‘@')).toBe('#AED581'); - expect(getCategoryColor('๊ตํ†ต#')).toBe('#2E7D32'); - expect(getCategoryColor('๊ธฐํƒ€$')).toBe('#9E9E9E'); + it("handles special characters", () => { + expect(getCategoryColor("์Œ์‹!")).toBe("#81c784"); + expect(getCategoryColor("์‡ผํ•‘@")).toBe("#AED581"); + expect(getCategoryColor("๊ตํ†ต#")).toBe("#2E7D32"); + expect(getCategoryColor("๊ธฐํƒ€$")).toBe("#9E9E9E"); }); - it('handles numbers in category names', () => { - expect(getCategoryColor('์Œ์‹123')).toBe('#81c784'); - expect(getCategoryColor('์‡ผํ•‘456')).toBe('#AED581'); - expect(getCategoryColor('๊ตํ†ต789')).toBe('#2E7D32'); - expect(getCategoryColor('๊ธฐํƒ€000')).toBe('#9E9E9E'); + it("handles numbers in category names", () => { + expect(getCategoryColor("์Œ์‹123")).toBe("#81c784"); + expect(getCategoryColor("์‡ผํ•‘456")).toBe("#AED581"); + expect(getCategoryColor("๊ตํ†ต789")).toBe("#2E7D32"); + expect(getCategoryColor("๊ธฐํƒ€000")).toBe("#9E9E9E"); }); - it('handles very long category names', () => { - const longCategory = '๋งค์šฐ ๊ธด ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์ธ๋ฐ ์Œ์‹์ด๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค'; - expect(getCategoryColor(longCategory)).toBe('#81c784'); + it("handles very long category names", () => { + const longCategory = + "๋งค์šฐ ๊ธด ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์ธ๋ฐ ์Œ์‹์ด๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค"; + expect(getCategoryColor(longCategory)).toBe("#81c784"); }); - it('handles mixed languages', () => { - expect(getCategoryColor('food ์Œ์‹')).toBe('#81c784'); - expect(getCategoryColor('shopping ์‡ผํ•‘')).toBe('#AED581'); - expect(getCategoryColor('transport ๊ตํ†ต')).toBe('#2E7D32'); - expect(getCategoryColor('other ๊ธฐํƒ€')).toBe('#9E9E9E'); + it("handles mixed languages", () => { + expect(getCategoryColor("food ์Œ์‹")).toBe("#81c784"); + expect(getCategoryColor("shopping ์‡ผํ•‘")).toBe("#AED581"); + expect(getCategoryColor("transport ๊ตํ†ต")).toBe("#2E7D32"); + expect(getCategoryColor("other ๊ธฐํƒ€")).toBe("#9E9E9E"); }); }); - describe('multiple keyword matches', () => { - it('prioritizes first match when multiple keywords present', () => { + describe("multiple keyword matches", () => { + it("prioritizes first match when multiple keywords present", () => { // When multiple categories are mentioned, it should match the first one found - expect(getCategoryColor('์Œ์‹๊ณผ ์‡ผํ•‘')).toBe('#81c784'); // ์Œ์‹ comes first in the if-else chain - expect(getCategoryColor('์‡ผํ•‘๊ณผ ๊ตํ†ต')).toBe('#AED581'); // ์‡ผํ•‘ comes first - expect(getCategoryColor('๊ตํ†ต๊ณผ ๊ธฐํƒ€')).toBe('#2E7D32'); // ๊ตํ†ต comes first + expect(getCategoryColor("์Œ์‹๊ณผ ์‡ผํ•‘")).toBe("#81c784"); // ์Œ์‹ comes first in the if-else chain + expect(getCategoryColor("์‡ผํ•‘๊ณผ ๊ตํ†ต")).toBe("#AED581"); // ์‡ผํ•‘ comes first + expect(getCategoryColor("๊ตํ†ต๊ณผ ๊ธฐํƒ€")).toBe("#2E7D32"); // ๊ตํ†ต comes first }); }); - describe('consistency tests', () => { - it('returns consistent colors for same normalized input', () => { - const testCases = [ - '์Œ์‹', - ' ์Œ์‹ ', - '์Œ์‹', - '์Œ์‹!@#', - 'abc์Œ์‹xyz' - ]; + describe("consistency tests", () => { + it("returns consistent colors for same normalized input", () => { + const testCases = ["์Œ์‹", " ์Œ์‹ ", "์Œ์‹", "์Œ์‹!@#", "abc์Œ์‹xyz"]; - const expectedColor = '#81c784'; - testCases.forEach(testCase => { + const expectedColor = "#81c784"; + testCases.forEach((testCase) => { expect(getCategoryColor(testCase)).toBe(expectedColor); }); }); - it('has unique colors for each main category', () => { + it("has unique colors for each main category", () => { const colors = { - food: getCategoryColor('์Œ์‹'), - shopping: getCategoryColor('์‡ผํ•‘'), - transport: getCategoryColor('๊ตํ†ต'), - other: getCategoryColor('๊ธฐํƒ€'), - default: getCategoryColor('unknown') + food: getCategoryColor("์Œ์‹"), + shopping: getCategoryColor("์‡ผํ•‘"), + transport: getCategoryColor("๊ตํ†ต"), + other: getCategoryColor("๊ธฐํƒ€"), + default: getCategoryColor("unknown"), }; const uniqueColors = new Set(Object.values(colors)); @@ -174,16 +169,16 @@ describe('categoryColorUtils', () => { }); }); - describe('color format validation', () => { - it('returns valid hex color format', () => { - const categories = ['์Œ์‹', '์‡ผํ•‘', '๊ตํ†ต', '๊ธฐํƒ€', 'unknown']; + describe("color format validation", () => { + it("returns valid hex color format", () => { + const categories = ["์Œ์‹", "์‡ผํ•‘", "๊ตํ†ต", "๊ธฐํƒ€", "unknown"]; const hexColorRegex = /^#[0-9A-F]{6}$/i; - categories.forEach(category => { + categories.forEach((category) => { const color = getCategoryColor(category); expect(color).toMatch(hexColorRegex); }); }); }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/__tests__/currencyFormatter.test.ts b/src/utils/__tests__/currencyFormatter.test.ts index db4294c..eca725b 100644 --- a/src/utils/__tests__/currencyFormatter.test.ts +++ b/src/utils/__tests__/currencyFormatter.test.ts @@ -1,106 +1,110 @@ -import { describe, expect, it } from 'vitest'; -import { formatCurrency, extractNumber, formatInputCurrency } from '../currencyFormatter'; +import { describe, expect, it } from "vitest"; +import { + formatCurrency, + extractNumber, + formatInputCurrency, +} from "../currencyFormatter"; -describe('currencyFormatter', () => { - describe('formatCurrency', () => { - it('formats positive numbers correctly', () => { - expect(formatCurrency(1000)).toBe('1,000์›'); - expect(formatCurrency(1234567)).toBe('1,234,567์›'); - expect(formatCurrency(100)).toBe('100์›'); +describe("currencyFormatter", () => { + describe("formatCurrency", () => { + it("formats positive numbers correctly", () => { + expect(formatCurrency(1000)).toBe("1,000์›"); + expect(formatCurrency(1234567)).toBe("1,234,567์›"); + expect(formatCurrency(100)).toBe("100์›"); }); - it('formats zero correctly', () => { - expect(formatCurrency(0)).toBe('0์›'); + it("formats zero correctly", () => { + expect(formatCurrency(0)).toBe("0์›"); }); - it('formats negative numbers correctly', () => { - expect(formatCurrency(-1000)).toBe('-1,000์›'); - expect(formatCurrency(-123456)).toBe('-123,456์›'); + it("formats negative numbers correctly", () => { + expect(formatCurrency(-1000)).toBe("-1,000์›"); + expect(formatCurrency(-123456)).toBe("-123,456์›"); }); - it('handles decimal numbers as-is (toLocaleString preserves decimals)', () => { - expect(formatCurrency(1000.99)).toBe('1,000.99์›'); - expect(formatCurrency(999.1)).toBe('999.1์›'); - expect(formatCurrency(1000.0)).toBe('1,000์›'); + it("handles decimal numbers as-is (toLocaleString preserves decimals)", () => { + expect(formatCurrency(1000.99)).toBe("1,000.99์›"); + expect(formatCurrency(999.1)).toBe("999.1์›"); + expect(formatCurrency(1000.0)).toBe("1,000์›"); }); - it('handles very large numbers', () => { - expect(formatCurrency(1000000000)).toBe('1,000,000,000์›'); + it("handles very large numbers", () => { + expect(formatCurrency(1000000000)).toBe("1,000,000,000์›"); }); }); - describe('extractNumber', () => { - it('extracts numbers from currency strings', () => { - expect(extractNumber('1,000์›')).toBe(1000); - expect(extractNumber('1,234,567์›')).toBe(1234567); - expect(extractNumber('100์›')).toBe(100); + describe("extractNumber", () => { + it("extracts numbers from currency strings", () => { + expect(extractNumber("1,000์›")).toBe(1000); + expect(extractNumber("1,234,567์›")).toBe(1234567); + expect(extractNumber("100์›")).toBe(100); }); - it('extracts numbers from strings with mixed characters', () => { - expect(extractNumber('abc123def456')).toBe(123456); - expect(extractNumber('$1,000!')).toBe(1000); - expect(extractNumber('test 500 won')).toBe(500); + it("extracts numbers from strings with mixed characters", () => { + expect(extractNumber("abc123def456")).toBe(123456); + expect(extractNumber("$1,000!")).toBe(1000); + expect(extractNumber("test 500 won")).toBe(500); }); - it('returns 0 for strings without numbers', () => { - expect(extractNumber('')).toBe(0); - expect(extractNumber('abc')).toBe(0); - expect(extractNumber('์›')).toBe(0); - expect(extractNumber('!@#$%')).toBe(0); + it("returns 0 for strings without numbers", () => { + expect(extractNumber("")).toBe(0); + expect(extractNumber("abc")).toBe(0); + expect(extractNumber("์›")).toBe(0); + expect(extractNumber("!@#$%")).toBe(0); }); - it('handles strings with only numbers', () => { - expect(extractNumber('1000')).toBe(1000); - expect(extractNumber('0')).toBe(0); - expect(extractNumber('123456789')).toBe(123456789); + it("handles strings with only numbers", () => { + expect(extractNumber("1000")).toBe(1000); + expect(extractNumber("0")).toBe(0); + expect(extractNumber("123456789")).toBe(123456789); }); - it('ignores non-digit characters', () => { - expect(extractNumber('1,000.50์›')).toBe(100050); - expect(extractNumber('-1000')).toBe(1000); // ์Œ์ˆ˜ ๊ธฐํ˜ธ๋Š” ์ œ๊ฑฐ๋จ - expect(extractNumber('+1000')).toBe(1000); // ์–‘์ˆ˜ ๊ธฐํ˜ธ๋Š” ์ œ๊ฑฐ๋จ + it("ignores non-digit characters", () => { + expect(extractNumber("1,000.50์›")).toBe(100050); + expect(extractNumber("-1000")).toBe(1000); // ์Œ์ˆ˜ ๊ธฐํ˜ธ๋Š” ์ œ๊ฑฐ๋จ + expect(extractNumber("+1000")).toBe(1000); // ์–‘์ˆ˜ ๊ธฐํ˜ธ๋Š” ์ œ๊ฑฐ๋จ }); }); - describe('formatInputCurrency', () => { - it('formats numeric strings with commas', () => { - expect(formatInputCurrency('1000')).toBe('1,000'); - expect(formatInputCurrency('1234567')).toBe('1,234,567'); - expect(formatInputCurrency('100')).toBe('100'); + describe("formatInputCurrency", () => { + it("formats numeric strings with commas", () => { + expect(formatInputCurrency("1000")).toBe("1,000"); + expect(formatInputCurrency("1234567")).toBe("1,234,567"); + expect(formatInputCurrency("100")).toBe("100"); }); - it('handles strings with existing commas', () => { - expect(formatInputCurrency('1,000')).toBe('1,000'); - expect(formatInputCurrency('1,234,567')).toBe('1,234,567'); + it("handles strings with existing commas", () => { + expect(formatInputCurrency("1,000")).toBe("1,000"); + expect(formatInputCurrency("1,234,567")).toBe("1,234,567"); }); - it('extracts and formats numbers from mixed strings', () => { - expect(formatInputCurrency('1000์›')).toBe('1,000'); - expect(formatInputCurrency('abc1000def')).toBe('1,000'); - expect(formatInputCurrency('$1000!')).toBe('1,000'); + it("extracts and formats numbers from mixed strings", () => { + expect(formatInputCurrency("1000์›")).toBe("1,000"); + expect(formatInputCurrency("abc1000def")).toBe("1,000"); + expect(formatInputCurrency("$1000!")).toBe("1,000"); }); - it('returns empty string for non-numeric input', () => { - expect(formatInputCurrency('')).toBe(''); - expect(formatInputCurrency('abc')).toBe(''); - expect(formatInputCurrency('!@#$%')).toBe(''); - expect(formatInputCurrency('์›')).toBe(''); + it("returns empty string for non-numeric input", () => { + expect(formatInputCurrency("")).toBe(""); + expect(formatInputCurrency("abc")).toBe(""); + expect(formatInputCurrency("!@#$%")).toBe(""); + expect(formatInputCurrency("์›")).toBe(""); }); - it('handles zero correctly', () => { - expect(formatInputCurrency('0')).toBe('0'); - expect(formatInputCurrency('00')).toBe('0'); - expect(formatInputCurrency('000')).toBe('0'); + it("handles zero correctly", () => { + expect(formatInputCurrency("0")).toBe("0"); + expect(formatInputCurrency("00")).toBe("0"); + expect(formatInputCurrency("000")).toBe("0"); }); - it('handles large numbers', () => { - expect(formatInputCurrency('1000000000')).toBe('1,000,000,000'); - expect(formatInputCurrency('999999999')).toBe('999,999,999'); + it("handles large numbers", () => { + expect(formatInputCurrency("1000000000")).toBe("1,000,000,000"); + expect(formatInputCurrency("999999999")).toBe("999,999,999"); }); - it('removes leading zeros', () => { - expect(formatInputCurrency('001000')).toBe('1,000'); - expect(formatInputCurrency('000100')).toBe('100'); + it("removes leading zeros", () => { + expect(formatInputCurrency("001000")).toBe("1,000"); + expect(formatInputCurrency("000100")).toBe("100"); }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/__tests__/transactionUtils.test.ts b/src/utils/__tests__/transactionUtils.test.ts index 14ab489..f3e2fc6 100644 --- a/src/utils/__tests__/transactionUtils.test.ts +++ b/src/utils/__tests__/transactionUtils.test.ts @@ -1,170 +1,177 @@ -import { describe, expect, it } from 'vitest'; -import { - filterTransactionsByMonth, - filterTransactionsByQuery, - calculateTotalExpenses -} from '../transactionUtils'; -import { Transaction } from '@/contexts/budget/types'; +import { describe, expect, it } from "vitest"; +import { + filterTransactionsByMonth, + filterTransactionsByQuery, + calculateTotalExpenses, +} from "../transactionUtils"; +import { Transaction } from "@/contexts/budget/types"; // Mock transaction data for testing -const createMockTransaction = (overrides: Partial = {}): Transaction => ({ - id: 'test-id', - title: 'Test Transaction', +const createMockTransaction = ( + overrides: Partial = {} +): Transaction => ({ + id: "test-id", + title: "Test Transaction", amount: 1000, - date: '2024-06-15', - category: 'Food', - type: 'expense', - paymentMethod: '์‹ ์šฉ์นด๋“œ', + date: "2024-06-15", + category: "Food", + type: "expense", + paymentMethod: "์‹ ์šฉ์นด๋“œ", ...overrides, }); -describe('transactionUtils', () => { - describe('filterTransactionsByMonth', () => { +describe("transactionUtils", () => { + describe("filterTransactionsByMonth", () => { const mockTransactions: Transaction[] = [ - createMockTransaction({ id: '1', date: '2024-06-01', amount: 1000 }), - createMockTransaction({ id: '2', date: '2024-06-15', amount: 2000 }), - createMockTransaction({ id: '3', date: '2024-07-01', amount: 3000 }), - createMockTransaction({ id: '4', date: '2024-05-30', amount: 4000 }), - createMockTransaction({ id: '5', date: '2024-06-30', amount: 5000, type: 'income' }), + createMockTransaction({ id: "1", date: "2024-06-01", amount: 1000 }), + createMockTransaction({ id: "2", date: "2024-06-15", amount: 2000 }), + createMockTransaction({ id: "3", date: "2024-07-01", amount: 3000 }), + createMockTransaction({ id: "4", date: "2024-05-30", amount: 4000 }), + createMockTransaction({ + id: "5", + date: "2024-06-30", + amount: 5000, + type: "income", + }), ]; - it('filters transactions by month correctly', () => { - const result = filterTransactionsByMonth(mockTransactions, '2024-06'); - + it("filters transactions by month correctly", () => { + const result = filterTransactionsByMonth(mockTransactions, "2024-06"); + expect(result).toHaveLength(2); // Only expense transactions in June - expect(result.map(t => t.id)).toEqual(['1', '2']); - expect(result.every(t => t.type === 'expense')).toBe(true); - expect(result.every(t => t.date.includes('2024-06'))).toBe(true); + expect(result.map((t) => t.id)).toEqual(["1", "2"]); + expect(result.every((t) => t.type === "expense")).toBe(true); + expect(result.every((t) => t.date.includes("2024-06"))).toBe(true); }); - it('returns empty array for non-matching month', () => { - const result = filterTransactionsByMonth(mockTransactions, '2024-12'); + it("returns empty array for non-matching month", () => { + const result = filterTransactionsByMonth(mockTransactions, "2024-12"); expect(result).toHaveLength(0); }); - it('excludes income transactions', () => { - const result = filterTransactionsByMonth(mockTransactions, '2024-06'); - const incomeTransaction = result.find(t => t.type === 'income'); + it("excludes income transactions", () => { + const result = filterTransactionsByMonth(mockTransactions, "2024-06"); + const incomeTransaction = result.find((t) => t.type === "income"); expect(incomeTransaction).toBeUndefined(); }); - it('handles partial month string matching', () => { - const result = filterTransactionsByMonth(mockTransactions, '2024-0'); + it("handles partial month string matching", () => { + const result = filterTransactionsByMonth(mockTransactions, "2024-0"); expect(result).toHaveLength(4); // All expense transactions with '2024-0' in date (includes 2024-05, 2024-06, 2024-07) }); - it('handles empty transaction array', () => { - const result = filterTransactionsByMonth([], '2024-06'); + it("handles empty transaction array", () => { + const result = filterTransactionsByMonth([], "2024-06"); expect(result).toEqual([]); }); - it('handles empty month string', () => { - const result = filterTransactionsByMonth(mockTransactions, ''); + it("handles empty month string", () => { + const result = filterTransactionsByMonth(mockTransactions, ""); expect(result).toHaveLength(4); // All expense transactions (empty string matches all) }); }); - describe('filterTransactionsByQuery', () => { + describe("filterTransactionsByQuery", () => { const mockTransactions: Transaction[] = [ - createMockTransaction({ - id: '1', - title: 'Coffee Shop', - category: 'Food', - amount: 5000 + createMockTransaction({ + id: "1", + title: "Coffee Shop", + category: "Food", + amount: 5000, }), - createMockTransaction({ - id: '2', - title: 'Grocery Store', - category: 'Food', - amount: 30000 + createMockTransaction({ + id: "2", + title: "Grocery Store", + category: "Food", + amount: 30000, }), - createMockTransaction({ - id: '3', - title: 'Gas Station', - category: 'Transportation', - amount: 50000 + createMockTransaction({ + id: "3", + title: "Gas Station", + category: "Transportation", + amount: 50000, }), - createMockTransaction({ - id: '4', - title: 'Restaurant', - category: 'Dining', - amount: 25000 + createMockTransaction({ + id: "4", + title: "Restaurant", + category: "Dining", + amount: 25000, }), ]; - it('filters by transaction title (case insensitive)', () => { - const result = filterTransactionsByQuery(mockTransactions, 'coffee'); + it("filters by transaction title (case insensitive)", () => { + const result = filterTransactionsByQuery(mockTransactions, "coffee"); expect(result).toHaveLength(1); - expect(result[0].title).toBe('Coffee Shop'); + expect(result[0].title).toBe("Coffee Shop"); }); - it('filters by category (case insensitive)', () => { - const result = filterTransactionsByQuery(mockTransactions, 'food'); + it("filters by category (case insensitive)", () => { + const result = filterTransactionsByQuery(mockTransactions, "food"); expect(result).toHaveLength(2); - expect(result.every(t => t.category === 'Food')).toBe(true); + expect(result.every((t) => t.category === "Food")).toBe(true); }); - it('filters by partial matches', () => { - const result = filterTransactionsByQuery(mockTransactions, 'shop'); + it("filters by partial matches", () => { + const result = filterTransactionsByQuery(mockTransactions, "shop"); expect(result).toHaveLength(1); - expect(result[0].title).toBe('Coffee Shop'); + expect(result[0].title).toBe("Coffee Shop"); }); - it('returns all transactions for empty query', () => { - const result = filterTransactionsByQuery(mockTransactions, ''); + it("returns all transactions for empty query", () => { + const result = filterTransactionsByQuery(mockTransactions, ""); expect(result).toHaveLength(4); expect(result).toEqual(mockTransactions); }); - it('returns all transactions for whitespace-only query', () => { - const result = filterTransactionsByQuery(mockTransactions, ' '); + it("returns all transactions for whitespace-only query", () => { + const result = filterTransactionsByQuery(mockTransactions, " "); expect(result).toHaveLength(4); expect(result).toEqual(mockTransactions); }); - it('handles no matches', () => { - const result = filterTransactionsByQuery(mockTransactions, 'nonexistent'); + it("handles no matches", () => { + const result = filterTransactionsByQuery(mockTransactions, "nonexistent"); expect(result).toHaveLength(0); }); - it('handles special characters in query', () => { + it("handles special characters in query", () => { const specialTransactions = [ - createMockTransaction({ - id: '1', - title: 'Store & More', - category: 'Shopping' + createMockTransaction({ + id: "1", + title: "Store & More", + category: "Shopping", }), - createMockTransaction({ - id: '2', - title: 'Regular Store', - category: 'Shopping' + createMockTransaction({ + id: "2", + title: "Regular Store", + category: "Shopping", }), ]; - const result = filterTransactionsByQuery(specialTransactions, '&'); + const result = filterTransactionsByQuery(specialTransactions, "&"); expect(result).toHaveLength(1); - expect(result[0].title).toBe('Store & More'); + expect(result[0].title).toBe("Store & More"); }); - it('trims whitespace from query', () => { - const result = filterTransactionsByQuery(mockTransactions, ' coffee '); + it("trims whitespace from query", () => { + const result = filterTransactionsByQuery(mockTransactions, " coffee "); expect(result).toHaveLength(1); - expect(result[0].title).toBe('Coffee Shop'); + expect(result[0].title).toBe("Coffee Shop"); }); - it('matches both title and category in single query', () => { - const result = filterTransactionsByQuery(mockTransactions, 'o'); // matches 'Coffee', 'Food', etc. + it("matches both title and category in single query", () => { + const result = filterTransactionsByQuery(mockTransactions, "o"); // matches 'Coffee', 'Food', etc. expect(result.length).toBeGreaterThan(0); }); - it('handles empty transaction array', () => { - const result = filterTransactionsByQuery([], 'test'); + it("handles empty transaction array", () => { + const result = filterTransactionsByQuery([], "test"); expect(result).toEqual([]); }); }); - describe('calculateTotalExpenses', () => { - it('calculates total for multiple transactions', () => { + describe("calculateTotalExpenses", () => { + it("calculates total for multiple transactions", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 1000 }), createMockTransaction({ amount: 2000 }), @@ -175,12 +182,12 @@ describe('transactionUtils', () => { expect(result).toBe(6000); }); - it('returns 0 for empty array', () => { + it("returns 0 for empty array", () => { const result = calculateTotalExpenses([]); expect(result).toBe(0); }); - it('handles single transaction', () => { + it("handles single transaction", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 5000 }), ]; @@ -189,7 +196,7 @@ describe('transactionUtils', () => { expect(result).toBe(5000); }); - it('handles zero amounts', () => { + it("handles zero amounts", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 0 }), createMockTransaction({ amount: 1000 }), @@ -200,7 +207,7 @@ describe('transactionUtils', () => { expect(result).toBe(1000); }); - it('handles negative amounts (refunds)', () => { + it("handles negative amounts (refunds)", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 1000 }), createMockTransaction({ amount: -500 }), // refund @@ -211,7 +218,7 @@ describe('transactionUtils', () => { expect(result).toBe(2500); }); - it('handles large amounts', () => { + it("handles large amounts", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 999999 }), createMockTransaction({ amount: 1 }), @@ -221,7 +228,7 @@ describe('transactionUtils', () => { expect(result).toBe(1000000); }); - it('handles decimal amounts (though typically avoided)', () => { + it("handles decimal amounts (though typically avoided)", () => { const transactions: Transaction[] = [ createMockTransaction({ amount: 1000.5 }), createMockTransaction({ amount: 999.5 }), @@ -232,46 +239,49 @@ describe('transactionUtils', () => { }); }); - describe('integration tests', () => { + describe("integration tests", () => { const mockTransactions: Transaction[] = [ - createMockTransaction({ - id: '1', - title: 'Coffee Shop', + createMockTransaction({ + id: "1", + title: "Coffee Shop", amount: 5000, - date: '2024-06-01', - category: 'Food', - type: 'expense' + date: "2024-06-01", + category: "Food", + type: "expense", }), - createMockTransaction({ - id: '2', - title: 'Grocery Store', + createMockTransaction({ + id: "2", + title: "Grocery Store", amount: 30000, - date: '2024-06-15', - category: 'Food', - type: 'expense' + date: "2024-06-15", + category: "Food", + type: "expense", }), - createMockTransaction({ - id: '3', - title: 'Gas Station', + createMockTransaction({ + id: "3", + title: "Gas Station", amount: 50000, - date: '2024-07-01', - category: 'Transportation', - type: 'expense' + date: "2024-07-01", + category: "Transportation", + type: "expense", }), - createMockTransaction({ - id: '4', - title: 'Salary', + createMockTransaction({ + id: "4", + title: "Salary", amount: 3000000, - date: '2024-06-01', - category: 'Income', - type: 'income' + date: "2024-06-01", + category: "Income", + type: "income", }), ]; - it('chains filtering operations correctly', () => { + it("chains filtering operations correctly", () => { // Filter by month, then by query, then calculate total - const monthFiltered = filterTransactionsByMonth(mockTransactions, '2024-06'); - const queryFiltered = filterTransactionsByQuery(monthFiltered, 'food'); + const monthFiltered = filterTransactionsByMonth( + mockTransactions, + "2024-06" + ); + const queryFiltered = filterTransactionsByQuery(monthFiltered, "food"); const total = calculateTotalExpenses(queryFiltered); expect(monthFiltered).toHaveLength(2); // Only June expenses @@ -279,9 +289,12 @@ describe('transactionUtils', () => { expect(total).toBe(35000); // 5000 + 30000 }); - it('handles empty results in chained operations', () => { - const monthFiltered = filterTransactionsByMonth(mockTransactions, '2024-12'); - const queryFiltered = filterTransactionsByQuery(monthFiltered, 'any'); + it("handles empty results in chained operations", () => { + const monthFiltered = filterTransactionsByMonth( + mockTransactions, + "2024-12" + ); + const queryFiltered = filterTransactionsByQuery(monthFiltered, "any"); const total = calculateTotalExpenses(queryFiltered); expect(monthFiltered).toHaveLength(0); @@ -289,4 +302,4 @@ describe('transactionUtils', () => { expect(total).toBe(0); }); }); -}); \ No newline at end of file +}); diff --git a/vercel.json b/vercel.json index 165b242..69d156b 100644 --- a/vercel.json +++ b/vercel.json @@ -65,4 +65,4 @@ "includeFiles": "dist/**" } } -} \ No newline at end of file +} diff --git a/vitest.config.ts b/vitest.config.ts index 8b3d69f..34aca4a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,7 @@ /// -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' -import path from 'path' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; export default defineConfig({ plugins: [react()], @@ -12,51 +12,51 @@ export default defineConfig({ }, test: { // ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - environment: 'jsdom', - + environment: "jsdom", + // ์ „์—ญ ์„ค์ • ํŒŒ์ผ - setupFiles: ['./src/setupTests.ts'], - + setupFiles: ["./src/setupTests.ts"], + // ์ „์—ญ ๋ณ€์ˆ˜ ์„ค์ • globals: true, - + // CSS ๋ชจ๋“ˆ ๋ฐ ์Šคํƒ€์ผ ํŒŒ์ผ ๋ฌด์‹œ css: { modules: { - classNameStrategy: 'non-scoped', + classNameStrategy: "non-scoped", }, }, - + // ํฌํ•จํ•  ํŒŒ์ผ ํŒจํ„ด - include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - + include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + // ์ œ์™ธํ•  ํŒŒ์ผ ํŒจํ„ด exclude: [ - 'node_modules', - 'dist', - '.git', - '.cache', - 'ccusage/**', - '**/.{idea,git,cache,output,temp}/**', - '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*' + "node_modules", + "dist", + ".git", + ".cache", + "ccusage/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", ], - + // ์ปค๋ฒ„๋ฆฌ์ง€ ์„ค์ • coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], + provider: "v8", + reporter: ["text", "json", "html"], exclude: [ - 'node_modules/', - 'src/setupTests.ts', - '**/*.d.ts', - '**/*.config.{js,ts}', - '**/index.ts', // ๋‹จ์ˆœ re-export ํŒŒ์ผ๋“ค - 'src/main.tsx', // ์•ฑ ์ง„์ž…์  - 'src/vite-env.d.ts', - '**/*.stories.{js,ts,jsx,tsx}', // Storybook ํŒŒ์ผ - 'coverage/**', - 'dist/**', - '**/.{git,cache,output,temp}/**', + "node_modules/", + "src/setupTests.ts", + "**/*.d.ts", + "**/*.config.{js,ts}", + "**/index.ts", // ๋‹จ์ˆœ re-export ํŒŒ์ผ๋“ค + "src/main.tsx", // ์•ฑ ์ง„์ž…์  + "src/vite-env.d.ts", + "**/*.stories.{js,ts,jsx,tsx}", // Storybook ํŒŒ์ผ + "coverage/**", + "dist/**", + "**/.{git,cache,output,temp}/**", ], // 80% ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ thresholds: { @@ -68,39 +68,40 @@ export default defineConfig({ }, }, }, - + // ํ…Œ์ŠคํŠธ ์‹คํ–‰ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ env: { - NODE_ENV: 'test', - VITE_APPWRITE_URL: 'https://test.appwrite.io/v1', - VITE_APPWRITE_PROJECT_ID: 'test-project-id', - VITE_APPWRITE_DATABASE_ID: 'test-database-id', - VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID: 'test-transactions-collection-id', + NODE_ENV: "test", + VITE_APPWRITE_URL: "https://test.appwrite.io/v1", + VITE_APPWRITE_PROJECT_ID: "test-project-id", + VITE_APPWRITE_DATABASE_ID: "test-database-id", + VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID: + "test-transactions-collection-id", }, - + // ํ…Œ์ŠคํŠธ ๋ณ‘๋ ฌ ์‹คํ–‰ ๋ฐ ์„ฑ๋Šฅ ์„ค์ • - pool: 'threads', + pool: "threads", poolOptions: { threads: { singleThread: false, }, }, - + // ํ…Œ์ŠคํŠธ ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์ง€ watch: { - ignore: ['**/node_modules/**', '**/dist/**'], + ignore: ["**/node_modules/**", "**/dist/**"], }, - + // ๋กœ๊ทธ ๋ ˆ๋ฒจ ์„ค์ • - logLevel: 'info', - + logLevel: "info", + // ์žฌ์‹œ๋„ ์„ค์ • retry: 2, - + // ํ…Œ์ŠคํŠธ ํƒ€์ž„์•„์›ƒ (๋ฐ€๋ฆฌ์ดˆ) testTimeout: 10000, - + // ํ›… ํƒ€์ž„์•„์›ƒ (๋ฐ€๋ฆฌ์ดˆ) hookTimeout: 10000, }, -}); \ No newline at end of file +});