-
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 8 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,15 +37,19 @@ 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 flex-col items-center justify-between rounded-[2.4rem] pt-[3.2rem]', | ||
| { | ||
| variants: { | ||
| overflow: { | ||
| true: 'overflow-visible', | ||
| false: 'overflow-hidden', | ||
| }, | ||
| size: { | ||
| default: 'max-w-[63.2rem]', | ||
| wide: 'max-w-[82.6rem]', | ||
| }, | ||
| }, | ||
| defaultVariants: { overflow: false }, | ||
| defaultVariants: { overflow: false, size: 'default' }, | ||
| } | ||
| ); | ||
|
|
||
|
|
@@ -60,6 +65,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 +137,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} /> | ||
|
|
@@ -201,6 +214,7 @@ const MainCard = () => { | |
| <div | ||
| className={CardStyle({ | ||
| overflow: step === Step.ALARM && alarmSelected === 3, | ||
| size: step === Step.JOB ? 'wide' : 'default', | ||
|
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.
직무 step이 다른 알람 step과 다르게 더 넓은 width값을 가지고 있어서 이를 분기처리하긴했어요. 처음에는 container에 width값을 안주고 내부 요소에 width를 설정해서 조절하도록 할까 했는데, 맥북 알림/리마인드 시간 선택 step등이 내부는 다른데 container는 63.2rem으로 동일해서 일단 저렇게 wide값을 가질때를 분기했어요. 사실 저렇게 하면 step이 바뀔 때 container의 width값이 변경되니 CLS(레이아웃 변경 횟수) 평가 지표에 좋지 않은 영향이 갈 것이라고 생각해요. 그래서 일단 디자이너에게 통일할 수 있는 방법을 물어본 상태이고, 만약 안된다면 layout 변경을 최소화하고 최적화 할 수 있는지 체크해볼게요! |
||
| })} | ||
| > | ||
| {storySteps.includes(step) && ( | ||
|
|
@@ -246,6 +260,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,143 @@ | ||||||||||
| import { useMemo, 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 = useMemo( | ||||||||||
|
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. 여기서 왜 useMemo가 필요하나요?
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. 불필요한 useMemo 같네요! 제거 완완입니다~ |
||||||||||
| () => [ | ||||||||||
| { 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 as JobKey)} | ||||||||||
| className={jobCardStyle({ selected: isSelected })} | ||||||||||
| > | ||||||||||
| <div className="flex flex-col items-center gap-[1.6rem]"> | ||||||||||
| <JobIcon type={job.key as JobKey} /> | ||||||||||
| <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