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
11 changes: 11 additions & 0 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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>
</>
);
};

export default Navigation;
39 changes: 39 additions & 0 deletions src/components/onboarding/FormButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const FormButton = ({ type, onClick }: { type: string; onClick: () => void }) => {
const buttonStyle =
'bg-[#1d4ed8] w-full text-white py-3 rounded-lg text-center font-medium cursor-pointer';

const grayButtonStyle =
'bg-gray-300 w-full text-gray-700 py-3 rounded-lg text-center font-medium cursor-pointer';

const handleClick = () => {
onClick();
};

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;
}
Comment on lines +12 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

접근성 문제: 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.

Suggested change
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.

};

export default FormButton;
12 changes: 12 additions & 0 deletions src/components/onboarding/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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;
Comment on lines +1 to +12
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

컴포넌트 이름과 구현의 불일치

컴포넌트 이름은 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.

32 changes: 32 additions & 0 deletions src/components/onboarding/PriceInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react';

const PriceInput = ({ title, placeholder }: { title: string; placeholder?: string }) => {
const [value, setValue] = useState('');

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 숫자만 입력 가능하도록
const numericValue = e.target.value.replace(/[^0-9]/g, '');
setValue(numericValue);
};

return (
<div className="flex flex-col gap-6">
{/* 제목 */}
<p className="text-[24px] font-semibold">{title}을 입력해주세요</p>

{/* 입력칸 */}
<div className="relative flex items-center rounded-xl border border-gray-200">
<input
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder || '0'}
className="flex-1 rounded-xl px-4 py-4 text-[20px] outline-none"
/>
<span className="pointer-events-none pr-4 text-[20px] text-gray-500">만원</span>
</div>
</div>
);
};

export default PriceInput;
31 changes: 31 additions & 0 deletions src/components/onboarding/SingleSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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' },
];
Comment on lines +1 to +9
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

실제 업종 옵션으로 교체 필요

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.

Suggested change
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.


const SingleSelect = ({ title }: { title: string }) => {
return (
<div className="flex flex-col gap-6">
{/* 제목 */}
<p className="text-[24px] font-semibold">{title} 선택하세요</p>
{/* 단일 선택 버튼 */}
<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>
Comment on lines +17 to +26
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

포커스 상태 시각적 피드백 개선 필요

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.

Suggested change
<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.

</div>
);
};

export default SingleSelect;
16 changes: 16 additions & 0 deletions src/components/onboarding/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const TextInput = ({ title, placeholder }: { title: string; placeholder?: string }) => {
return (
<div className="flex flex-col gap-6">
{/* 제목 */}
<p className="text-[24px] font-semibold">{title} 입력해주세요</p>
{/* 입력칸 */}
<input
type="text"
placeholder={placeholder}
className="rounded-xl border border-gray-200 px-2 py-4 text-[20px] outline-none focus:border-gray-200"
/>
Comment on lines +7 to +11
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

포커스 상태 시각적 피드백 개선 필요

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.

Suggested change
<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.

</div>
);
};

export default TextInput;
2 changes: 1 addition & 1 deletion src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const HomePage = () => {
return (
<div className="flex h-full flex-col items-center justify-center gap-8">
<img src={logo} alt="Vite logo" className="h-40" />
<h1 className="text-6xl text-[#0D2D84]">Snowgent</h1>
<h1 className="text-6xl font-bold text-[#0D2D84]">Snowgent</h1>
<button
onClick={() => navigate('/onboarding')}
className="cursor-pointer rounded-lg bg-blue-50 px-10 py-5 text-xl font-semibold text-[#0D2D84] hover:bg-blue-100"
Expand Down
79 changes: 78 additions & 1 deletion src/pages/onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
import { useState } from 'react';
import Navigation from '../../components/Navigation';
import FormButton from '../../components/onboarding/FormButton';
import { useNavigate } from 'react-router-dom';
import TextInput from '../../components/onboarding/TextInput';
import PriceInput from '../../components/onboarding/PriceInput';
import SingleSelect from '../../components/onboarding/SingleSelect';
import MultiSelect from '../../components/onboarding/MultiSelect';

const Onboarding = () => {
return <div>Onboarding</div>;
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 5;

const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};

const handleNext = () => {
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};

const handleComplete = () => {
navigate('/chat');
};

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="원하는 타겟층" />;
}
};
Comment on lines +31 to +46
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.


return (
<div className="flex h-full flex-col">
<Navigation />
<div className="flex flex-1 flex-col p-5">
<div className="mb-6">
<div className="mb-2 flex justify-between text-sm text-gray-600">
<span>단계 {currentStep}</span>
<span>
{currentStep} / {totalSteps}
</span>
</div>
<div className="h-2 w-full rounded-full bg-gray-200">
<div
className="h-2 rounded-full bg-blue-500 transition-all duration-300"
style={{ width: `${(currentStep / totalSteps) * 100}%` }}
/>
</div>
</div>

<div className="flex-1 overflow-y-auto">{renderStepContent()}</div>

<div className="mt-6 flex gap-3">
{currentStep > 1 && <FormButton type="prev" onClick={handlePrev} />}
{currentStep === totalSteps ? (
<FormButton type="submit" onClick={handleComplete} />
) : (
<FormButton type="next" onClick={handleNext} />
)}
</div>
</div>
</div>
);
};

export default Onboarding;