Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 19 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ StartPage (활성 스프린트 여부 분기)
└─ NoticePage (안내)
└─ SprintCodePage (인증 코드 입력)
└─ SprintIntroPage (스프린트 소개)
└─ UserInfoPage (사용자 정보 입력, step 1/7)
└─ StopCommentPage (step 2/7)
└─ StartCommentPage (step 3/7)
└─ ContinueCommentPage (step 4/7)
└─ MvpPage (step 6/7)
└─ ClosingPage (step 7/7)
└─ UserInfoPage (사용자 정보 입력, step 1/6)
└─ StopCommentPage (step 2/6)
└─ StartCommentPage (step 3/6)
└─ ContinueCommentPage (step 4/6)
└─ MvpPage (step 5/6)
└─ ClosingPage (step 6/6)
```

---
Expand Down Expand Up @@ -85,7 +85,7 @@ src/
│ ├── common/ → 도메인 무관한 범용 컴포넌트
│ │ ├── layout/ (PageLayout, StepLayout)
│ │ ├── form/ (SelectField, InputField)
│ │ └── ui/ (ContentHeading, ImageSection, ProgressBar, MemberChip)
│ │ └── ui/ (ContentHeading, ImageSection, ProgressBar, MemberChip, FieldSection)
│ ├── sprint-code/ → 스프린트 코드 도메인
│ │ └── SprintCodeInput
│ ├── peer-comment/ → 피어 코멘트 도메인
Expand Down Expand Up @@ -139,7 +139,7 @@ src/
| `Picker` | 목록에서 항목을 선택하는 UI | `PeerMemberPicker` |
| `Chip` | 선택된 항목을 표시하는 태그 UI | `MemberChip` |
| `Repeater` | 동일한 입력 블록을 반복·관리 | `PeerCommentRepeater` |
| `Block` / `Section` | 여러 요소를 묶은 영역 단위 | `PeerCommentRecipientBlock` |
| `Block` / `Section` | 여러 요소를 묶은 영역 단위 | `PeerCommentRecipientBlock`, `FieldSection` |

## 🗂️ Import Aliases

Expand Down Expand Up @@ -175,6 +175,13 @@ src/
- 인라인 스타일 또는 CSS 모듈(`.module.css`) 혼용 금지
- 새 컴포넌트/페이지 추가 시 같은 경로에 `*.css.ts` 파일 함께 생성

### 레이아웃 기준선

- `StepLayout` 콘텐츠 영역 좌우 padding: `0 28px` (모든 페이지 공통 기준)
- `FieldSection`: 논리 블록 하나를 감싸는 padding wrapper (`paddingTop: 24, paddingBottom: 12`)
- **하나의 FieldSection = 하나의 논리 블록** (ContentHeading, 개별 폼 필드 등)
- `gap` 없음 — 여러 요소의 간격은 각 FieldSection이 담당

---


Expand Down Expand Up @@ -219,6 +226,10 @@ src/
- 서비스 오류 → `ServiceError` → `/error` 페이지로 이동
- `useErrorHandler` 훅 활용

### push 전 체크리스트

- CLAUDE.md가 현재 코드 상태를 반영하고 있는지 확인 후 업데이트

### 하지 말아야 할 것

- 요청 범위를 벗어난 리팩토링, 기능 추가, 코드 정리
Expand Down
3 changes: 2 additions & 1 deletion src/components/common/layout/StepLayout.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export const contentSection = style({
overflowY: 'auto',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
padding: '0 20px',
padding: '0 28px',
textAlign: 'left',
});

export const contentSectionWithProgress = style({
Expand Down
8 changes: 8 additions & 0 deletions src/components/common/ui/FieldSection.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';

export const container = style({
display: 'flex',
flexDirection: 'column',
paddingTop: 24,
paddingBottom: 12,
});
Comment on lines +3 to +8
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PR 설명의 gap: 28px이 누락되어 있습니다.

PR 본문에는 FieldSectionpaddingTop: 24, paddingBottom: 12, gap: 28을 가진다고 기술되어 있으나 실제 스타일에는 gap이 정의되어 있지 않습니다. 의도적으로 제거한 것이 아니라면 추가가 필요하고, 의도적이라면 PR 설명을 수정해 주세요. UserInfoPage처럼 ContentHeading<p>를 같은 섹션에 나란히 두는 케이스에서는 자식 간 간격이 없어 시각적으로 붙어 보일 수 있습니다.

♻️ 제안
 export const container = style({
   display: 'flex',
   flexDirection: 'column',
+  gap: 28,
   paddingTop: 24,
   paddingBottom: 12,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const container = style({
display: 'flex',
flexDirection: 'column',
paddingTop: 24,
paddingBottom: 12,
});
export const container = style({
display: 'flex',
flexDirection: 'column',
gap: 28,
paddingTop: 24,
paddingBottom: 12,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/ui/FieldSection.css.ts` around lines 3 - 8, The CSS for
FieldSection is missing the gap property described in the PR; update the
exported container style (container in
src/components/common/ui/FieldSection.css.ts) to include gap: 28 (e.g., gap: 28)
so children like ContentHeading and <p> have the intended spacing, or if removal
was intentional, update the PR description to reflect the change instead of
adding the gap.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

