-
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 3 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); | ||
|
|
||
| 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', | ||
|
||
| })} | ||
| > | ||
| {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,148 @@ | ||||||||||
| import { useId, 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 agreeId = useId(); | ||||||||||
|
||||||||||
|
|
||||||||||
| const activeJob = selectedJob ?? internalJob; | ||||||||||
| const activeAgree = agreeChecked ?? internalAgree; | ||||||||||
|
|
||||||||||
| const jobs = 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 | ||||||||||
| htmlFor={agreeId} | ||||||||||
| className="mt-[2.4rem] flex max-w-[62rem] items-start gap-[1.2rem]" | ||||||||||
| > | ||||||||||
| <Checkbox | ||||||||||
| id={agreeId} | ||||||||||
| 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.
jobShareAgree기본값true+ 미동의 시 진행 불가 = 강제 동의 패턴jobShareAgree가true로 초기화되어 동의 체크박스가 사전 선택된 상태로 표시되고, 사용자가 이를 해제하면isDisabled={step === Step.JOB && !jobShareAgree}에 의해 다음 버튼이 비활성화됩니다. 이는 사실상 "동의하지 않으면 사용 불가"인 강제 동의 구조로, GDPR 제7조 4항의 동의 번들링 금지 원칙(개인정보 공유 동의를 서비스 이용의 조건으로 강제)에 위반될 수 있습니다.권장 수정:
false로 변경하여 사용자가 명시적으로 동의를 선택(opt-in)하도록 설계🛡️ 제안된 수정
Also applies to: 263-263
🤖 Prompt for AI Agents