diff --git a/.cursor/commands/commit-convention.md b/.cursor/commands/commit-convention.md new file mode 100644 index 0000000..5bc50a7 --- /dev/null +++ b/.cursor/commands/commit-convention.md @@ -0,0 +1,282 @@ +# Git Commit Convention (Google Style) + +## ๐Ÿ“ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ํ˜•์‹ + +Google์˜ ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ํ˜•์‹์ž…๋‹ˆ๋‹ค. + +### ๊ธฐ๋ณธ ๊ตฌ์กฐ + +``` +<ํƒ€์ž…>(<๋ฒ”์œ„>): <์ œ๋ชฉ> + +<๋ณธ๋ฌธ> + +<ํ‘ธํ„ฐ> +``` + +### 1. ํƒ€์ž… (Type) + +์ปค๋ฐ‹์˜ ์„ฑ๊ฒฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค: + +| ํƒ€์ž… | ์„ค๋ช… | ์˜ˆ์‹œ | +| ---------- | ---------------------------------------------- | -------------------------------------------- | +| `feat` | ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ | `feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€` | +| `fix` | ๋ฒ„๊ทธ ์ˆ˜์ • | `fix(ui): ๋ชจ๋ฐ”์ผ ๋ชจ๋‹ฌ z-index ์ˆ˜์ •` | +| `docs` | ๋ฌธ์„œ ์ˆ˜์ • | `docs(readme): ํŽ˜์ด์ง€ ๊ตฌ์กฐ ์„ค๋ช… ์ถ”๊ฐ€` | +| `style` | ์ฝ”๋“œ ํฌ๋งทํŒ…, ์„ธ๋ฏธ์ฝœ๋ก  ๋ˆ„๋ฝ ๋“ฑ (๊ธฐ๋Šฅ ๋ณ€๊ฒฝ ์—†์Œ) | `style(components): Prettier ํฌ๋งทํŒ… ์ ์šฉ` | +| `refactor` | ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง (๊ธฐ๋Šฅ ๋ณ€๊ฒฝ ์—†์Œ) | `refactor(services): ์—ฌํ–‰ ์„œ๋น„์Šค ๋กœ์ง ๊ฐœ์„ ` | +| `perf` | ์„ฑ๋Šฅ ๊ฐœ์„  | `perf(api): ์ฝ”์Šค ์กฐํšŒ ์ฟผ๋ฆฌ ์ตœ์ ํ™”` | +| `test` | ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€ ๋˜๋Š” ์ˆ˜์ • | `test(courses): ์ฝ”์Šค ์ƒ์„ฑ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€` | +| `chore` | ๋นŒ๋“œ ์—…๋ฌด ์ˆ˜์ •, ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ์„ค์ • ๋“ฑ | `chore(deps): Next.js 16.0.7 ์—…๊ทธ๋ ˆ์ด๋“œ` | +| `ci` | CI/CD ์„ค์ • ๋ณ€๊ฒฝ | `ci(github): GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ ์ถ”๊ฐ€` | +| `build` | ๋นŒ๋“œ ์‹œ์Šคํ…œ ๋˜๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ ๋ณ€๊ฒฝ | `build(webpack): ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”` | +| `revert` | ์ด์ „ ์ปค๋ฐ‹ ๋˜๋Œ๋ฆฌ๊ธฐ | `revert: "feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ"` | + +### 2. ๋ฒ”์œ„ (Scope) + +๋ณ€๊ฒฝ์ด ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ๋ฒ”์œ„๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. ์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉด ํฌํ•จํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. + +**์ผ๋ฐ˜์ ์ธ ๋ฒ”์œ„:** + +- `api`: API ๋ผ์šฐํŠธ +- `components`: React ์ปดํฌ๋„ŒํŠธ +- `services`: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„œ๋น„์Šค +- `ui`: UI ์ปดํฌ๋„ŒํŠธ +- `hooks`: React ํ›… +- `utils`: ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +- `types`: TypeScript ํƒ€์ž… ์ •์˜ +- `config`: ์„ค์ • ํŒŒ์ผ +- `docs`: ๋ฌธ์„œ + +**์˜ˆ์‹œ:** + +``` +feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +fix(ui): ๋ชจ๋‹ฌ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ • +refactor(services): ์—ฌํ–‰ ์„œ๋น„์Šค ๋ฆฌํŒฉํ† ๋ง +``` + +### 3. ์ œ๋ชฉ (Subject) + +์ปค๋ฐ‹์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +**๊ทœ์น™:** + +- 50์ž ์ด๋‚ด๋กœ ์ž‘์„ฑ +- ์ฒซ ๊ธ€์ž๋Š” ๋Œ€๋ฌธ์ž๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Œ (๋ฌธ์žฅ์ด ์•„๋‹Œ ๋ช…๋ นํ˜•์œผ๋กœ) +- ๋งˆ์นจํ‘œ(.)๋กœ ๋๋‚˜์ง€ ์•Š์Œ +- ํ˜„์žฌํ˜• ๋™์‚ฌ ์‚ฌ์šฉ (๊ณผ๊ฑฐํ˜•, ์ง„ํ–‰ํ˜• ์ง€์–‘) +- ๋ฌด์—‡์„ ํ–ˆ๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œํ˜„ + +**์ข‹์€ ์˜ˆ:** + +``` +feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +fix(ui): ๋ชจ๋ฐ”์ผ์—์„œ ๋ชจ๋‹ฌ์ด ๊นจ์ง€๋Š” ๋ฌธ์ œ ์ˆ˜์ • +refactor(api): Supabase ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ๋กœ์ง ๊ฐœ์„  +``` + +**๋‚˜์œ ์˜ˆ:** + +``` +feat: ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. (#12) +fix: ๋ชจ๋‹ฌ ๋ฒ„๊ทธ ์ˆ˜์ • +refactor: ์ฝ”๋“œ ๊ฐœ์„  +``` + +### 4. ๋ณธ๋ฌธ (Body) + +์ปค๋ฐ‹์˜ ์ƒ์„ธํ•œ ์„ค๋ช…์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ ๋ณต์žกํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +**๊ทœ์น™:** + +- ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ ์‚ฌ์ด์— ๋นˆ ์ค„ ํ•˜๋‚˜ +- 72์ž๋งˆ๋‹ค ์ค„๋ฐ”๊ฟˆ +- ๋ฌด์—‡์„ ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€, ์™œ ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ ์„ค๋ช… +- ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ (ํ•„์š” ์‹œ) +- ๋ณ€๊ฒฝ ์ „ํ›„ ๋น„๊ต (ํ•„์š” ์‹œ) + +**์˜ˆ์‹œ:** + +``` +feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ฝ”์Šค๋ฅผ ๊ณต๊ฐœํ•˜์—ฌ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์™€ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. + +์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: +- ์ฝ”์Šค ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€ ๊ธฐ๋Šฅ +- ๊ณต๊ฐœ ์ฝ”์Šค ์กฐํšŒ API ์—”๋“œํฌ์ธํŠธ +- ์ข‹์•„์š”/์ €์žฅ ๊ธฐ๋Šฅ ์—ฐ๋™ +- ๊ฒŒ์ด๋ฏธํ”ผ์ผ€์ด์…˜ ๋ณด์ƒ ์‹œ์Šคํ…œ ์—ฐ๋™ + +Closes #12 +``` + +### 5. ํ‘ธํ„ฐ (Footer) + +์ปค๋ฐ‹๊ณผ ๊ด€๋ จ๋œ ๋ฉ”ํƒ€ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +**์ผ๋ฐ˜์ ์ธ ํ‘ธํ„ฐ:** + +- `Closes #์ด์Šˆ๋ฒˆํ˜ธ`: ์ด์Šˆ๋ฅผ ๋‹ซ์Œ +- `Fixes #์ด์Šˆ๋ฒˆํ˜ธ`: ๋ฒ„๊ทธ ์ด์Šˆ๋ฅผ ์ˆ˜์ •ํ•จ +- `Refs #์ด์Šˆ๋ฒˆํ˜ธ`: ๊ด€๋ จ ์ด์Šˆ ์ฐธ์กฐ +- `BREAKING CHANGE`: ๋ธŒ๋ ˆ์ดํ‚น ์ฒด์ธ์ง€ ์„ค๋ช… + +**์˜ˆ์‹œ:** + +``` +feat(api): ์ƒˆ๋กœ์šด ์ธ์ฆ ์‹œ์Šคํ…œ ๋„์ž… + +BREAKING CHANGE: ๊ธฐ์กด JWT ํ† ํฐ ๋ฐฉ์‹์—์„œ Supabase Auth๋กœ ์ „ํ™˜ +๊ธฐ์กด ํ† ํฐ์€ ๋” ์ด์ƒ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +Closes #45 +Refs #30 +``` + +## ๐Ÿ“‹ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ์˜ˆ์‹œ + +### ๊ฐ„๋‹จํ•œ ๋ฒ„๊ทธ ์ˆ˜์ • + +``` +fix(ui): ๋ชจ๋ฐ”์ผ ๋ชจ๋‹ฌ z-index ์ถฉ๋Œ ํ•ด๊ฒฐ +``` + +### ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (๋ณธ๋ฌธ ํฌํ•จ) + +``` +feat(courses): ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ฝ”์Šค๋ฅผ ๊ณต๊ฐœํ•˜์—ฌ ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. + +์ฃผ์š” ๊ตฌํ˜„: +- ์ฝ”์Šค ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€ UI +- POST /api/user-courses/[id]/publish ์—”๋“œํฌ์ธํŠธ +- ๊ณต๊ฐœ ์ฝ”์Šค ์กฐํšŒ ํŽ˜์ด์ง€ (/courses) +- ์ข‹์•„์š”/์ €์žฅ ๊ธฐ๋Šฅ ์—ฐ๋™ + +Closes #12 +``` + +### ๋ฆฌํŒฉํ† ๋ง + +``` +refactor(services): ์—ฌํ–‰ ์„œ๋น„์Šค ๋กœ์ง ๊ฐœ์„  + +์—ฌํ–‰ ๊ณ„ํš ์ƒ์„ฑ ๋กœ์ง์„ ๋” ๋ช…ํ™•ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋ฆฌํŒฉํ† ๋งํ–ˆ์Šต๋‹ˆ๋‹ค. + +๋ณ€๊ฒฝ์‚ฌํ•ญ: +- ํ•จ์ˆ˜ ๋ถ„๋ฆฌ๋กœ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ +- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  +- ํƒ€์ž… ์•ˆ์ •์„ฑ ๊ฐ•ํ™” + +Refs #30 +``` + +### ์„ฑ๋Šฅ ๊ฐœ์„  + +``` +perf(api): ์ฝ”์Šค ์กฐํšŒ ์ฟผ๋ฆฌ ์ตœ์ ํ™” + +๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•˜์—ฌ ์ฝ”์Šค ์กฐํšŒ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +- ์ธ๋ฑ์Šค ์ถ”๊ฐ€๋กœ ์กฐํšŒ ์†๋„ 50% ํ–ฅ์ƒ +- ๋ถˆํ•„์š”ํ•œ JOIN ์ œ๊ฑฐ +- ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ ์šฉ + +Refs #25 +``` + +### ๋ธŒ๋ ˆ์ดํ‚น ์ฒด์ธ์ง€ + +``` +feat(auth): Supabase Auth๋กœ ์ „ํ™˜ + +๊ธฐ์กด JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ์„ Supabase Auth๋กœ ์ „ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค. + +BREAKING CHANGE: +- ๊ธฐ์กด JWT ํ† ํฐ์€ ๋” ์ด์ƒ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค +- ๋ชจ๋“  ์‚ฌ์šฉ์ž๋Š” ์žฌ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค +- API ์—”๋“œํฌ์ธํŠธ์˜ ์ธ์ฆ ๋ฐฉ์‹์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ: docs/migration/auth.md + +Closes #50 +``` + +## ๐Ÿšซ ํ”ผํ•ด์•ผ ํ•  ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ + +### ๋‚˜์œ ์˜ˆ์‹œ๋“ค + +``` +fix: ๋ฒ„๊ทธ ์ˆ˜์ • +``` + +โ†’ ๋ฌด์—‡์„ ์ˆ˜์ •ํ–ˆ๋Š”์ง€ ๋ถˆ๋ช…ํ™• + +``` +update: ํŒŒ์ผ ์ˆ˜์ • +``` + +โ†’ ํƒ€์ž…์ด ์—†๊ณ  ๋ฒ”์œ„๊ฐ€ ๋ถˆ๋ช…ํ™• + +``` +WIP +``` + +โ†’ ์ž‘์—… ์ค‘์ธ ๋‚ด์šฉ์ด ๋ฌด์—‡์ธ์ง€ ์•Œ ์ˆ˜ ์—†์Œ + +``` +fix typo +``` + +โ†’ ๋ฒ”์œ„์™€ ํƒ€์ž…์ด ์—†์Œ + +``` +feat: ์ฝ”์Šค ๊ณต๊ฐœ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค (#12) +``` + +โ†’ ์ œ๋ชฉ์ด ๋„ˆ๋ฌด ๊ธธ๊ณ  ์„ค๋ช…๋ฌธ ํ˜•์‹ + +## โœ… ์ปค๋ฐ‹ ์ž‘์„ฑ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +์ปค๋ฐ‹ ์ „ ๋‹ค์Œ ์‚ฌํ•ญ์„ ํ™•์ธํ•˜์„ธ์š”: + +- [ ] ํƒ€์ž…์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ ํƒ๋˜์—ˆ๋Š”๊ฐ€? +- [ ] ๋ฒ”์œ„๊ฐ€ ๋ช…ํ™•ํ•œ๊ฐ€? +- [ ] ์ œ๋ชฉ์ด 50์ž ์ด๋‚ด์ด๊ณ  ๋ช…ํ™•ํ•œ๊ฐ€? +- [ ] ๋ณธ๋ฌธ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ถฉ๋ถ„ํžˆ ์„ค๋ช…๋˜์—ˆ๋Š”๊ฐ€? +- [ ] ๊ด€๋ จ ์ด์Šˆ ๋ฒˆํ˜ธ๊ฐ€ ํฌํ•จ๋˜์—ˆ๋Š”๊ฐ€? +- [ ] ๋ธŒ๋ ˆ์ดํ‚น ์ฒด์ธ์ง€๊ฐ€ ๋ฌธ์„œํ™”๋˜์—ˆ๋Š”๊ฐ€? +- [ ] ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ์ด ํฌํ•จ๋˜์ง€ ์•Š์•˜๋Š”๊ฐ€? + +## ๐Ÿ”ง ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ์ž‘์„ฑ ํŒ + +### 1. ์ปค๋ฐ‹ ๋‹จ์œ„ + +- **ํ•˜๋‚˜์˜ ์ปค๋ฐ‹์€ ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ์  ๋ณ€๊ฒฝ์‚ฌํ•ญ๋งŒ ํฌํ•จ** +- ๊ด€๋ จ ์—†๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ๋ณ„๋„ ์ปค๋ฐ‹์œผ๋กœ ๋ถ„๋ฆฌ +- ๋„ˆ๋ฌด ์ž‘์€ ์ปค๋ฐ‹๋ณด๋‹ค๋Š” ์˜๋ฏธ ์žˆ๋Š” ๋‹จ์œ„๋กœ ๋ฌถ๊ธฐ + +### 2. ์ปค๋ฐ‹ ๋นˆ๋„ + +- ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ์ปค๋ฐ‹ +- ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ›„ ์ปค๋ฐ‹ +- ๋ฆฌ๋ทฐ ์ „์— ์˜๋ฏธ ์žˆ๋Š” ์ปค๋ฐ‹์œผ๋กœ ์ •๋ฆฌ + +### 3. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ๊ฐœ์„  + +- ์ปค๋ฐ‹ ์ „ ๋ฉ”์‹œ์ง€ ์žฌ๊ฒ€ํ†  +- ํŒ€์›์ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ๋ช…ํ™•ํ•œ ์„ค๋ช… +- ์™œ ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ ์„ค๋ช… (ํ•„์š” ์‹œ) + +## ๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ + +- [Google Commit Convention](https://google.github.io/eng-practices/review/developer/cl-descriptions.html) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [PR ๊ทœ์น™](./pr-guidelines.md) + +--- + +**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ**: 2025๋…„ diff --git a/.cursor/commands/create-api-route.md b/.cursor/commands/create-api-route.md new file mode 100644 index 0000000..f5d453f --- /dev/null +++ b/.cursor/commands/create-api-route.md @@ -0,0 +1,256 @@ +# Create API Route Guide + +Use this command to scaffold a new Next.js API route following project patterns. + +## Route Handler Structure + +### Basic GET Route + +```typescript +// app/api/{resource}/route.ts +import { NextRequest, NextResponse } from "next/server" +import { createClient } from "@lovetrip/api/supabase/server" + +export async function GET(request: NextRequest) { + try { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) { + return NextResponse.json({ error: "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }, { status: 401 }) + } + + const { data, error } = await supabase.from("table").select("*") + + if (error) throw error + + return NextResponse.json({ success: true, data }) + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" }, + { status: 500 } + ) + } +} +``` + +### POST Route with Body + +```typescript +export async function POST(request: NextRequest) { + try { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) { + return NextResponse.json({ error: "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }, { status: 401 }) + } + + const body = await request.json() + + // Validation + if (!body.title) { + return NextResponse.json({ error: "์ œ๋ชฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค" }, { status: 400 }) + } + + const { data, error } = await supabase + .from("table") + .insert({ + user_id: user.id, + ...body, + }) + .select() + .single() + + if (error) throw error + + return NextResponse.json({ success: true, data }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" }, + { status: 500 } + ) + } +} +``` + +## Route Structure + +### Single Resource + +``` +app/api/travel-plans/ +โ””โ”€โ”€ route.ts +``` + +### Nested Resource + +``` +app/api/travel-plans/ +โ””โ”€โ”€ [id]/ + โ”œโ”€โ”€ route.ts + โ””โ”€โ”€ budget/ + โ””โ”€โ”€ route.ts +``` + +### Dynamic Segments + +```typescript +// app/api/travel-plans/[id]/route.ts +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params + // Use id +} +``` + +## Authentication + +### Check Authentication + +```typescript +const supabase = await createClient() +const { + data: { user }, +} = await supabase.auth.getUser() + +if (!user) { + return NextResponse.json({ error: "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }, { status: 401 }) +} +``` + +## Request Handling + +### Query Parameters + +```typescript +const { searchParams } = new URL(request.url) +const page = searchParams.get("page") || "1" +const limit = searchParams.get("limit") || "10" +``` + +### Request Body + +```typescript +const body = await request.json() +``` + +### Headers + +```typescript +const contentType = request.headers.get("content-type") +``` + +## Response Patterns + +### Success Response + +```typescript +return NextResponse.json({ + success: true, + data: result, +}) +``` + +### Error Response + +```typescript +return NextResponse.json({ error: "์—๋Ÿฌ ๋ฉ”์‹œ์ง€" }, { status: 400 }) +``` + +### Status Codes + +- 200: Success (GET, PUT, PATCH) +- 201: Created (POST) +- 204: No Content (DELETE) +- 400: Bad Request (validation errors) +- 401: Unauthorized (not authenticated) +- 403: Forbidden (not authorized) +- 404: Not Found +- 500: Internal Server Error + +## Error Handling + +### Try-Catch Pattern + +```typescript +try { + // Operation +} catch (error) { + console.error("Error:", error) + return NextResponse.json( + { error: error instanceof Error ? error.message : "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" }, + { status: 500 } + ) +} +``` + +### Supabase Errors + +```typescript +const { data, error } = await supabase.from("table").select("*") + +if (error) { + console.error("Supabase error:", error) + return NextResponse.json({ error: "๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" }, { status: 500 }) +} +``` + +## Validation + +### Input Validation + +```typescript +const body = await request.json() + +if (!body.title || typeof body.title !== "string") { + return NextResponse.json({ error: "์ œ๋ชฉ์€ ํ•„์ˆ˜์ด๋ฉฐ ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" }, { status: 400 }) +} + +if (body.title.length > 100) { + return NextResponse.json({ error: "์ œ๋ชฉ์€ 100์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" }, { status: 400 }) +} +``` + +## Type Safety + +### Request Types + +```typescript +interface CreateRequest { + title: string + description?: string +} + +const body = (await request.json()) as CreateRequest +``` + +### Response Types + +```typescript +interface ApiResponse { + success: boolean + data?: T + error?: string +} + +return NextResponse.json>({ + success: true, + data: result, +}) +``` + +## Checklist + +- [ ] Route file created in app/api/{resource}/route.ts +- [ ] Authentication check included (if needed) +- [ ] Input validation added +- [ ] Error handling with try-catch +- [ ] Proper HTTP status codes +- [ ] Error messages in Korean +- [ ] Type definitions for request/response +- [ ] Supabase server client used +- [ ] Success response format consistent diff --git a/.cursor/commands/create-component.md b/.cursor/commands/create-component.md new file mode 100644 index 0000000..f650ad7 --- /dev/null +++ b/.cursor/commands/create-component.md @@ -0,0 +1,208 @@ +# Create Component Guide + +Use this command to scaffold a new React component following project conventions. + +## Component Structure + +### Basic Component + +```typescript +"use client" // Only if needed + +import { useState } from "react" +import { Button } from "@lovetrip/ui/components" +import type { ComponentProps } from "./types" + +interface ComponentProps { + title: string + description?: string + onAction?: () => void +} + +export function Component({ + title, + description, + onAction, +}: ComponentProps) { + const [state, setState] = useState(false) + + return ( +
+

