Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ StartPage (활성 스프린트 여부 분기)
└─ NoticePage (안내)
└─ SprintCodePage (인증 코드 입력)
└─ SprintIntroPage (스프린트 소개)
└─ UserInfoPage (사용자 정보 입력)
└─ (회고 설문 스텝들 — 추후 구현 예정)
└─ UserInfoPage (사용자 정보 입력, step 1/7)
└─ StopCommentPage (step 2/7)
└─ StartCommentPage (step 3/7)
└─ ContinueCommentPage (step 4/7)
└─ MvpPage (step 6/7)
└─ ClosingPage (step 7/7)
```
Comment on lines +15 to 21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

스텝 번호가 5/7을 건너뜁니다.

ContinueCommentPage가 step 4/7인데 바로 MvpPage를 step 6/7로 표기하여 step 5/7이 누락되어 있습니다. 전체 7단계가 맞다면 MvpPage를 5/7로, ClosingPage를 6/7로 조정하거나, 중간에 빠진 페이지를 보완해야 플로우 문서와 실제 구현(MvpPage.tsxcurrentStep={6} totalSteps={7})이 일치합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 15 - 21, The step numbering in the flow and
implementation is inconsistent: Update the step values so they match the
documented 7-step flow — either set MvpPage's props in MvpPage.tsx from
currentStep={6} to currentStep={5} and ClosingPage from step 7 to step 6, or
insert the missing intermediate page between ContinueCommentPage and MvpPage;
specifically adjust the MvpPage and ClosingPage step labels/props (and any
related UI indicators) to reflect the corrected sequence (ContinueCommentPage ->
MvpPage (5/7) -> ClosingPage (6/7) -> final step if applicable) so flow text and
the component prop currentStep/totalSteps remain consistent.


---
Expand All @@ -23,29 +27,18 @@ StartPage (활성 스프린트 여부 분기)
```
src/
├── assets/ # 정적 이미지 리소스
├── components/ # 공통 재사용 컴포넌트
│ ├── CodeInput # 인증 코드 입력 필드
│ ├── ContentHeading # 페이지 제목 영역
│ ├── ImageSection # 이미지 섹션
│ ├── PageLayout # 전체 페이지 래퍼
│ ├── ProgressBar # 스텝 진행 바
│ ├── SelectField # 셀렉트 폼 필드
│ └── StepLayout # 스텝 기반 레이아웃 (하단 버튼 포함)
├── components/ # 공통 재사용 컴포넌트 (상세 구조는 📦 패키지 구조 참고)
├── context/
│ └── SubmissionContext.tsx # 설문 제출 데이터 전역 상태
├── hooks/
│ └── useErrorHandler.ts # 에러 핸들링 커스텀 훅
│ └── CommentFormContext.tsx # 설문 폼 전역 상태 (CommentFormProvider)
├── hooks/ # 커스텀 훅 (useCommentForm, usePeerMembers, useErrorHandler)
├── lib/
│ ├── api/
│ │ ├── chapter.ts # 챕터 API
│ │ ├── comment.ts # 댓글(회고) API
│ │ ├── sprint.ts # 스프린트 API
│ │ └── user.ts # 유저 API
│ ├── apiClient.ts # Supabase 호출 래퍼 (에러 분류)
│ ├── errors.ts # 커스텀 에러 클래스
│ └── supabase.ts # Supabase 클라이언트 초기화
│ ├── api/ # 도메인별 API (comment, sprint, sprintPeers, chapter, user)
│ ├── apiClient.ts # Supabase 호출 래퍼 (에러 분류)
│ ├── errors.ts # 커스텀 에러 클래스
│ └── supabase.ts # Supabase 클라이언트 초기화
├── pages/ # 라우트 단위 페이지 컴포넌트
├── types/ # TypeScript 타입 정의
├── utils/ # 순수 비즈니스 로직 유틸
├── App.tsx # 라우터 설정 진입점
└── main.tsx # 앱 마운트
```
Expand All @@ -59,7 +52,7 @@ src/
| 프레임워크 | React 18 + TypeScript |
| 빌드 | Vite |
| 라우팅 | React Router v7 |
| 상태 관리 | React Context (`SubmissionContext`) + Zustand (필요 시) |
| 상태 관리 | React Context (`CommentFormContext`) + Zustand (필요 시) |
| 백엔드/DB | Supabase (RPC 기반 쿼리) |
| 스타일링 | vanilla-extract (`*.css.ts`) |
| 디자인 시스템 | `@sopt-makers/ui`, `@sopt-makers/colors`, `@sopt-makers/fonts`, `@sopt-makers/icons` |
Expand Down Expand Up @@ -111,6 +104,7 @@ src/
│ ├── StopCommentPage
│ ├── StartCommentPage
│ ├── ContinueCommentPage
│ ├── MvpPage
│ ├── ClosingPage
│ └── ErrorPage
├── types/ → 도메인 타입 정의
Expand Down Expand Up @@ -194,9 +188,9 @@ src/

## 상태 관리 규칙

- 설문 제출 데이터는 `SubmissionContext` (`src/context/SubmissionContext.tsx`)를 통해 관리
- 페이지 간 데이터 전달은 `location.state` 또는 `SubmissionContext` 사용
- Zustand는 SubmissionContext로 처리하기 어려운 전역 상태에만 도입
- 설문 데이터는 `CommentFormContext` (`src/context/CommentFormContext.tsx`)를 통해 관리
- 페이지 간 데이터 전달은 `location.state` 또는 `CommentFormContext` 사용
- Zustand는 CommentFormContext로 처리하기 어려운 전역 상태에만 도입

---

Expand All @@ -217,7 +211,7 @@ src/

1. `src/pages/` 아래 `XxxPage.tsx` + `XxxPage.css.ts` 생성
2. `src/App.tsx` 라우터에 경로 등록
3. 필요한 경우 `SubmissionContext` 타입 확장
3. 필요한 경우 `src/types/comment.ts`의 `CommentFormState` 타입 확장

### 에러 처리

Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 MvpPage from '@pages/MvpPage';
import ClosingPage from '@pages/ClosingPage';

const router = createBrowserRouter([
Expand All @@ -29,6 +30,7 @@ const router = createBrowserRouter([
{ path: '/stop-comment', element: <StopCommentPage /> },
{ path: '/start-comment', element: <StartCommentPage /> },
{ path: '/continue-comment', element: <ContinueCommentPage /> },
{ path: '/mvp', element: <MvpPage /> },
{ path: '/closing', element: <ClosingPage /> },
]);

Expand Down
2 changes: 0 additions & 2 deletions src/pages/ClosingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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';

Expand All @@ -14,7 +13,6 @@ function ClosingPage() {

return (
<StepLayout
bannerImage={headerImg}
showProgressBar
currentStep={7}
totalSteps={7}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ContinueCommentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function ContinueCommentPage() {
<PeerCommentStepTemplate
content={CONTINUE_COMMENT_STEP_CONTENT}
currentStep={4}
nextPath="/next"
nextPath="/mvp"
/>
);
}
Expand Down
117 changes: 117 additions & 0 deletions src/pages/MvpPage.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { style } from '@vanilla-extract/css';
import { colors } from '@sopt-makers/colors';
import { fontsObject } from '@sopt-makers/fonts';

export const body = style({
display: 'flex',
flexDirection: 'column',
gap: 32,
color: colors.white,
});

export const fields = style({
display: 'flex',
flexDirection: 'column',
gap: 28,
});

export const fieldGroup = style({
display: 'flex',
flexDirection: 'column',
gap: 8,
});

export const searchContainer = style({
position: 'relative',
marginTop: 4,
});

export const inputWrapper = style({
display: 'flex',
alignItems: 'center',
backgroundColor: colors.gray800,
borderRadius: 6,
padding: '12px 14px',
gap: 8,
});

export const searchInput = style({
flex: 1,
background: 'transparent',
border: 'none',
outline: 'none',
color: colors.white,
...fontsObject.BODY_3_14_M,
selectors: {
'&::placeholder': {
color: colors.gray400,
},
},
});

export const clearButton = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
border: 'none',
background: 'transparent',
cursor: 'pointer',
flexShrink: 0,
});


export const dropdown = style({
position: 'absolute',
top: '100%',
left: 0,
right: 0,
backgroundColor: colors.gray800,
borderRadius: 6,
marginTop: 4,
zIndex: 10,
overflow: 'hidden',
listStyle: 'none',
padding: 0,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

하드코딩된 색상 사용 지양.

boxShadowrgba(0, 0, 0, 0.4)@sopt-makers/colors를 사용하지 않은 하드코딩 값입니다. 디자인 시스템 토큰을 사용하거나, 불가피할 경우 주석으로 사유를 남겨주세요.

As per coding guidelines: "Use @sopt-makers/colors for all color values instead of hardcoding color values".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/MvpPage.css.ts` at line 76, The boxShadow currently hardcodes
rgba(0, 0, 0, 0.4); update the MvpPage styles to use the design token from
`@sopt-makers/colors` instead (import the appropriate black/alpha token and
compose the boxShadow using that token) and replace the literal in the boxShadow
property; if a token cannot represent the exact alpha value, add a short inline
comment above the boxShadow explaining why a hardcoded rgba is required and
reference the missing token so reviewers can follow up.

});