FieldSection 간격이 0이 되면서 gap이 불필요해진게 맞아

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@yungu0010, 의도적인 변경이었군요! 설명 감사합니다. 다만 PR 설명에 아직 gap: 28이 언급되어 있을 수 있으니, 혼란을 줄이기 위해 PR 본문도 간략히 수정해 두시면 좋을 것 같습니다.


✏️ Learnings added
Learnt from: yungu0010
Repo: sopt-makers/hear-your-voice PR: 27
File: src/components/common/ui/FieldSection.css.ts:3-8
Timestamp: 2026-04-22T12:09:05.575Z
Learning: In `src/components/common/ui/FieldSection.css.ts`, the `gap` property is intentionally omitted from the `container` style. The spacing between child elements became 0, making `gap` unnecessary. Do not flag its absence as a missing style.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: sopt-makers/hear-your-voice PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T08:56:59.118Z
Learning: Applies to **/*.css.ts : Use vanilla-extract (`*.css.ts`) for styling — do not use inline styles or CSS modules (`.module.css`)

Learnt from: CR
Repo: sopt-makers/hear-your-voice PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T08:56:59.118Z
Learning: Applies to src/components/**/*Field.tsx : Form field components with labels and descriptions must have postfix `Field` (e.g., `SelectField`, `InputField`)

12 changes: 12 additions & 0 deletions src/components/common/ui/FieldSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';
import * as styles from './FieldSection.css';

interface FieldSectionProps {
children: ReactNode;
}

function FieldSection({ children }: FieldSectionProps) {
return <div className={styles.container}>{children}</div>;
}

export default FieldSection;
2 changes: 1 addition & 1 deletion src/components/common/ui/ProgressBar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { colors } from '@sopt-makers/colors';

export const container = style({
width: '100%',
padding: '10px 20px 36px',
padding: '16px 20px',
});