{title}

+ {description &&

{description}

} + {onAction && } +
+ ) +} +``` + +## Component Location + +### Feature Component + +- Location: `apps/web/src/components/features/{feature}/components/` +- Use when: Component is specific to a feature + +### Shared Component + +- Location: `apps/web/src/components/shared/` +- Use when: Component is used across multiple features + +### UI Component + +- Location: `packages/ui/components/` +- Use when: Component is reusable across projects + +## Props Design + +### Interface Definition + +```typescript +interface ComponentProps { + // Required props + id: string + title: string + + // Optional props + description?: string + className?: string + + // Event handlers + onClick?: () => void + onChange?: (value: string) => void + + // Children + children?: React.ReactNode +} +``` + +### Default Values + +```typescript +export function Component({ + title, + description = "Default description", + variant = "default", +}: ComponentProps) { + // Implementation +} +``` + +## State Management + +### Local State + +```typescript +const [isOpen, setIsOpen] = useState(false) +const [value, setValue] = useState("") +``` + +### Derived State + +```typescript +const isDisabled = !value || isLoading +``` + +## Event Handlers + +### Inline Handlers + +```typescript + +``` + +### Extracted Handlers + +```typescript +const handleClick = (id: string) => { + // Complex logic +} + + +``` + +## Styling + +### Tailwind Classes + +```typescript +
+``` + +### Conditional Classes + +```typescript +import { cn } from "@/lib/utils" + +
+``` + +## Accessibility + +### Semantic HTML + +```typescript + +``` + +### ARIA Attributes + +```typescript + +
+ ) +} +``` + +### Loading and Error States + +```typescript +if (isLoading) { + return +} + +if (error) { + return +} + +return +``` + +## Form Error Handling + +### Field-Level Errors + +```typescript +const [errors, setErrors] = useState>({}) + +const validate = (data: FormData) => { + const newErrors: Record = {} + + if (!data.title) { + newErrors.title = "์ œ๋ชฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค" + } + + if (data.title && data.title.length > 100) { + newErrors.title = "์ œ๋ชฉ์€ 100์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 +} +``` + +### Display Errors + +```typescript + setTitle(e.target.value)} + error={errors.title} +/> +``` + +## Error Logging + +### Server-Side Logging + +```typescript +console.error("Error details:", { + message: error.message, + stack: error.stack, + context: { + userId, + resourceId, + // Additional context + }, +}) +``` + +### Client-Side Logging + +```typescript +// Only log in development +if (process.env.NODE_ENV === "development") { + console.error("Error:", error) +} +``` + +## Best Practices + +1. **Throw in services**, catch in API routes/components +2. **Use appropriate HTTP status codes** +3. **Provide user-friendly messages** in Korean +4. **Log detailed errors** server-side +5. **Don't expose internal details** to clients +6. **Handle async errors** with try-catch +7. **Validate input** before operations +8. **Use error boundaries** for React components +9. **Show loading states** during operations +10. **Provide retry mechanisms** when appropriate diff --git a/.cursor/rules/index.txt b/.cursor/rules/index.txt new file mode 100644 index 0000000..503e805 --- /dev/null +++ b/.cursor/rules/index.txt @@ -0,0 +1,83 @@ +# LOVETRIP Project Base Rules + +## Project Overview + +LOVETRIP is a couple travel recommendation service built with Next.js full-stack application. + +- Monorepo structure (pnpm workspace) +- Next.js 15.2.4 + TypeScript +- Supabase backend +- Naver Maps API + +## Coding Style + +### Prettier Configuration + +- No semicolons (semi: false) +- Use double quotes instead of single quotes (singleQuote: false) +- Tab width: 2 spaces (tabWidth: 2) +- Line length: 100 characters (printWidth: 100) +- Arrow function parentheses: avoid for single parameter (arrowParens: "avoid") +- Line endings: LF (endOfLine: "lf") + +### File Naming Conventions + +- Component files: PascalCase (e.g., HomePageClient.tsx) +- Regular files: kebab-case (e.g., travel-service.ts) +- Hook files: camelCase with "use" prefix (e.g., use-travel-courses.ts) +- Type files: kebab-case (e.g., types.ts, database.ts) + +### Code Writing Principles + +- All responses should be in Korean +- Use clear and concise variable names +- Functions should follow single responsibility principle +- Comments explain "why", code explains "what" +- Avoid magic numbers/strings, define as constants + +### Commit Messages + +- Write in Korean +- Provide clear descriptions +- Format: "type: brief description" (e.g., "feat: add travel plan creation feature") + +## Project Structure + +### Monorepo Packages + +- apps/web: Next.js web application +- packages/ui: UI component library +- packages/api: Supabase client +- packages/shared: Common types and utilities +- packages/\*: Domain-specific packages (user, couple, planner, expense, etc.) + +### Architecture + +- Feature-Sliced Design (FSD) structure +- Domain-Driven Design (DDD) +- Layer separation: Presentation โ†’ Domain โ†’ Data + +## General Rules + +1. Type safety first: Explicitly define TypeScript types +2. Error handling: Include error handling for all async operations +3. Accessibility: Follow web accessibility guidelines +4. Performance: Prevent unnecessary re-renders, utilize code splitting +5. Security: Validate user input, prevent SQL injection (automatically handled by Supabase) + +## Related Rules + +For detailed guidelines, refer to: +- `architecture.txt` - Feature-Sliced Design and Domain-Driven Design patterns +- `monorepo.txt` - Monorepo structure and package management +- `react-nextjs.txt` - React and Next.js best practices +- `typescript.txt` - TypeScript type safety and patterns +- `styling.txt` - Tailwind CSS and component styling +- `testing.txt` - Testing strategies and patterns +- `supabase.txt` - Supabase client usage and RLS policies +- `error-handling.txt` - Error handling patterns and best practices +- `security.txt` - Security guidelines and authentication patterns + +For workflow guides, see `.cursor/commands/`: +- `commit-convention.md` - Git commit message conventions +- `pr-guidelines.md` - Pull request guidelines and templates diff --git a/.cursor/rules/monorepo.txt b/.cursor/rules/monorepo.txt new file mode 100644 index 0000000..a73d42d --- /dev/null +++ b/.cursor/rules/monorepo.txt @@ -0,0 +1,129 @@ +# Monorepo Structure Rules + +## Package Structure + +### Inter-package Dependencies + +- Only unidirectional dependencies allowed: apps/web โ†’ packages/\* +- Minimize dependencies between packages/\* +- Circular dependencies are prohibited +- packages/shared can be used by all other packages + +### Package Exports + +- Explicit exports through index.ts + ```typescript + // packages/planner/index.ts + export * from "./services" + export * from "./components" + export * from "./types" + ``` + +### Internal Package Imports + +- Use @lovetrip/\* format + ```typescript + import { Button } from "@lovetrip/ui/components" + import type { Place } from "@lovetrip/shared/types" + import { createClient } from "@lovetrip/api/supabase/client" + ``` + +### Package Versions + +- Use pnpm workspace +- Use workspace:\* in package.json + ```json + { + "dependencies": { + "@lovetrip/ui": "workspace:*", + "@lovetrip/shared": "workspace:*" + } + } + ``` + +## Package Roles + +### apps/web + +- Next.js application +- Uses packages/\* to implement features +- Minimize direct business logic + +### packages/ui + +- Reusable UI components +- No dependencies on other packages +- Radix UI based components + +### packages/shared + +- Common types, utilities, constants +- No dependencies on other packages +- Includes Database types + +### packages/api + +- Supabase client +- External API clients +- Only depends on packages/shared + +### packages/\* (Domain Packages) + +- Business logic (services/) +- Domain-specific components (components/) +- Domain-specific hooks (hooks/) +- Domain types (types/) +- Can depend on packages/shared, packages/api + +## Package Creation Guide + +### When Creating New Package + +1. Create new folder in packages/ directory +2. Create package.json (name: @lovetrip/{package-name}) +3. Create tsconfig.json (extends root tsconfig.json) +4. Create index.ts for exports +5. Automatically included in pnpm-workspace.yaml + +### Package Structure Example + +``` +packages/{domain}/ +โ”œโ”€โ”€ package.json +โ”œโ”€โ”€ tsconfig.json +โ”œโ”€โ”€ eslint.config.mjs +โ”œโ”€โ”€ index.ts +โ”œโ”€โ”€ services/ +โ”‚ โ”œโ”€โ”€ index.ts +โ”‚ โ””โ”€โ”€ {domain}-service.ts +โ”œโ”€โ”€ components/ +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ hooks/ +โ”‚ โ””โ”€โ”€ index.ts +โ””โ”€โ”€ types/ + โ””โ”€โ”€ index.ts +``` + +## Dependency Management + +### External Dependencies + +- Add only necessary dependencies to each package's package.json +- Common dependencies in root package.json devDependencies + +### Internal Dependencies + +- Add only necessary packages as dependencies +- Avoid unnecessary dependencies + +## Build and Deployment + +### Build Order + +- Build packages/\* first +- Build apps/web last + +### Type Checking + +- Run pnpm type-check from root +- Verify types are correctly linked across packages diff --git a/.cursor/rules/react-nextjs.txt b/.cursor/rules/react-nextjs.txt new file mode 100644 index 0000000..3fcccda --- /dev/null +++ b/.cursor/rules/react-nextjs.txt @@ -0,0 +1,154 @@ +# React & Next.js Rules + +## Next.js App Router + +### Server Components vs Client Components + +- Use Server Components by default +- Use "use client" only when client-side interaction is needed: + - Event handlers (onClick, onChange, etc.) + - State management (useState, useReducer) + - Browser API usage (localStorage, window, etc.) + - useEffect, useLayoutEffect usage + +### File Structure + +- Pages: page.tsx in app/ directory +- Layout: layout.tsx +- Loading: loading.tsx +- Error: error.tsx +- API routes: route.ts in app/api/ directory + +### Data Fetching + +- Fetch data directly in Server Components + + ```typescript + // app/travel/page.tsx + export default async function TravelPage() { + const courses = await getTravelCourses() + return + } + ``` + +- Client Components receive data via props + ```typescript + "use client" + export function TravelPageClient({ courses }: { courses: TravelCourse[] }) { + // client logic + } + ``` + +## React Components + +### Component Structure + +1. "use client" directive (if needed) +2. Import statements +3. Type definitions +4. Component function +5. Export + +### Props Types + +- Define Props types with interface + + ```typescript + interface TravelSidebarProps { + courses: TravelCourse[] + selectedCourse: TravelCourse | null + onCourseSelect: (course: TravelCourse) => void + } + + export function TravelSidebar({ courses, selectedCourse, onCourseSelect }: TravelSidebarProps) { + // implementation + } + ``` + +### Hook Usage Rules + +- Hooks must be called at the top level of function components +- Never call hooks inside conditions, loops, or nested functions +- Custom hooks must use "use" prefix + +### State Management + +- Local state: useState +- Complex state: useReducer +- Server state: Direct fetch or SWR/React Query (if needed) +- Global state: Context API (only when necessary) + +### Event Handlers + +- Inline arrow functions are acceptable (when no performance issue) +- Extract complex logic to separate functions + ```typescript + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + // logic + } + ``` + +## Performance Optimization + +### Memoization + +- React.memo: For components with infrequently changing props +- useMemo: For expensive computed values +- useCallback: For functions passed to child components + +### Code Splitting + +- Use dynamic import + ```typescript + const NaverMapView = dynamic(() => import("@/components/shared/naver-map-view"), { + ssr: false, + }) + ``` + +### Image Optimization + +- Use next/image + + ```typescript + import Image from "next/image" + + {alt} + ``` + +## Form Handling + +### Controlled Components + +- Manage form state with useState +- Update state with onChange handlers +- Validate and submit in onSubmit + +### Error Handling + +- Distinguish between form-level and field-level errors +- Display user-friendly error messages + +## Routing + +### Navigation + +- Use useRouter, usePathname from next/navigation + + ```typescript + import { useRouter } from "next/navigation" + + const router = useRouter() + router.push("/travel") + ``` + +### Dynamic Routes + +- Use [id] folder structure +- Receive params as async (Next.js 15+) + ```typescript + export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + // implementation + } + ``` diff --git a/.cursor/rules/security.txt b/.cursor/rules/security.txt new file mode 100644 index 0000000..28b1479 --- /dev/null +++ b/.cursor/rules/security.txt @@ -0,0 +1,319 @@ +# Security Rules + +## Authentication + +### Always Check Authentication + +```typescript +// In API routes +const supabase = await createClient() +const { + data: { user }, +} = await supabase.auth.getUser() + +if (!user) { + return NextResponse.json( + { error: "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }, + { status: 401 } + ) +} +``` + +### Client-Side Auth Check + +```typescript +"use client" + +import { useEffect } from "react" +import { useRouter } from "next/navigation" +import { createClient } from "@lovetrip/api/supabase/client" + +export function ProtectedComponent() { + const router = useRouter() + const supabase = createClient() + + useEffect(() => { + const checkAuth = async () => { + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) { + router.push("/login") + } + } + + checkAuth() + }, [router]) +} +``` + +## Authorization + +### Resource Ownership Check + +```typescript +// In API routes +const resource = await service.getResource(id) + +if (resource.user_id !== user.id) { + return NextResponse.json( + { error: "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค" }, + { status: 403 } + ) +} +``` + +### Role-Based Access + +```typescript +// Check user role +const { + data: { user }, +} = await supabase.auth.getUser() + +const userRole = user?.user_metadata?.role + +if (userRole !== "admin") { + return NextResponse.json( + { error: "๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }, + { status: 403 } + ) +} +``` + +## Input Validation + +### Validate All User Input + +```typescript +function validateInput(input: unknown): ValidInput { + if (!input || typeof input !== "object") { + throw new ValidationError("Invalid input") + } + + const obj = input as Record + + // Type checks + if (typeof obj.title !== "string") { + throw new ValidationError("Title must be a string") + } + + // Length checks + if (obj.title.length > 100) { + throw new ValidationError("Title must be 100 characters or less") + } + + // Format checks + if (!/^[a-zA-Z0-9\s]+$/.test(obj.title)) { + throw new ValidationError("Title contains invalid characters") + } + + return obj as ValidInput +} +``` + +### Sanitize User Input + +```typescript +// Remove potentially dangerous characters +function sanitizeInput(input: string): string { + return input + .trim() + .replace(/[<>]/g, "") // Remove HTML brackets + .slice(0, 1000) // Limit length +} +``` + +## SQL Injection Prevention + +### Use Supabase Client (Automatic Protection) + +```typescript +// โœ… Safe - Supabase handles parameterization +const { data } = await supabase + .from("table") + .select("*") + .eq("id", userId) // Automatically parameterized +``` + +### Never Use Raw SQL with User Input + +```typescript +// โŒ NEVER DO THIS +const query = `SELECT * FROM table WHERE id = '${userId}'` + +// โœ… Use Supabase client methods +const { data } = await supabase + .from("table") + .select("*") + .eq("id", userId) +``` + +## XSS Prevention + +### Sanitize HTML Content + +```typescript +// Use libraries like DOMPurify for HTML content +import DOMPurify from "isomorphic-dompurify" + +const sanitized = DOMPurify.sanitize(userInput) +``` + +### React's Built-in Protection + +```typescript +// React automatically escapes content +
{userInput}
// Safe + +// โš ๏ธ Dangerous - only use with sanitized content +
+``` + +## CSRF Protection + +### Next.js Built-in Protection + +- Next.js API routes are protected by default +- Use SameSite cookies for authentication +- Verify origin for sensitive operations + +## Environment Variables + +### Never Commit Secrets + +```typescript +// โœ… Use environment variables +const apiKey = process.env.NEXT_PUBLIC_API_KEY + +// โŒ Never hardcode secrets +const apiKey = "secret-key-123" +``` + +### Environment Variable Naming + +- `NEXT_PUBLIC_*`: Exposed to client (safe for public keys) +- No prefix: Server-only (never exposed to client) + +### Access Control + +```typescript +// Server-only variables +const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY + +// Public variables (safe to expose) +const publicKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY +``` + +## Row Level Security (RLS) + +### Always Enable RLS + +```sql +-- Enable RLS on all user tables +ALTER TABLE public.table_name ENABLE ROW LEVEL SECURITY; +``` + +### Write Secure Policies + +```sql +-- Users can only access their own data +CREATE POLICY "Users access own data" +ON public.table_name +FOR ALL +TO authenticated +USING (auth.uid() = user_id) +WITH CHECK (auth.uid() = user_id); +``` + +### Test RLS Policies + +- Test with different user accounts +- Verify policies work as expected +- Don't rely on client-side checks alone + +## Sensitive Data + +### Never Expose Sensitive Data + +```typescript +// โŒ Don't expose service role key +const response = { + serviceKey: process.env.SUPABASE_SERVICE_ROLE_KEY, // NEVER +} + +// โœ… Only return necessary data +const response = { + id: resource.id, + title: resource.title, + // Don't include sensitive fields +} +``` + +### Hash Passwords + +- Supabase handles password hashing automatically +- Never store plain text passwords +- Use secure password requirements + +## API Security + +### Rate Limiting + +```typescript +// Implement rate limiting for API routes +// Use middleware or external service +``` + +### CORS Configuration + +```typescript +// Configure CORS in next.config.mjs +const nextConfig = { + async headers() { + return [ + { + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Origin", value: "https://yourdomain.com" }, + ], + }, + ] + }, +} +``` + +## Error Messages + +### Don't Expose Internal Details + +```typescript +// โŒ Exposes internal structure +return NextResponse.json( + { error: `Database error: ${error.code} at ${error.file}:${error.line}` }, + { status: 500 } +) + +// โœ… Generic user-friendly message +return NextResponse.json( + { error: "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" }, + { status: 500 } +) + +// Log detailed error server-side +console.error("Internal error:", error) +``` + +## Best Practices + +1. **Always authenticate** before sensitive operations +2. **Check authorization** for resource access +3. **Validate all input** from users +4. **Use RLS policies** for database security +5. **Never expose secrets** in client code +6. **Sanitize user input** before display +7. **Use HTTPS** in production +8. **Keep dependencies updated** (security patches) +9. **Review code** for security vulnerabilities +10. **Test security** regularly diff --git a/.cursor/rules/styling.txt b/.cursor/rules/styling.txt new file mode 100644 index 0000000..298172b --- /dev/null +++ b/.cursor/rules/styling.txt @@ -0,0 +1,183 @@ +# Styling Rules + +## Tailwind CSS + +### Basic Usage + +- Apply styles via className prop +- Minimize inline styles +- Define custom styles in globals.css + +### Class Order + +- Layout โ†’ Style โ†’ State order + ```tsx +
+ ``` + +### Responsive Design + +- Mobile-first approach +- Use sm:, md:, lg: breakpoints + ```tsx +
+ ``` + +## Component Styling + +### UI Component Usage + +- Prefer components from @lovetrip/ui package + ```tsx + import { Button, Card, Input } from "@lovetrip/ui/components" + ``` + +### Custom Styles + +- Use Tailwind utility classes +- Use separate CSS modules or styled-components for complex styles (if needed) + +### Conditional Styles + +- Use clsx or cn utility + + ```tsx + import { cn } from "@/lib/utils" + +
+ ``` + +## Dark Mode + +### Theme Provider + +- Use theme-provider component +- Support system theme detection + +### Dark Mode Styles + +- Use dark: prefix + ```tsx +
+ ``` + +## Responsive Design + +### Breakpoints + +- sm: 640px +- md: 768px +- lg: 1024px +- xl: 1280px +- 2xl: 1536px + +### Mobile First + +- Base styles for mobile +- Add larger screen styles with md:, lg:, etc. + +### Touch Friendly + +- Minimum button size: 44x44px +- Ensure sufficient touch area + +## Animation + +### Framer Motion + +- Use for complex animations + + ```tsx + import { motion } from "framer-motion" + + + ``` + +### CSS Transitions + +- Use Tailwind transition for simple transitions + ```tsx +
+ ``` + +## Accessibility + +### Color Contrast + +- Follow WCAG AA standards (4.5:1 minimum) +- Verify text and background color contrast + +### Focus Indicators + +- All interactive elements must have focus styles + ```tsx + - - -
- ) - } - const days = getDaysInMonth() - const weekDays = ["์ผ", "์›”", "ํ™”", "์ˆ˜", "๋ชฉ", "๊ธˆ", "ํ† "] + const { couple, calendars, partnerInfo, currentUserInfo } = await getCalendarData(user.id) return ( -
-
-

- ์ปคํ”Œ ์บ˜๋ฆฐ๋” -

- {partnerInfo && ( -

- - {partnerInfo.nickname || partnerInfo.name || "ํŒŒํŠธ๋„ˆ"}์™€ ํ•จ๊ป˜ ์ผ์ •์„ ๊ณต์œ ํ•˜์„ธ์š” -

- )} -
- -
- {/* ์‚ฌ์ด๋“œ๋ฐ” */} -
- - - ์บ˜๋ฆฐ๋” - - - {calendars.map(cal => ( - - ))} - - - - - - - - - - ์ƒˆ ์ผ์ • ์ถ”๊ฐ€ - ์ปคํ”Œ๊ณผ ๊ณต์œ ํ•  ์ผ์ •์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” - -
-
- - setNewEvent({ ...newEvent, title: e.target.value })} - placeholder="์ผ์ • ์ œ๋ชฉ" - /> -
-
- - setNewEvent({ ...newEvent, start_time: e.target.value })} - /> -
-
- - setNewEvent({ ...newEvent, end_time: e.target.value })} - /> -
-
- - {selectedPlace ? ( -
-
- {selectedPlace.image_url && ( -
- {selectedPlace.name} -
- )} -
-

{selectedPlace.name}

- {selectedPlace.address && ( -

- {selectedPlace.address} -

- )} -
-
- -
- ) : ( -
-
- { - setPlaceSearchQuery(e.target.value) - searchPlaces(e.target.value) - }} - onFocus={() => setShowPlaceSearch(true)} - /> - -
- {showPlaceSearch && searchResults.length > 0 && ( -
- {searchResults.map(place => ( - - ))} -
- )} -
- )} -
-
- - setNewEvent({ ...newEvent, location: e.target.value })} - placeholder="์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" - disabled={!!selectedPlace} - /> -
-
- -