export const dropdownItem = style({
display: 'flex',
alignItems: 'center',
gap: 8,
width: '100%',
padding: '10px 14px',
border: 'none',
background: 'transparent',
cursor: 'pointer',
color: colors.white,
...fontsObject.BODY_3_14_M,
textAlign: 'left',
selectors: {
'&:hover': {
backgroundColor: colors.gray700,
},
},
});

export const avatarIcon = style({
width: 28,
height: 28,
borderRadius: '50%',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
backgroundColor: colors.gray700,
color: colors.gray500,
});

export const chipList = style({
listStyle: 'none',
margin: '4px 0 0',
padding: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
});
132 changes: 132 additions & 0 deletions src/pages/MvpPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useState } from 'react';
import type { ChangeEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { FieldBox } from '@sopt-makers/ui';
import { colors } from '@sopt-makers/colors';
import { IconUser, IconXCircle } from '@sopt-makers/icons';
import { StepLayout, ContentHeading, MemberChip, InputField } from '@components';
import { usePeerMembers, useCommentForm } from '@hooks';
import type { PeerMember } from '@types';
import * as styles from './MvpPage.css';

function MvpPage() {
const navigate = useNavigate();
const { update } = useCommentForm();
const peerMembers = usePeerMembers();

const [searchQuery, setSearchQuery] = useState('');
const [selectedMember, setSelectedMember] = useState<PeerMember | null>(null);
const [reason, setReason] = useState('');

const filteredMembers = searchQuery
? peerMembers.filter((m) => m.name.includes(searchQuery))
: [];

const isAllFilled = selectedMember !== null && reason.trim().length > 0;

const handleSelectMember = (member: PeerMember) => {
setSelectedMember(member);
setSearchQuery('');
};

const handleSubmit = () => {
if (!selectedMember) return;
update({ mvp: { target_user_id: selectedMember.userId, comment_text: reason } });
navigate('/closing');
};

return (
<StepLayout
showProgressBar
currentStep={6}
totalSteps={7}
nextLabel="제출하기"
showNextRightIcon={false}
onNext={handleSubmit}
isNextDisabled={!isAllFilled}
>
<div className={styles.body}>
<ContentHeading
title="MVP 선정"
description={
<>
마지막이에요!
<br />
이번 스프린트에서 뛰어난 모습을 보여주었던 동료가 있다면
<br />
이름과 이유를 작성해주세요.
</>
}
/>

<div className={styles.fields}>
<div className={styles.fieldGroup}>
<FieldBox.Label
label="MVP로 선정하고 싶은 동료"
description="MVP는 한 명만 선택할 수 있어요."
required
/>

<div className={styles.searchContainer}>
<div className={styles.inputWrapper}>
<input
className={styles.searchInput}
placeholder="멤버 검색"
value={searchQuery}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchQuery(e.target.value)
}
/>
{searchQuery && (
<button
type="button"
className={styles.clearButton}
onClick={() => setSearchQuery('')}
aria-label="검색어 지우기"
>
<IconXCircle style={{ width: 20, height: 20, color: colors.gray50 }} />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</button>
)}
</div>

{searchQuery && filteredMembers.length > 0 && (
<ul className={styles.dropdown}>
{filteredMembers.map((member) => (
<li key={member.userId}>
<button
type="button"
className={styles.dropdownItem}
onClick={() => handleSelectMember(member)}
>
<span className={styles.avatarIcon} aria-hidden>
<IconUser style={{ width: 20, height: 20 }} />
</span>
<span>{member.name}</span>
</button>
</li>
))}
</ul>
)}
</div>

{selectedMember && (
<ul className={styles.chipList}>
<MemberChip label={selectedMember.name} showRemoveButton={false} />
</ul>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>

<InputField
labelText="MVP 선정 이유를 작성해주세요."
required
placeholder="선정하는 이유"
value={reason}
onChange={setReason}
/>
</div>
</div>
</StepLayout>
);
}

export default MvpPage;