export const track = style({
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as StepLayout } from './common/layout/StepLayout';
export { default as ProgressBar } from './common/ui/ProgressBar';
export { default as MemberChip } from './common/ui/MemberChip';
export { default as ContentHeading } from './common/ui/ContentHeading';
export { default as FieldSection } from './common/ui/FieldSection';
export { default as ImageSection } from './common/ui/ImageSection';
export { default as SelectField } from './common/form/SelectField';
export { default as InputField } from './common/form/InputField';
Expand Down
18 changes: 2 additions & 16 deletions src/components/peer-comment/PeerCommentStepTemplate.css.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
import { colors } from '@sopt-makers/colors';
import { fontsObject } from '@sopt-makers/fonts';

/** 제목 블록 ↔ 이미지 ↔ 코멘트 반복 영역 사이 16px */
export const stepContent = style({
display: 'flex',
flexDirection: 'column',
gap: 16,
width: '100%',
});

/**
* ContentHeading + 페이지별 안내 문단( children )
* 제목/설명과 그 아래 문단 사이 간격 없음
*/
export const headingBlock = style({
display: 'flex',
flexDirection: 'column',
gap: 0,
width: '100%',
});

globalStyle(`${headingBlock} p`, {
export const noticeText = style({
margin: 0,
marginTop: 0,
textAlign: 'left',
color: colors.white,
...fontsObject.BODY_3_14_M,
});
7 changes: 4 additions & 3 deletions src/components/peer-comment/PeerCommentStepTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ContentHeading from '../common/ui/ContentHeading';
import FieldSection from '../common/ui/FieldSection';
import ImageSection from '../common/ui/ImageSection';
import StepLayout from '../common/layout/StepLayout';
import PeerCommentRepeater from './PeerCommentRepeater';
Expand Down Expand Up @@ -59,10 +60,10 @@ function PeerCommentStepTemplate({
totalSteps={totalSteps}
>
<div className={styles.stepContent}>
<div className={styles.headingBlock}>
<FieldSection>
<ContentHeading title={title} description={description} />
<p>해당 코멘트는 무기명으로 전달되어요.</p>
</div>
<p className={styles.noticeText}>해당 코멘트는 무기명으로 전달되어요.</p>
</FieldSection>
Comment on lines +63 to +66
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: FieldSection 스타일과 사용처를 확인해 내부 gap이 공통 컴포넌트에서 제공되는지 검증합니다.
# Expect: FieldSection.css.ts에 gap: 28이 있거나, 각 사용처에서 별도 간격을 명시해야 합니다.

fd -i 'FieldSection\.(tsx|css\.ts)$' -x sed -n '1,120p' {}
rg -n -C2 '<FieldSection>|gap\s*:' --type ts --type tsx

Repository: sopt-makers/hear-your-voice

Length of output: 577


🏁 Script executed:

# noticeText 스타일 확인
rg -n "noticeText" src/components/peer-comment/ -A5 -B2

# PeerCommentStepTemplate.tsx의 전체 스타일 파일 확인
fd "PeerCommentStepTemplate" --type f -e css.ts -e tsx | head -5 | xargs cat

# FieldSection 다른 사용처 확인
rg "<FieldSection>" --type tsx -B2 -A2 | head -50

Repository: sopt-makers/hear-your-voice

Length of output: 4568


🏁 Script executed:

# ContentHeading 스타일 확인
rg "ContentHeading" src/components/common/ui/ -l | xargs cat

# FieldSection 다른 사용처 찾기
rg "<FieldSection>" --type ts -B5 -A5

Repository: sopt-makers/hear-your-voice

Length of output: 6133


🏁 Script executed:

# ContentHeading.css.ts 전체 파일 확인
cat src/components/common/ui/ContentHeading.css.ts

Repository: sopt-makers/hear-your-voice

Length of output: 592


FieldSection 내부 자식 요소들 사이에 간격이 없어서 ContentHeading과 안내 문구가 붙을 수 있습니다.

FieldSection.css.ts에는 gap 스타일이 없고, noticeText도 상단 마진이 없어서 이 구조에서는 제목과 안내 텍스트가 직접 붙게 됩니다. FieldSectiongap: 28을 추가하여 공유 컴포넌트 차원에서 일관된 간격을 제공하는 것이 좋습니다.

수정 예시
 export const container = style({
   display: 'flex',
   flexDirection: 'column',
   paddingTop: 24,
   paddingBottom: 12,
+  gap: 28,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/peer-comment/PeerCommentStepTemplate.tsx` around lines 63 -
66, FieldSection currently lacks spacing between its children causing
ContentHeading and the paragraph with class noticeText to run together; update
the FieldSection component styles (FieldSection.css.ts) to include a gap: 28px
(or gap: 28) so all children (including ContentHeading and the p.noticeText)
receive consistent vertical spacing across usages, and remove any per-instance
margin hacks from PeerCommentStepTemplate; target the FieldSection style
definition rather than changing individual consumers.

{guideImages ? (
<ImageSection>
<ImageSection.Image src={guideImages[0]} alt="comment 작성 설명 이미지" />
Expand Down
1 change: 0 additions & 1 deletion src/components/sprint-code/SprintCodeInput.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const root = style({
display: 'flex',
flexDirection: 'column',
gap: 12,
padding: '24px 0 12px',
maxWidth: 320,
textAlign: 'left',
});
Expand Down
14 changes: 3 additions & 11 deletions src/components/sprint-code/SprintCodeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ function CodeInput({
onChange(sanitized);
};

const activeBoxIndex = isInputFocused
? Math.min(value.length, SPRINT_CODE_LENGTH - 1)
: -1;
const activeBoxIndex = isInputFocused ? Math.min(value.length, SPRINT_CODE_LENGTH - 1) : -1;

return (
<div className={styles.root}>
Expand Down Expand Up @@ -75,21 +73,15 @@ function CodeInput({
data-filled={isFilled}
data-error={showError}
>
<span className={styles.codeDigit}>
{digit ?? ''}
</span>
<span className={styles.codeDigit}>{digit ?? ''}</span>
</div>
);
})}
</div>
</div>

{showError && (
<p
id={errorId}
className={styles.errorText}
role="alert"
>
<p id={errorId} className={styles.errorText} role="alert">
{errorMessage}
</p>
)}
Expand Down
13 changes: 0 additions & 13 deletions src/pages/MvpPage.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@ 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',
Expand Down
120 changes: 62 additions & 58 deletions src/pages/MvpPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ChangeEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { FieldBox } from '@sopt-makers/ui';
import { IconUser, IconXCircle } from '@sopt-makers/icons';
import { StepLayout, ContentHeading, MemberChip, InputField } from '@components';
import { StepLayout, ContentHeading, FieldSection, MemberChip, InputField } from '@components';
import { usePeerMembers, useCommentForm } from '@hooks';
import type { PeerMember } from '@types';
import * as styles from './MvpPage.css';
Expand Down Expand Up @@ -44,7 +44,7 @@ function MvpPage() {
onNext={handleSubmit}
isNextDisabled={!isAllFilled}
>
<div className={styles.body}>
<FieldSection>
<ContentHeading
title="MVP 선정"
description={
Expand All @@ -57,71 +57,75 @@ function MvpPage() {
</>
}
/>
</FieldSection>

<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 className={styles.clearIcon} />
</button>
)}
</div>
<FieldSection>
<div className={styles.fieldGroup}>
<FieldBox.Label
label="MVP로 선정하고 싶은 동료"
description="MVP는 한 명만 선택할 수 있어요."
required
/>

{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 className={styles.avatarIconSvg} />
</span>
<span>{member.name}</span>
</button>
</li>
))}
</ul>
<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 className={styles.clearIcon} />
</button>
)}
</div>

{selectedMember && (
<div className={styles.chipWrapper}>
<MemberChip label={selectedMember.name} showRemoveButton={false} />
</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 className={styles.avatarIconSvg} />
</span>
<span>{member.name}</span>
</button>
</li>
))}
</ul>
)}
</div>

<InputField
labelText="MVP 선정 이유를 작성해주세요."
required
placeholder="선정하는 이유"
value={reason}
onChange={setReason}
/>
{selectedMember && (
<div className={styles.chipWrapper}>
<MemberChip label={selectedMember.name} showRemoveButton={false} />
</div>
)}
</div>
</div>
</FieldSection>

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