diff --git a/CLAUDE.md b/CLAUDE.md index 5ab6ec8..38d86d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,19 +1,88 @@ # CLAUDE.md -## 🚫 Must Not +## ν”„λ‘œμ νŠΈ κ°œμš” + +**hear-your-voice (λ„ˆλͺ©λ“€)**λŠ” SOPT μŠ€ν”„λ¦°νŠΈ 회고 μ„€λ¬Έ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μž…λ‹ˆλ‹€. +멀버듀이 μŠ€ν”„λ¦°νŠΈ 인증 μ½”λ“œλ₯Ό μž…λ ₯ν•œ λ’€ Stop / Start / Continue λ°©μ‹μ˜ 회고 ν”Όλ“œλ°±κ³Ό MVPλ₯Ό μ œμΆœν•˜λŠ” ν”Œλ‘œμš°λ‘œ κ΅¬μ„±λ©λ‹ˆλ‹€. + +### νŽ˜μ΄μ§€ ν”Œλ‘œμš° + +``` +StartPage (ν™œμ„± μŠ€ν”„λ¦°νŠΈ μ—¬λΆ€ λΆ„κΈ°) + └─ NoticePage (μ•ˆλ‚΄) + └─ SprintCodePage (인증 μ½”λ“œ μž…λ ₯) + └─ SprintIntroPage (μŠ€ν”„λ¦°νŠΈ μ†Œκ°œ) + └─ UserInfoPage (μ‚¬μš©μž 정보 μž…λ ₯) + └─ (회고 μ„€λ¬Έ μŠ€ν…λ“€ β€” μΆ”ν›„ κ΅¬ν˜„ μ˜ˆμ •) +``` + +--- + +## ν”„λ‘œμ νŠΈ ꡬ쑰 + +``` +src/ +β”œβ”€β”€ assets/ # 정적 이미지 λ¦¬μ†ŒμŠ€ +β”œβ”€β”€ components/ # 곡톡 μž¬μ‚¬μš© μ»΄ν¬λ„ŒνŠΈ +β”‚ β”œβ”€β”€ CodeInput # 인증 μ½”λ“œ μž…λ ₯ ν•„λ“œ +β”‚ β”œβ”€β”€ ContentHeading # νŽ˜μ΄μ§€ 제λͺ© μ˜μ—­ +β”‚ β”œβ”€β”€ ImageSection # 이미지 μ„Ήμ…˜ +β”‚ β”œβ”€β”€ PageLayout # 전체 νŽ˜μ΄μ§€ 래퍼 +β”‚ β”œβ”€β”€ ProgressBar # μŠ€ν… μ§„ν–‰ λ°” +β”‚ β”œβ”€β”€ SelectField # μ…€λ ‰νŠΈ 폼 ν•„λ“œ +β”‚ └── StepLayout # μŠ€ν… 기반 λ ˆμ΄μ•„μ›ƒ (ν•˜λ‹¨ λ²„νŠΌ 포함) +β”œβ”€β”€ context/ +β”‚ └── SubmissionContext.tsx # μ„€λ¬Έ 제좜 데이터 μ „μ—­ μƒνƒœ +β”œβ”€β”€ hooks/ +β”‚ └── useErrorHandler.ts # μ—λŸ¬ 핸듀링 μ»€μŠ€ν…€ ν›… +β”œβ”€β”€ lib/ +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ chapter.ts # 챕터 API +β”‚ β”‚ β”œβ”€β”€ comment.ts # λŒ“κΈ€(회고) API +β”‚ β”‚ β”œβ”€β”€ sprint.ts # μŠ€ν”„λ¦°νŠΈ API +β”‚ β”‚ └── user.ts # μœ μ € API +β”‚ β”œβ”€β”€ apiClient.ts # Supabase 호좜 래퍼 (μ—λŸ¬ λΆ„λ₯˜) +β”‚ β”œβ”€β”€ errors.ts # μ»€μŠ€ν…€ μ—λŸ¬ 클래슀 +β”‚ └── supabase.ts # Supabase ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” +β”œβ”€β”€ pages/ # 라우트 λ‹¨μœ„ νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈ +β”œβ”€β”€ types/ # TypeScript νƒ€μž… μ •μ˜ +β”œβ”€β”€ App.tsx # λΌμš°ν„° μ„€μ • μ§„μž…μ  +└── main.tsx # μ•± 마운트 +``` + +--- + +## 기술 μŠ€νƒ + +| λΆ„λ₯˜ | 기술 | +|------|------| +| ν”„λ ˆμž„μ›Œν¬ | React 18 + TypeScript | +| λΉŒλ“œ | Vite | +| λΌμš°νŒ… | React Router v7 | +| μƒνƒœ 관리 | React Context (`SubmissionContext`) + Zustand (ν•„μš” μ‹œ) | +| λ°±μ—”λ“œ/DB | Supabase (RPC 기반 쿼리) | +| μŠ€νƒ€μΌλ§ | vanilla-extract (`*.css.ts`) | +| λ””μžμΈ μ‹œμŠ€ν…œ | `@sopt-makers/ui`, `@sopt-makers/colors`, `@sopt-makers/fonts`, `@sopt-makers/icons` | +| 린트/포맷 | ESLint + Prettier | + +--- + +## κΈˆμ§€ 사항 - `.pen` 파일 μ ˆλŒ€ μˆ˜μ • κΈˆμ§€ -- ν•˜λ“œμ½”λ”©λœ color / font μ‚¬μš© κΈˆμ§€ +- ν•˜λ“œμ½”λ”©λœ color / font κ°’ μ‚¬μš© κΈˆμ§€ β†’ λ°˜λ“œμ‹œ `@sopt-makers/colors`, `@sopt-makers/fonts` μ‚¬μš© +- Supabase `service_role` key μ‚¬μš© κΈˆμ§€ +- 민감 데이터 ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 직접 쑰회 κΈˆμ§€ β†’ RPC λ˜λŠ” μ„œλ²„ 둜직 μ‚¬μš© -## 🎨 Design System +--- -- color β†’ `@sopt-makers/colors` -- font β†’ `@sopt-makers/fonts` +## λ””μžμΈ μ‹œμŠ€ν…œ -## 🧩 UI Rules +- 색상 β†’ `@sopt-makers/colors` +- 폰트 β†’ `@sopt-makers/fonts` +- UI μ»΄ν¬λ„ŒνŠΈ β†’ 항상 `@sopt-makers/ui` λ¨Όμ € 확인, 없을 λ•Œλ§Œ `src/components/`에 μ»€μŠ€ν…€ κ΅¬ν˜„ -- 항상 `@sopt-makers/ui` λ¨Όμ € μ‚¬μš© -- 없을 λ•Œλ§Œ `/components`에 μ»€μŠ€ν…€ κ΅¬ν˜„ +--- ## πŸ“¦ νŒ¨ν‚€μ§€ ꡬ쑰 @@ -40,6 +109,9 @@ src/ β”‚ β”œβ”€β”€ SprintIntroPage β”‚ β”œβ”€β”€ UserInfoPage β”‚ β”œβ”€β”€ StopCommentPage +β”‚ β”œβ”€β”€ StartCommentPage +β”‚ β”œβ”€β”€ ContinueCommentPage +β”‚ β”œβ”€β”€ ClosingPage β”‚ └── ErrorPage β”œβ”€β”€ types/ β†’ 도메인 νƒ€μž… μ •μ˜ β”‚ β”œβ”€β”€ comment.ts (Comment, Mvp, CommentFormState, CommentSubmissionPayload, CommentsKey, PeerCommentKind, PeerCommentRowState, CommentSubmitResult) @@ -92,6 +164,7 @@ src/ - `src/components/` λ‚΄λΆ€ κ°„ μ°Έμ‘°λŠ” μƒλŒ€ 경둜 μœ μ§€ (μˆœν™˜μ°Έμ‘° λ°©μ§€) - 같은 폴더 λ‚΄ μ°Έμ‘°λŠ” `./`, μƒμœ„Β·ν˜•μ œ 폴더 μ°Έμ‘°λŠ” `../` μ‚¬μš© + ## πŸ“Œ ꡬ쑰 원칙 - μ΅œμƒμœ„ κΈ°μ€€: `common/` (λ²”μš©) vs 도메인 폴더 (νŠΉν™”) @@ -102,12 +175,59 @@ src/ - `context/`λŠ” Provider μ»΄ν¬λ„ŒνŠΈμ™€ Context 객체만 export β€” 훅은 λ°˜λ“œμ‹œ `hooks/`에 뢄리, νƒ€μž…μ€ λ°˜λ“œμ‹œ `types/`에 뢄리 - νƒ€μž…μ€ 항상 `@types`μ—μ„œ import β€” `@context` λ“± λ‹€λ₯Έ κ²½λ‘œμ—μ„œ νƒ€μž…μ„ re-exportν•˜μ§€ μ•ŠμŒ -## πŸ” Supabase +## μŠ€νƒ€μΌλ§ κ·œμΉ™ + +- μŠ€νƒ€μΌμ€ λ°˜λ“œμ‹œ **vanilla-extract** (`*.css.ts`) 파일둜 뢄리 +- 인라인 μŠ€νƒ€μΌ λ˜λŠ” CSS λͺ¨λ“ˆ(`.module.css`) 혼용 κΈˆμ§€ +- μƒˆ μ»΄ν¬λ„ŒνŠΈ/νŽ˜μ΄μ§€ μΆ”κ°€ μ‹œ 같은 κ²½λ‘œμ— `*.css.ts` 파일 ν•¨κ»˜ 생성 + +--- + + +## API κ·œμΉ™ + +- λͺ¨λ“  Supabase ν˜ΈμΆœμ€ `callApi()` 래퍼(`src/lib/apiClient.ts`)둜 감싸야 함 +- DB 직접 쿼리 λŒ€μ‹  RPC(`supabase.rpc(...)`) μ‚¬μš© 원칙 +- API ν•¨μˆ˜λŠ” `src/lib/api/` ν•˜μœ„ 도메인별 νŒŒμΌμ— μœ„μΉ˜ + +--- + +## μƒνƒœ 관리 κ·œμΉ™ + +- μ„€λ¬Έ 제좜 λ°μ΄ν„°λŠ” `SubmissionContext` (`src/context/SubmissionContext.tsx`)λ₯Ό 톡해 관리 +- νŽ˜μ΄μ§€ κ°„ 데이터 전달은 `location.state` λ˜λŠ” `SubmissionContext` μ‚¬μš© +- ZustandλŠ” SubmissionContext둜 μ²˜λ¦¬ν•˜κΈ° μ–΄λ €μš΄ μ „μ—­ μƒνƒœμ—λ§Œ λ„μž… + +--- + +## AI Agent μž‘μ—… μ§€μΉ¨ + +### μ½”λ“œ μˆ˜μ • μ „ + +- μˆ˜μ • μ „ λ°˜λ“œμ‹œ κ΄€λ ¨ νŒŒμΌμ„ λ¨Όμ € 읽고 κΈ°μ‘΄ νŒ¨ν„΄Β·κ΅¬μ‘°λ₯Ό νŒŒμ•…ν•œ λ’€ μž‘μ—… +- κΈ°μ‘΄ μ½”λ“œ μŠ€νƒ€μΌκ³Ό 일관성을 μœ μ§€ (넀이밍, 파일 ꡬ성 방식 λ“±) + +### μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€ μ‹œ + +1. `@sopt-makers/ui`에 μ ν•©ν•œ μ»΄ν¬λ„ŒνŠΈκ°€ μžˆλŠ”μ§€ λ¨Όμ € 확인 +2. μ—†μœΌλ©΄ `src/components/`에 `.tsx` + `.css.ts` 쌍으둜 생성 +3. `src/components/index.ts`에 export μΆ”κ°€ + +### νŽ˜μ΄μ§€ μΆ”κ°€ μ‹œ + +1. `src/pages/` μ•„λž˜ `XxxPage.tsx` + `XxxPage.css.ts` 생성 +2. `src/App.tsx` λΌμš°ν„°μ— 경둜 등둝 +3. ν•„μš”ν•œ 경우 `SubmissionContext` νƒ€μž… ν™•μž₯ + +### μ—λŸ¬ 처리 -- service_role key μ ˆλŒ€ μ‚¬μš© κΈˆμ§€ -- 민감 데이터 직접 쑰회 κΈˆμ§€ -- 검증은 RPC λ˜λŠ” μ„œλ²„ 둜직 μ‚¬μš© +- λ„€νŠΈμ›Œν¬ 였λ₯˜ β†’ `NetworkError` (toast ν‘œμ‹œ) +- μ„œλΉ„μŠ€ 였λ₯˜ β†’ `ServiceError` β†’ `/error` νŽ˜μ΄μ§€λ‘œ 이동 +- `useErrorHandler` ν›… ν™œμš© -## πŸ“Œ General +### ν•˜μ§€ 말아야 ν•  것 -- κΈ°μ‘΄ μ½”λ“œ μŠ€νƒ€μΌ/ꡬ쑰λ₯Ό λ°˜λ“œμ‹œ λ”°λ₯Ό 것 +- μš”μ²­ λ²”μœ„λ₯Ό λ²—μ–΄λ‚œ λ¦¬νŒ©ν† λ§, κΈ°λŠ₯ μΆ”κ°€, μ½”λ“œ 정리 +- λΆˆν•„μš”ν•œ μ£Όμ„Β·νƒ€μž… μ–΄λ…Έν…Œμ΄μ…˜ μΆ”κ°€ +- 투기적 좔상화 (미래 μš”κ΅¬μ‚¬ν•­ λŒ€λΉ„ ꡬ쑰 섀계) +- λ‹¨μˆœ 버그 μˆ˜μ •μΈλ° μ£Όλ³€ μ½”λ“œκΉŒμ§€ κ°œμ„  diff --git a/src/App.tsx b/src/App.tsx index 6abb819..c89bc34 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import UserInfoPage from '@pages/UserInfoPage'; import StopCommentPage from '@pages/StopCommentPage'; import StartCommentPage from '@pages/StartCommentPage'; import ContinueCommentPage from '@pages/ContinueCommentPage'; +import ClosingPage from '@pages/ClosingPage'; const router = createBrowserRouter([ { @@ -27,7 +28,8 @@ const router = createBrowserRouter([ { path: '/user-info', element: }, { path: '/stop-comment', element: }, { path: '/start-comment', element: }, - { path: '/continue-comment', element: } + { path: '/continue-comment', element: }, + { path: '/closing', element: }, ]); function App() { diff --git a/src/assets/ending.svg b/src/assets/ending.svg new file mode 100644 index 0000000..de25ecb --- /dev/null +++ b/src/assets/ending.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/stop_comment_example.png b/src/assets/stop_comment_example.png new file mode 100644 index 0000000..cc4d324 Binary files /dev/null and b/src/assets/stop_comment_example.png differ diff --git a/src/assets/stop_comment_explanation.png b/src/assets/stop_comment_explanation.png new file mode 100644 index 0000000..b120130 Binary files /dev/null and b/src/assets/stop_comment_explanation.png differ diff --git a/src/components/common/layout/StepLayout.tsx b/src/components/common/layout/StepLayout.tsx index 4ea79a6..f4e09c2 100644 --- a/src/components/common/layout/StepLayout.tsx +++ b/src/components/common/layout/StepLayout.tsx @@ -17,6 +17,8 @@ interface StepLayoutProps { showProgressBar?: boolean; currentStep?: number; totalSteps?: number; + /** 헀더 λ°°λ„ˆμ— ν‘œμ‹œν•  이미지 경둜. λ―Έμ§€μ • μ‹œ κΈ°λ³Έ header_title.svg μ‚¬μš© */ + bannerImage?: string; } function StepLayout({ @@ -28,6 +30,7 @@ function StepLayout({ showProgressBar = false, currentStep, totalSteps, + bannerImage, }: StepLayoutProps) { const contentSectionClassName = showProgressBar ? `${styles.contentSection} ${styles.contentSectionWithProgress}` @@ -36,7 +39,7 @@ function StepLayout({ return (
- +
{showProgressBar && currentStep !== undefined && totalSteps !== undefined && ( diff --git a/src/pages/ClosingPage.css.ts b/src/pages/ClosingPage.css.ts new file mode 100644 index 0000000..f51dfe6 --- /dev/null +++ b/src/pages/ClosingPage.css.ts @@ -0,0 +1,25 @@ +import { style } from '@vanilla-extract/css'; +import { colors } from '@sopt-makers/colors'; +import { fontsObject } from '@sopt-makers/fonts'; + +export const imageArea = style({ + marginTop: 56, + width: '100%', + height: 276, + objectFit: 'contain', +}); + +export const textArea = style({ + width: '100%', + height: 100, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +export const textContent = style({ + padding: '20px 28px', + color: colors.white, + textAlign: 'center', + ...fontsObject.HEADING_5_20_B, +}); diff --git a/src/pages/ClosingPage.tsx b/src/pages/ClosingPage.tsx new file mode 100644 index 0000000..580c5f7 --- /dev/null +++ b/src/pages/ClosingPage.tsx @@ -0,0 +1,38 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { StepLayout } from '../components'; +import headerImg from '../assets/header_img.png'; +import endingImg from '../assets/ending.svg'; +import * as styles from './ClosingPage.css'; + +function ClosingPage() { + const navigate = useNavigate(); + + const handleClose = useCallback(() => { + navigate('/'); + }, [navigate]); + + return ( + + μ—”λ”© 이미지 + +
+

+ 이번 μŠ€ν”„λ¦°νŠΈλ„ κ³ μƒν•˜μ…¨μ–΄μš” +
+ λ§ˆμ§€λ§‰κΉŒμ§€ ν™”μ΄νŒ…! +

+
+
+ ); +} + +export default ClosingPage;