-
Notifications
You must be signed in to change notification settings - Fork 1
[FEATURE] 온보딩 페이지 구현 #3 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
개요새로운 다단계 온보딩 흐름을 구현하기 위해 네비게이션 컴포넌트와 다섯 개의 입력 폼 컴포넌트를 추가하고, 기존 온보딩 페이지에 상태 관리 및 단계별 렌더링 로직을 도입했습니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant User
participant Onboarding as Onboarding<br/>Component
participant FormInputs as Form Input<br/>Components
participant Navigation as Navigation<br/>Header
User->>Onboarding: 온보딩 페이지 접근
Onboarding->>Navigation: Navigation 렌더링
Onboarding->>FormInputs: 현재 단계별 입력 컴포넌트 렌더링
Note over Onboarding: 진행 표시기 및 진행 막대 표시
User->>FormInputs: 입력 데이터 입력
FormInputs-->>Onboarding: 입력값 업데이트
User->>Onboarding: "다음" 버튼 클릭
Onboarding->>Onboarding: 단계 증가
Onboarding->>FormInputs: 다음 단계 입력 컴포넌트 렌더링
User->>Onboarding: "완료" 버튼 클릭
Onboarding->>Onboarding: 완료 처리
Onboarding-->>User: /chat로 리디렉션
코드 리뷰 예상 소요 시간🎯 3 (Moderate) | ⏱️ ~20분 추가 검토 대상:
관련 이슈
관련 PR
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (4)
src/components/Navigation.tsx (1)
1-11: 불필요한 Fragment 제거 권장단일 자식 요소만 반환하는 경우 Fragment(
<></>)가 불필요합니다.다음과 같이 단순화할 수 있습니다:
const Navigation = () => { return ( - <> - <div className="flex h-16 w-full items-center px-4 shadow-md"> - <h1 className="text-2xl font-bold text-[#0D2D84]">Snowgent</h1> - </div> - </> + <div className="flex h-16 w-full items-center px-4 shadow-md"> + <h1 className="text-2xl font-bold text-[#0D2D84]">Snowgent</h1> + </div> ); };src/pages/onboarding/Onboarding.tsx (1)
36-38: 빈 placeholder 값 개선 권장TextInput 컴포넌트에 빈 문자열("")을 placeholder로 전달하고 있습니다. 사용자 경험 향상을 위해 구체적인 힌트를 제공하는 것이 좋습니다.
예시:
case 2: - return <TextInput key="step2" title="업체명을" placeholder="" />; + return <TextInput key="step2" title="업체명을" placeholder="예: 스노우카페" />; case 3: - return <TextInput key="step3" title="위치(시군구)를" placeholder="" />; + return <TextInput key="step3" title="위치(시군구)를" placeholder="예: 강남구" />;src/components/onboarding/FormButton.tsx (2)
1-1: 타입 안전성 강화 권장
typeprop에 문자열 리터럴 유니온 타입을 사용하면 컴파일 타임에 오류를 잡을 수 있습니다.다음과 같이 타입을 정의하세요:
-const FormButton = ({ type, onClick }: { type: string; onClick: () => void }) => { +const FormButton = ({ type, onClick }: { type: 'next' | 'prev' | 'submit'; onClick: () => void }) => {
8-10: 불필요한 handleClick 래퍼 함수 제거 권장
handleClick함수는 단순히onClick을 호출하기만 하므로 불필요합니다. 직접onClick을 전달하는 것이 더 간결합니다.다음과 같이 단순화할 수 있습니다:
- const handleClick = () => { - onClick(); - }; - switch (type) { case 'next': return ( - <div className={buttonStyle} onClick={handleClick}> + <button type="button" className={buttonStyle} onClick={onClick}> 다음 - </div> + </button> );
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/components/Navigation.tsx(1 hunks)src/components/onboarding/FormButton.tsx(1 hunks)src/components/onboarding/MultiSelect.tsx(1 hunks)src/components/onboarding/PriceInput.tsx(1 hunks)src/components/onboarding/SingleSelect.tsx(1 hunks)src/components/onboarding/TextInput.tsx(1 hunks)src/pages/HomePage.tsx(1 hunks)src/pages/onboarding/Onboarding.tsx(1 hunks)
🔇 Additional comments (2)
src/pages/HomePage.tsx (1)
9-9: LGTM! 일관된 타이포그래피 개선Navigation 컴포넌트의 스타일링과 일관성을 유지하는 좋은 변경사항입니다.
src/components/onboarding/PriceInput.tsx (1)
1-32: LGTM! 잘 구현된 가격 입력 컴포넌트숫자 전용 입력 검증과 "만원" 단위 표시가 적절히 구현되었습니다. 사용자 경험을 고려한 좋은 구현입니다.
| switch (type) { | ||
| case 'next': | ||
| return ( | ||
| <div className={buttonStyle} onClick={handleClick}> | ||
| 다음 | ||
| </div> | ||
| ); | ||
|
|
||
| case 'prev': | ||
| return ( | ||
| <div className={grayButtonStyle} onClick={handleClick}> | ||
| 이전 | ||
| </div> | ||
| ); | ||
|
|
||
| case 'submit': | ||
| return ( | ||
| <div className={buttonStyle} onClick={handleClick}> | ||
| 완료 | ||
| </div> | ||
| ); | ||
|
|
||
| default: | ||
| break; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
접근성 문제: button 요소 대신 div 사용
클릭 가능한 요소에 <div>를 사용하면 키보드 네비게이션과 스크린 리더 지원이 제대로 작동하지 않습니다. 이는 웹 접근성(WCAG) 기준을 충족하지 못하는 중대한 문제입니다.
다음과 같이 <button> 요소를 사용하도록 수정하세요:
switch (type) {
case 'next':
return (
- <div className={buttonStyle} onClick={handleClick}>
+ <button type="button" className={buttonStyle} onClick={handleClick}>
다음
- </div>
+ </button>
);
case 'prev':
return (
- <div className={grayButtonStyle} onClick={handleClick}>
+ <button type="button" className={grayButtonStyle} onClick={handleClick}>
이전
- </div>
+ </button>
);
case 'submit':
return (
- <div className={buttonStyle} onClick={handleClick}>
+ <button type="button" className={buttonStyle} onClick={handleClick}>
완료
- </div>
+ </button>
);
default:
- break;
+ return null;
}
};📝 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.
| switch (type) { | |
| case 'next': | |
| return ( | |
| <div className={buttonStyle} onClick={handleClick}> | |
| 다음 | |
| </div> | |
| ); | |
| case 'prev': | |
| return ( | |
| <div className={grayButtonStyle} onClick={handleClick}> | |
| 이전 | |
| </div> | |
| ); | |
| case 'submit': | |
| return ( | |
| <div className={buttonStyle} onClick={handleClick}> | |
| 완료 | |
| </div> | |
| ); | |
| default: | |
| break; | |
| } | |
| switch (type) { | |
| case 'next': | |
| return ( | |
| <button type="button" className={buttonStyle} onClick={handleClick}> | |
| 다음 | |
| </button> | |
| ); | |
| case 'prev': | |
| return ( | |
| <button type="button" className={grayButtonStyle} onClick={handleClick}> | |
| 이전 | |
| </button> | |
| ); | |
| case 'submit': | |
| return ( | |
| <button type="button" className={buttonStyle} onClick={handleClick}> | |
| 완료 | |
| </button> | |
| ); | |
| default: | |
| return null; | |
| } |
🤖 Prompt for AI Agents
In src/components/onboarding/FormButton.tsx around lines 12-36, the switch
returns clickable <div> elements which break keyboard and screen-reader
accessibility; replace each rendered <div> with an actual <button> element,
preserve className and onClick handlers, set an appropriate button type
attribute ("button" for 'next' and 'prev', "submit" for 'submit'), and ensure
any necessary aria-label or role is added if the visible text isn't sufficient;
also return null (or nothing) in the default case instead of breaking.
| const MultiSelect = ({ title }: { title: string }) => { | ||
| return ( | ||
| <div className="flex flex-col gap-6"> | ||
| {/* 제목 */} | ||
| <p className="text-[24px] font-semibold">{title}을 선택하세요</p> | ||
| {/* 입력칸 */} | ||
| <input type="text" className="rounded-xl border border-gray-200 px-2 py-4 text-[20px]" /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MultiSelect; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컴포넌트 이름과 구현의 불일치
컴포넌트 이름은 MultiSelect이지만 실제로는 단일 텍스트 입력 필드만 렌더링하고 있습니다. PR 설명에서 타깃층 다중 선택 기능이 향후 계획 단계라고 명시되어 있으나, 이러한 불일치는 향후 유지보수 시 혼란을 야기할 수 있습니다.
다음 중 하나의 방식을 권장합니다:
옵션 1: 임시 구현임을 명확히 표시
-const MultiSelect = ({ title }: { title: string }) => {
+// TODO: 실제 다중 선택 UI 구현 예정
+const MultiSelect = ({ title }: { title: string }) => {
return (
<div className="flex flex-col gap-6">
{/* 제목 */}
<p className="text-[24px] font-semibold">{title}을 선택하세요</p>
- {/* 입력칸 */}
+ {/* 임시 입력칸 - 추후 다중 선택 버튼으로 교체 예정 */}
<input type="text" className="rounded-xl border border-gray-200 px-2 py-4 text-[20px]" />
</div>
);
};옵션 2: 컴포넌트 이름 변경
-const MultiSelect = ({ title }: { title: string }) => {
+const MultiSelectPlaceholder = ({ title }: { title: string }) => {🤖 Prompt for AI Agents
In src/components/onboarding/MultiSelect.tsx lines 1-12, the component is named
MultiSelect but only renders a single text input which creates confusion; either
rename the component to reflect its current behavior (e.g., SingleTextInput or
OnboardingInput) and update the filename, component name and default export
accordingly, or make the temporary intent explicit by keeping the name but
adding a clear TODO comment and a prop/flag (e.g., placeholder prop and a
boolean isTemporaryComponent) and a short inline comment in the component header
indicating this is a placeholder implementation until the real multi-select is
implemented.
| const defaultOptions = [ | ||
| { id: 1, name: 'option1' }, | ||
| { id: 2, name: 'option2' }, | ||
| { id: 3, name: 'option3' }, | ||
| { id: 4, name: 'option4' }, | ||
| { id: 5, name: 'option5' }, | ||
| { id: 6, name: 'option6' }, | ||
| { id: 7, name: 'option7' }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실제 업종 옵션으로 교체 필요
defaultOptions에 'option1', 'option2' 등 일반적인 이름이 사용되고 있습니다. Onboarding.tsx에서 이 컴포넌트가 "업종" 선택에 사용되므로, 실제 업종 데이터로 교체해야 합니다.
다음과 같이 실제 업종 데이터를 사용하도록 수정하세요:
const defaultOptions = [
- { id: 1, name: 'option1' },
- { id: 2, name: 'option2' },
- { id: 3, name: 'option3' },
- { id: 4, name: 'option4' },
- { id: 5, name: 'option5' },
- { id: 6, name: 'option6' },
- { id: 7, name: 'option7' },
+ { id: 1, name: '카페' },
+ { id: 2, name: '음식점' },
+ { id: 3, name: '소매점' },
+ { id: 4, name: '미용실' },
+ { id: 5, name: '편의점' },
+ { id: 6, name: '학원' },
+ { id: 7, name: '기타' },
];📝 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.
| const defaultOptions = [ | |
| { id: 1, name: 'option1' }, | |
| { id: 2, name: 'option2' }, | |
| { id: 3, name: 'option3' }, | |
| { id: 4, name: 'option4' }, | |
| { id: 5, name: 'option5' }, | |
| { id: 6, name: 'option6' }, | |
| { id: 7, name: 'option7' }, | |
| ]; | |
| const defaultOptions = [ | |
| { id: 1, name: '카페' }, | |
| { id: 2, name: '음식점' }, | |
| { id: 3, name: '소매점' }, | |
| { id: 4, name: '미용실' }, | |
| { id: 5, name: '편의점' }, | |
| { id: 6, name: '학원' }, | |
| { id: 7, name: '기타' }, | |
| ]; |
🤖 Prompt for AI Agents
In src/components/onboarding/SingleSelect.tsx around lines 1-9, replace the
placeholder option objects (option1..option7) with actual industry entries used
by the onboarding flow: provide descriptive industry names (e.g., Restaurant,
Retail, Healthcare, Professional Services, Manufacturing, Education, Technology)
as the name values and keep unique ids; ensure the array matches the shape
expected by Onboarding.tsx and any downstream consumers (id:number, name:string)
and update any tests or snapshots that assert the defaultOptions content.
| <select className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200"> | ||
| <option className="" value=""> | ||
| 선택하세요 | ||
| </option> | ||
| {defaultOptions.map((option) => ( | ||
| <option key={option.id} value={option.id}> | ||
| {option.name} | ||
| </option> | ||
| ))} | ||
| </select> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
포커스 상태 시각적 피드백 개선 필요
TextInput 컴포넌트와 마찬가지로, focus:border-gray-200은 기본 테두리 색상과 동일하여 포커스 시 시각적 변화가 없습니다.
다음과 같이 수정하세요:
- <select className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200">
+ <select className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-blue-500">📝 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.
| <select className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200"> | |
| <option className="" value=""> | |
| 선택하세요 | |
| </option> | |
| {defaultOptions.map((option) => ( | |
| <option key={option.id} value={option.id}> | |
| {option.name} | |
| </option> | |
| ))} | |
| </select> | |
| <select className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-blue-500"> | |
| <option className="" value=""> | |
| 선택하세요 | |
| </option> | |
| {defaultOptions.map((option) => ( | |
| <option key={option.id} value={option.id}> | |
| {option.name} | |
| </option> | |
| ))} | |
| </select> |
🤖 Prompt for AI Agents
In src/components/onboarding/SingleSelect.tsx around lines 17 to 26, the select
uses focus:border-gray-200 which matches the default border and provides no
visible focus feedback; update the focus styles to provide clear visual change
(e.g., replace focus:border-gray-200 with a distinct focus class such as
focus:border-primary-500 or add focus:ring-2 focus:ring-primary-500) so the
select shows a visible border or ring on focus consistent with the TextInput
component.
| <input | ||
| type="text" | ||
| placeholder={placeholder} | ||
| className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
포커스 상태 시각적 피드백 개선 필요
focus:border-gray-200 클래스는 기본 border-gray-200과 동일한 색상이므로 포커스 시 시각적 변화가 없습니다. 사용자에게 현재 입력 중인 필드를 명확히 표시해야 합니다.
다음과 같이 수정을 권장합니다:
<input
type="text"
placeholder={placeholder}
- className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200"
+ className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-blue-500"
/>📝 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.
| <input | |
| type="text" | |
| placeholder={placeholder} | |
| className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200" | |
| /> | |
| <input | |
| type="text" | |
| placeholder={placeholder} | |
| className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-blue-500" | |
| /> |
🤖 Prompt for AI Agents
In src/components/onboarding/TextInput.tsx around lines 7 to 11, the input uses
focus:border-gray-200 which is identical to the default border and provides no
visual feedback; replace that class with a more prominent focus style such as
focus:border-primary (or focus:border-blue-500) and add an accessible focus ring
(e.g., focus:ring-2 focus:ring-primary/30 focus:ring-offset-0) and a transition
(e.g., transition-colors) so the focused field is clearly highlighted.
| const renderStepContent = () => { | ||
| switch (currentStep) { | ||
| case 1: | ||
| return <SingleSelect key="step1" title="업종을" />; | ||
| case 2: | ||
| return <TextInput key="step2" title="업체명을" placeholder="" />; | ||
| case 3: | ||
| return <TextInput key="step3" title="위치(시군구)를" placeholder="" />; | ||
| case 4: | ||
| return <PriceInput key="step4" title="평균 월매출" placeholder="숫자" />; | ||
| case 5: | ||
| return <PriceInput key="step5" title="목표 월매출" placeholder="숫자" />; | ||
| default: | ||
| return <MultiSelect key="default" title="원하는 타겟층" />; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default 케이스 처리 개선 필요
switch 문의 default 케이스가 MultiSelect를 반환하고 있으나, 정상적인 플로우에서는 1-5단계만 존재하므로 default는 도달하지 않아야 합니다. 예기치 않은 상태에 대한 처리가 명확하지 않습니다.
다음과 같이 수정을 권장합니다:
const renderStepContent = () => {
switch (currentStep) {
case 1:
return <SingleSelect key="step1" title="업종을" />;
case 2:
return <TextInput key="step2" title="업체명을" placeholder="" />;
case 3:
return <TextInput key="step3" title="위치(시군구)를" placeholder="" />;
case 4:
return <PriceInput key="step4" title="평균 월매출" placeholder="숫자" />;
case 5:
return <PriceInput key="step5" title="목표 월매출" placeholder="숫자" />;
default:
- return <MultiSelect key="default" title="원하는 타겟층" />;
+ return null;
}
};📝 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.
| const renderStepContent = () => { | |
| switch (currentStep) { | |
| case 1: | |
| return <SingleSelect key="step1" title="업종을" />; | |
| case 2: | |
| return <TextInput key="step2" title="업체명을" placeholder="" />; | |
| case 3: | |
| return <TextInput key="step3" title="위치(시군구)를" placeholder="" />; | |
| case 4: | |
| return <PriceInput key="step4" title="평균 월매출" placeholder="숫자" />; | |
| case 5: | |
| return <PriceInput key="step5" title="목표 월매출" placeholder="숫자" />; | |
| default: | |
| return <MultiSelect key="default" title="원하는 타겟층" />; | |
| } | |
| }; | |
| const renderStepContent = () => { | |
| switch (currentStep) { | |
| case 1: | |
| return <SingleSelect key="step1" title="업종을" />; | |
| case 2: | |
| return <TextInput key="step2" title="업체명을" placeholder="" />; | |
| case 3: | |
| return <TextInput key="step3" title="위치(시군구)를" placeholder="" />; | |
| case 4: | |
| return <PriceInput key="step4" title="평균 월매출" placeholder="숫자" />; | |
| case 5: | |
| return <PriceInput key="step5" title="목표 월매출" placeholder="숫자" />; | |
| default: | |
| return null; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/pages/onboarding/Onboarding.tsx around lines 31 to 46, the switch default
currently returns a MultiSelect even though valid steps are only 1–5; replace
the default with an explicit unreachable-state handler such as throwing an Error
(e.g., throw new Error(`Invalid onboarding step: ${currentStep}`)) or logging
the unexpected value and returning null so unexpected states are surfaced during
development rather than silently rendering the wrong component.
온보딩 페이지 UI 구현
업종 (단일선택버튼)
업체명 (string)
위치(시군구) (string)
평균 월매출 (number) (숫자만 입력, 0000만원 에서 만원만 미리 들어가있는)
목표 월매출 (number)
타깃층 (다중선택 버튼) -> 추후 기획 재확정 시 구현
기능은 없으며 단순 UI 만 구현,
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일