-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(client, design-system): 온보딩 직무 step UI 추가, checkbox 구현 #257
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
Changes from all commits
282d5e8
9a3a286
107c0b4
b46e4e7
104c8e9
88232ae
2aff72f
cadc1c3
7fe4555
572f680
d2d8f41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { useState, useEffect, lazy, Suspense } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | ||
| import SocialLoginStep from './step/SocialLoginStep'; | ||
| const StoryStep = lazy(() => import('./step/StoryStep')); | ||
| const JobStep = lazy(() => import('./step/JobStep')); | ||
| const AlarmStep = lazy(() => import('./step/AlarmStep')); | ||
| const MacStep = lazy(() => import('./step/MacStep')); | ||
| const FinalStep = lazy(() => import('./step/FinalStep')); | ||
|
|
@@ -36,7 +37,7 @@ const variants = { | |
| }; | ||
|
|
||
| const CardStyle = cva( | ||
| 'bg-white-bg flex h-[54.8rem] w-[63.2rem] flex-col items-center justify-between rounded-[2.4rem] pt-[3.2rem]', | ||
| 'bg-white-bg flex h-[54.8rem] w-full max-w-[82.6rem] flex-col items-center justify-between rounded-[2.4rem] pt-[3.2rem]', | ||
| { | ||
| variants: { | ||
| overflow: { | ||
|
|
@@ -60,6 +61,7 @@ const MainCard = () => { | |
| const [userEmail, setUserEmail] = useState(''); | ||
| const [remindTime, setRemindTime] = useState('09:00'); | ||
| const [fcmToken, setFcmToken] = useState<string | null>(null); | ||
| const [jobShareAgree, setJobShareAgree] = useState(true); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
권장 수정:
🛡️ 제안된 수정- const [jobShareAgree, setJobShareAgree] = useState(true);
+ const [jobShareAgree, setJobShareAgree] = useState(false);- isDisabled={step === Step.JOB && !jobShareAgree}
+ isDisabled={false} // 동의는 선택 사항으로 처리Also applies to: 263-263 🤖 Prompt for AI Agents |
||
|
|
||
| useEffect(() => { | ||
| const params = new URLSearchParams(location.search); | ||
|
|
@@ -131,6 +133,13 @@ const MainCard = () => { | |
| ); | ||
| case Step.SOCIAL_LOGIN: | ||
| return <SocialLoginStep />; | ||
| case Step.JOB: | ||
| return ( | ||
| <JobStep | ||
| agreeChecked={jobShareAgree} | ||
| onAgreeChange={setJobShareAgree} | ||
| /> | ||
| ); | ||
|
Comment on lines
+136
to
+142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 선택된 직무(job)가 API 호출에 포함되지 않아 기능 목적이 불완전
직무 데이터가 온보딩 완료 시 실제로 필요하다면 MainCard 수준에서 상태로 관리하고 API 페이로드에 포함해야 합니다. // MainCard 내 상태 추가
const [selectedJob, setSelectedJob] = useState<JobKey>('planner');
// JobStep에 전달
<JobStep
selectedJob={selectedJob}
onSelectJob={setSelectedJob}
agreeChecked={jobShareAgree}
onAgreeChange={setJobShareAgree}
/>
// postSignData 페이로드에 포함
postSignData({ email: userEmail, remindDefault: remindTime, fcmToken, job: selectedJob }, ...);🤖 Prompt for AI Agents |
||
| case Step.ALARM: | ||
| return ( | ||
| <AlarmStep selected={alarmSelected} setSelected={setAlarmSelected} /> | ||
|
|
@@ -246,6 +255,7 @@ const MainCard = () => { | |
| size="medium" | ||
| className="ml-auto w-[4.8rem]" | ||
| onClick={nextStep} | ||
| isDisabled={step === Step.JOB && !jobShareAgree} | ||
| > | ||
| 다음 | ||
| </Button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||||||||
| import { useState } from 'react'; | ||||||||||
| import { cva } from 'class-variance-authority'; | ||||||||||
| import dotori from '/assets/onBoarding/icons/dotori.svg'; | ||||||||||
| import jobPlan from '/assets/onBoarding/jobs/jobPlan.svg'; | ||||||||||
| import jobDesign from '/assets/onBoarding/jobs/jobDesign.svg'; | ||||||||||
| import jobFrontend from '/assets/onBoarding/jobs/jobFrontend.svg'; | ||||||||||
| import jobBackend from '/assets/onBoarding/jobs/jobBackend.svg'; | ||||||||||
| import { Checkbox } from '@pinback/design-system/ui'; | ||||||||||
|
|
||||||||||
| type JobKey = 'planner' | 'designer' | 'frontend' | 'backend'; | ||||||||||
|
|
||||||||||
| interface JobStepProps { | ||||||||||
| selectedJob?: JobKey; | ||||||||||
| onSelectJob?: (job: JobKey) => void; | ||||||||||
| agreeChecked?: boolean; | ||||||||||
| onAgreeChange?: (checked: boolean) => void; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const jobCardStyle = cva( | ||||||||||
| 'flex h-[22.4rem] w-[18rem] flex-col items-center justify-center rounded-[1.2rem] border transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main400 focus-visible:ring-offset-2 focus-visible:ring-offset-white-bg', | ||||||||||
| { | ||||||||||
| variants: { | ||||||||||
| selected: { | ||||||||||
| true: 'border-main400 bg-main0', | ||||||||||
| false: 'border-transparent bg-white-bg hover:border-main300', | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| defaultVariants: { selected: false }, | ||||||||||
| } | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const JobIcon = ({ type }: { type: JobKey }) => { | ||||||||||
| const iconMap: Record<JobKey, string> = { | ||||||||||
| planner: jobPlan, | ||||||||||
| designer: jobDesign, | ||||||||||
| frontend: jobFrontend, | ||||||||||
| backend: jobBackend, | ||||||||||
| }; | ||||||||||
|
Comment on lines
+32
to
+38
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 따로 타입 분리하는건 어떤가요
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 JobIcon은 컴포넌트입니다! JobStep과 많이 연관되어있다고 판단해서 안에 같이 두는 것도 좋을 것 같은데 어떻게 생각하시나요? |
||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <img | ||||||||||
| src={iconMap[type]} | ||||||||||
| alt={`${type} 직무 아이콘`} | ||||||||||
| aria-hidden="true" | ||||||||||
| className="h-[10.2rem] w-[10.2rem]" | ||||||||||
| /> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const JobStep = ({ | ||||||||||
| selectedJob, | ||||||||||
| onSelectJob, | ||||||||||
| agreeChecked, | ||||||||||
| onAgreeChange, | ||||||||||
| }: JobStepProps) => { | ||||||||||
| const defaultJob: JobKey = 'planner'; | ||||||||||
| const [internalJob, setInternalJob] = useState<JobKey>(defaultJob); | ||||||||||
| const [internalAgree, setInternalAgree] = useState(true); | ||||||||||
|
Comment on lines
+57
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
동의 체크박스가 기본적으로 선택된 상태( 기본값을 🛡️ 제안된 수정- const [internalAgree, setInternalAgree] = useState(true);
+ const [internalAgree, setInternalAgree] = useState(false);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| const activeJob = selectedJob ?? internalJob; | ||||||||||
| const activeAgree = agreeChecked ?? internalAgree; | ||||||||||
|
|
||||||||||
| const jobs: { key: JobKey; label: string }[] = [ | ||||||||||
| { key: 'planner', label: '기획자' }, | ||||||||||
| { key: 'designer', label: '디자이너' }, | ||||||||||
| { key: 'frontend', label: '프론트엔드' }, | ||||||||||
| { key: 'backend', label: '백엔드' }, | ||||||||||
| ]; | ||||||||||
|
|
||||||||||
| const handleSelect = (job: JobKey) => { | ||||||||||
| onSelectJob?.(job); | ||||||||||
| if (!onSelectJob || selectedJob === undefined) { | ||||||||||
| setInternalJob(job); | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const handleAgreeChange = (checked: boolean) => { | ||||||||||
| onAgreeChange?.(checked); | ||||||||||
| if (!onAgreeChange || agreeChecked === undefined) { | ||||||||||
| setInternalAgree(checked); | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <div className="flex w-full flex-col items-center"> | ||||||||||
| <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> | ||||||||||
| <div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]"> | ||||||||||
| <p className="head3 text-font-black-1">직무를 선택해주세요</p> | ||||||||||
| <p className="body2-m text-font-gray-3 text-center"> | ||||||||||
| 직무에 따라 아티클을 추천해드려요 | ||||||||||
| </p> | ||||||||||
| </div> | ||||||||||
|
|
||||||||||
| <div | ||||||||||
| role="radiogroup" | ||||||||||
| aria-label="직무 선택" | ||||||||||
| className="grid w-full grid-cols-2 justify-items-center gap-[1.4rem] sm:grid-cols-4" | ||||||||||
| > | ||||||||||
| {jobs.map((job) => { | ||||||||||
| const isSelected = activeJob === job.key; | ||||||||||
| return ( | ||||||||||
| <button | ||||||||||
| key={job.key} | ||||||||||
| type="button" | ||||||||||
| role="radio" | ||||||||||
| aria-checked={isSelected} | ||||||||||
| onClick={() => handleSelect(job.key)} | ||||||||||
| className={jobCardStyle({ selected: isSelected })} | ||||||||||
| > | ||||||||||
| <div className="flex flex-col items-center gap-[1.6rem]"> | ||||||||||
| <JobIcon type={job.key} /> | ||||||||||
| <span | ||||||||||
| className={`sub3-sb ${ | ||||||||||
| isSelected ? 'text-main500' : 'text-font-black-1' | ||||||||||
| }`} | ||||||||||
| > | ||||||||||
| {job.label} | ||||||||||
| </span> | ||||||||||
| </div> | ||||||||||
| </button> | ||||||||||
| ); | ||||||||||
| })} | ||||||||||
| </div> | ||||||||||
|
Comment on lines
94
to
123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
WAI-ARIA Authoring Practices의 Radio Group 패턴에 따르면, 화살표 키 핸들러를 추가하거나, ARIA radiogroup 패턴 대신 단순 ♿ 화살표 키 내비게이션 추가 예시+ const jobKeys = jobs.map((j) => j.key as JobKey);
+
+ const handleKeyDown = (e: React.KeyboardEvent, currentKey: JobKey) => {
+ const idx = jobKeys.indexOf(currentKey);
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ handleSelect(jobKeys[(idx + 1) % jobKeys.length]);
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ handleSelect(jobKeys[(idx - 1 + jobKeys.length) % jobKeys.length]);
+ }
+ };각 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| <label className="mt-[2.4rem] flex max-w-[62rem] items-start gap-[1.2rem]"> | ||||||||||
| <Checkbox | ||||||||||
| size="small" | ||||||||||
| isSelected={activeAgree} | ||||||||||
| onSelectedChange={handleAgreeChange} | ||||||||||
| /> | ||||||||||
| <span className="body3-r text-font-gray-3"> | ||||||||||
| 내가 북마크한 아티클이 내 Google 이름과 함께 다른 사용자에게 추천될 수 | ||||||||||
| 있어요. | ||||||||||
| </span> | ||||||||||
| </label> | ||||||||||
| </div> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export default JobStep; | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Checkbox from './Checkbox'; | ||
|
|
||
| const meta: Meta<typeof Checkbox> = { | ||
| title: 'Components/Checkbox', | ||
| component: Checkbox, | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| docs: { | ||
| description: { | ||
| component: | ||
| '기본 체크박스 컴포넌트입니다. `isSelected`로 제어하고 `onSelectedChange`로 상태를 전달합니다. `size`로 크기를 조절할 수 있습니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| isSelected: { control: 'boolean' }, | ||
| defaultSelected: { control: 'boolean' }, | ||
| size: { control: 'inline-radio', options: ['small', 'medium'] }, | ||
| disabled: { control: 'boolean' }, | ||
| onSelectedChange: { action: 'selected' }, | ||
| className: { table: { disable: true } }, | ||
| }, | ||
| args: { | ||
| size: 'medium', | ||
| isSelected: false, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Checkbox>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { isSelected: false }, | ||
| }; | ||
|
|
||
| export const Selected: Story = { | ||
| args: { isSelected: true }, | ||
| }; | ||
|
|
||
| export const Medium: Story = { | ||
| args: { size: 'medium', isSelected: true }, | ||
| }; | ||
|
|
||
| export const Disabled: Story = { | ||
| args: { isSelected: true, disabled: true }, | ||
| }; |
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.
환경 변수 미설정 시
uri가undefined로 전송되어 API 요청이 실패할 수 있습니다VITE_GOOGLE_REDIRECT_URI_PROD또는VITE_GOOGLE_REDIRECT_URI_DEV가 설정되지 않으면redirectUri는undefined가 됩니다.{ code, uri: undefined }는JSON.stringify시uri키가 생략되어 ({"code":"..."}), v3 엔드포인트가 필수 필드 부재로 4xx를 반환하고catch블록에서/onboarding?step=SOCIAL_LOGIN으로 조용히 리다이렉트됩니다.🛡️ 제안된 수정 — 폴백 & 사전 검증 추가
const loginWithCode = async (code: string) => { try { + if (!redirectUri) { + console.error('Redirect URI가 설정되지 않았습니다.'); + navigate('/onboarding?step=SOCIAL_LOGIN'); + return; + } const res = await apiRequest.post('/api/v3/auth/google', { code, uri: redirectUri, });📝 Committable suggestion
🤖 Prompt for AI Agents