From 282d5e85a19d7a2ca82f3d232953e257dfde5cc3 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 19 Feb 2026 13:00:20 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20checkbox=20component=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/checkbox/Checkbox.stories.tsx | 49 +++++++ .../src/components/checkbox/Checkbox.tsx | 128 ++++++++++++++++++ .../design-system/src/components/index.ts | 1 + 3 files changed, 178 insertions(+) create mode 100644 packages/design-system/src/components/checkbox/Checkbox.stories.tsx create mode 100644 packages/design-system/src/components/checkbox/Checkbox.tsx diff --git a/packages/design-system/src/components/checkbox/Checkbox.stories.tsx b/packages/design-system/src/components/checkbox/Checkbox.stories.tsx new file mode 100644 index 00000000..923863b5 --- /dev/null +++ b/packages/design-system/src/components/checkbox/Checkbox.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import Checkbox from './Checkbox'; + +const meta: Meta = { + 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; + +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 }, +}; diff --git a/packages/design-system/src/components/checkbox/Checkbox.tsx b/packages/design-system/src/components/checkbox/Checkbox.tsx new file mode 100644 index 00000000..ef49318b --- /dev/null +++ b/packages/design-system/src/components/checkbox/Checkbox.tsx @@ -0,0 +1,128 @@ +import { cva } from 'class-variance-authority'; +import { useId, useState } from 'react'; +import { cn } from '../../lib/utils'; + +interface CheckboxProps + extends Omit< + React.InputHTMLAttributes, + 'type' | 'size' | 'checked' | 'defaultChecked' | 'onChange' + > { + isSelected?: boolean; + defaultSelected?: boolean; + onSelectedChange?: (checked: boolean) => void; + size?: 'small' | 'medium'; +} + +const checkboxBoxVariants = cva( + 'inline-flex items-center justify-center rounded-[0.4rem] transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-main400 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white-bg', + { + variants: { + size: { + small: 'h-[2rem] w-[2rem]', + medium: 'h-[2.8rem] w-[2.8rem]', + }, + selected: { + true: 'bg-main500 border-[1.5px] border-main500', + false: 'bg-gray0 border-gray100 border-[1.5px]', + }, + disabled: { + true: 'opacity-50', + false: '', + }, + }, + defaultVariants: { + size: 'medium', + selected: false, + disabled: false, + }, + } +); + +const checkIconVariants = cva('', { + variants: { + size: { + small: 'h-[0.8rem] w-[1.1rem]', + medium: 'h-[0.9rem] w-[1.2rem]', + }, + selected: { + true: 'text-white', + false: 'text-gray300', + }, + }, + defaultVariants: { + size: 'medium', + selected: false, + }, +}); + +const Checkbox = ({ + isSelected, + defaultSelected = false, + onSelectedChange, + size = 'medium', + className, + id, + disabled, + ...props +}: CheckboxProps) => { + const [internalSelected, setInternalSelected] = useState(defaultSelected); + const generatedId = useId(); + const inputId = id ?? generatedId; + const selected = isSelected ?? internalSelected; + + const handleChange = (e: React.ChangeEvent) => { + const nextChecked = e.target.checked; + onSelectedChange?.(nextChecked); + if (isSelected === undefined) { + setInternalSelected(nextChecked); + } + }; + + return ( +
+ + +
+ ); +}; + +export default Checkbox; diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts index fc1f291b..9d22080e 100644 --- a/packages/design-system/src/components/index.ts +++ b/packages/design-system/src/components/index.ts @@ -2,6 +2,7 @@ export { default as Badge } from './badge/Badge'; export { default as Button } from './button/Button'; export { default as Card } from './card/Card'; export { default as Chip } from './chip/Chip'; +export { default as Checkbox } from './checkbox/Checkbox'; export { default as Dropdown } from './dropdown/Dropdown'; export { default as Input } from './input/Input'; export { default as DateTime } from './dateTime/DateTime'; From 9a3a2860e58724165a35340ffe4eafcb0ab24b36 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 19 Feb 2026 13:00:47 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20onboarding=20jobStep=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/onBoarding/jobs/jobBackend.svg | 31 ++++ .../assets/onBoarding/jobs/jobDesign.svg | 31 ++++ .../assets/onBoarding/jobs/jobFrontend.svg | 29 ++++ .../public/assets/onBoarding/jobs/jobPlan.svg | 26 +++ .../components/funnel/step/JobStep.tsx | 148 ++++++++++++++++++ .../onBoarding/constants/onboardingSteps.ts | 2 + 6 files changed, 267 insertions(+) create mode 100644 apps/client/public/assets/onBoarding/jobs/jobBackend.svg create mode 100644 apps/client/public/assets/onBoarding/jobs/jobDesign.svg create mode 100644 apps/client/public/assets/onBoarding/jobs/jobFrontend.svg create mode 100644 apps/client/public/assets/onBoarding/jobs/jobPlan.svg create mode 100644 apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx diff --git a/apps/client/public/assets/onBoarding/jobs/jobBackend.svg b/apps/client/public/assets/onBoarding/jobs/jobBackend.svg new file mode 100644 index 00000000..89909531 --- /dev/null +++ b/apps/client/public/assets/onBoarding/jobs/jobBackend.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/public/assets/onBoarding/jobs/jobDesign.svg b/apps/client/public/assets/onBoarding/jobs/jobDesign.svg new file mode 100644 index 00000000..a76eabaf --- /dev/null +++ b/apps/client/public/assets/onBoarding/jobs/jobDesign.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/public/assets/onBoarding/jobs/jobFrontend.svg b/apps/client/public/assets/onBoarding/jobs/jobFrontend.svg new file mode 100644 index 00000000..db561232 --- /dev/null +++ b/apps/client/public/assets/onBoarding/jobs/jobFrontend.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/public/assets/onBoarding/jobs/jobPlan.svg b/apps/client/public/assets/onBoarding/jobs/jobPlan.svg new file mode 100644 index 00000000..c46d4772 --- /dev/null +++ b/apps/client/public/assets/onBoarding/jobs/jobPlan.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx b/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx new file mode 100644 index 00000000..0cbdfb04 --- /dev/null +++ b/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx @@ -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 = { + planner: jobPlan, + designer: jobDesign, + frontend: jobFrontend, + backend: jobBackend, + }; + + return ( + + ); +}; + +const JobStep = ({ + selectedJob, + onSelectJob, + agreeChecked, + onAgreeChange, +}: JobStepProps) => { + const defaultJob: JobKey = 'planner'; + const [internalJob, setInternalJob] = useState(defaultJob); + const [internalAgree, setInternalAgree] = useState(true); + 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 ( +
+ dotori +
+

직무를 선택해주세요

+

+ 직무에 따라 아티클을 추천해드려요 +

+
+ +
+ {jobs.map((job) => { + const isSelected = activeJob === job.key; + return ( + + ); + })} +
+ + +
+ ); +}; + +export default JobStep; diff --git a/apps/client/src/pages/onBoarding/constants/onboardingSteps.ts b/apps/client/src/pages/onBoarding/constants/onboardingSteps.ts index ca958fd7..79f9e958 100644 --- a/apps/client/src/pages/onBoarding/constants/onboardingSteps.ts +++ b/apps/client/src/pages/onBoarding/constants/onboardingSteps.ts @@ -3,6 +3,7 @@ export const Step = { STORY_1: 'STORY_1', STORY_2: 'STORY_2', SOCIAL_LOGIN: 'SOCIAL_LOGIN', + JOB: 'JOB', ALARM: 'ALARM', MAC: 'MAC', FINAL: 'FINAL', @@ -21,6 +22,7 @@ export const stepOrder: StepType[] = [ Step.STORY_1, Step.STORY_2, Step.SOCIAL_LOGIN, + Step.JOB, Step.ALARM, Step.MAC, Step.FINAL, From 107c0b4181f6fcf54200ff2048735933c235c41e Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 19 Feb 2026 13:01:07 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20jobStep=20mainCard=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onBoarding/components/funnel/MainCard.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx index 765bc5c1..e94def9c 100644 --- a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx +++ b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx @@ -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(null); + const [jobShareAgree, setJobShareAgree] = useState(true); useEffect(() => { const params = new URLSearchParams(location.search); @@ -131,6 +137,13 @@ const MainCard = () => { ); case Step.SOCIAL_LOGIN: return ; + case Step.JOB: + return ( + + ); case Step.ALARM: return ( @@ -201,6 +214,7 @@ const MainCard = () => {
{storySteps.includes(step) && ( @@ -246,6 +260,7 @@ const MainCard = () => { size="medium" className="ml-auto w-[4.8rem]" onClick={nextStep} + isDisabled={step === Step.JOB && !jobShareAgree} > 다음 From b46e4e7245b4a5c373c213303cabaab67bc7debb Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 19 Feb 2026 14:56:20 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20=EC=A0=9C=EC=96=B4/=EB=B9=84?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20fla?= =?UTF-8?q?g=20=EC=82=AC=EC=9A=A9=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/checkbox/Checkbox.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/design-system/src/components/checkbox/Checkbox.tsx b/packages/design-system/src/components/checkbox/Checkbox.tsx index ef49318b..3583e589 100644 --- a/packages/design-system/src/components/checkbox/Checkbox.tsx +++ b/packages/design-system/src/components/checkbox/Checkbox.tsx @@ -1,6 +1,6 @@ import { cva } from 'class-variance-authority'; -import { useId, useState } from 'react'; import { cn } from '../../lib/utils'; +import { useId, useState } from 'react'; interface CheckboxProps extends Omit< @@ -65,17 +65,21 @@ const Checkbox = ({ disabled, ...props }: CheckboxProps) => { - const [internalSelected, setInternalSelected] = useState(defaultSelected); const generatedId = useId(); const inputId = id ?? generatedId; - const selected = isSelected ?? internalSelected; + + const [internalSelected, setInternalSelected] = + useState(defaultSelected); + + const isUncontrolled = isSelected === undefined; + const selected = isUncontrolled ? internalSelected : isSelected; const handleChange = (e: React.ChangeEvent) => { const nextChecked = e.target.checked; + + if (isUncontrolled) setInternalSelected(nextChecked); + onSelectedChange?.(nextChecked); - if (isSelected === undefined) { - setInternalSelected(nextChecked); - } }; return ( @@ -95,6 +99,7 @@ const Checkbox = ({ onChange={handleChange} {...props} /> +
-