Skip to content

Commit 1c67694

Browse files
Merge pull request #257 from Pinback-Team/feat/#256/onboarding-job-step
Feat(client, design-system): 온보딩 직무 step UI 추가, checkbox 구현
2 parents 0ef7e22 + d2d8f41 commit 1c67694

File tree

11 files changed

+461
-2
lines changed

11 files changed

+461
-2
lines changed

apps/client/public/assets/onBoarding/jobs/jobBackend.svg

Lines changed: 31 additions & 0 deletions
Loading

apps/client/public/assets/onBoarding/jobs/jobDesign.svg

Lines changed: 31 additions & 0 deletions
Loading

apps/client/public/assets/onBoarding/jobs/jobFrontend.svg

Lines changed: 29 additions & 0 deletions
Loading

apps/client/public/assets/onBoarding/jobs/jobPlan.svg

Lines changed: 26 additions & 0 deletions
Loading

apps/client/src/pages/onBoarding/GoogleCallback.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,16 @@ const GoogleCallback = () => {
4444
}
4545
};
4646

47+
const redirectUri = import.meta.env.PROD
48+
? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
49+
: import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
50+
4751
const loginWithCode = async (code: string) => {
4852
try {
49-
const res = await apiRequest.post('/api/v2/auth/google', { code });
53+
const res = await apiRequest.post('/api/v3/auth/google', {
54+
code,
55+
uri: redirectUri,
56+
});
5057
const { isUser, userId, email, accessToken } = res.data.data;
5158

5259
localStorage.setItem('email', email);

apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useState, useEffect, lazy, Suspense } from 'react';
33
import { motion, AnimatePresence } from 'framer-motion';
44
import SocialLoginStep from './step/SocialLoginStep';
55
const StoryStep = lazy(() => import('./step/StoryStep'));
6+
const JobStep = lazy(() => import('./step/JobStep'));
67
const AlarmStep = lazy(() => import('./step/AlarmStep'));
78
const MacStep = lazy(() => import('./step/MacStep'));
89
const FinalStep = lazy(() => import('./step/FinalStep'));
@@ -36,7 +37,7 @@ const variants = {
3637
};
3738

3839
const CardStyle = cva(
39-
'bg-white-bg flex h-[54.8rem] w-[63.2rem] flex-col items-center justify-between rounded-[2.4rem] pt-[3.2rem]',
40+
'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]',
4041
{
4142
variants: {
4243
overflow: {
@@ -60,6 +61,7 @@ const MainCard = () => {
6061
const [userEmail, setUserEmail] = useState('');
6162
const [remindTime, setRemindTime] = useState('09:00');
6263
const [fcmToken, setFcmToken] = useState<string | null>(null);
64+
const [jobShareAgree, setJobShareAgree] = useState(true);
6365

6466
useEffect(() => {
6567
const params = new URLSearchParams(location.search);
@@ -131,6 +133,13 @@ const MainCard = () => {
131133
);
132134
case Step.SOCIAL_LOGIN:
133135
return <SocialLoginStep />;
136+
case Step.JOB:
137+
return (
138+
<JobStep
139+
agreeChecked={jobShareAgree}
140+
onAgreeChange={setJobShareAgree}
141+
/>
142+
);
134143
case Step.ALARM:
135144
return (
136145
<AlarmStep selected={alarmSelected} setSelected={setAlarmSelected} />
@@ -251,6 +260,7 @@ const MainCard = () => {
251260
size="medium"
252261
className="ml-auto w-[4.8rem]"
253262
onClick={nextStep}
263+
isDisabled={step === Step.JOB && !jobShareAgree}
254264
>
255265
다음
256266
</Button>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { useState } from 'react';
2+
import { cva } from 'class-variance-authority';
3+
import dotori from '/assets/onBoarding/icons/dotori.svg';
4+
import jobPlan from '/assets/onBoarding/jobs/jobPlan.svg';
5+
import jobDesign from '/assets/onBoarding/jobs/jobDesign.svg';
6+
import jobFrontend from '/assets/onBoarding/jobs/jobFrontend.svg';
7+
import jobBackend from '/assets/onBoarding/jobs/jobBackend.svg';
8+
import { Checkbox } from '@pinback/design-system/ui';
9+
10+
type JobKey = 'planner' | 'designer' | 'frontend' | 'backend';
11+
12+
interface JobStepProps {
13+
selectedJob?: JobKey;
14+
onSelectJob?: (job: JobKey) => void;
15+
agreeChecked?: boolean;
16+
onAgreeChange?: (checked: boolean) => void;
17+
}
18+
19+
const jobCardStyle = cva(
20+
'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',
21+
{
22+
variants: {
23+
selected: {
24+
true: 'border-main400 bg-main0',
25+
false: 'border-transparent bg-white-bg hover:border-main300',
26+
},
27+
},
28+
defaultVariants: { selected: false },
29+
}
30+
);
31+
32+
const JobIcon = ({ type }: { type: JobKey }) => {
33+
const iconMap: Record<JobKey, string> = {
34+
planner: jobPlan,
35+
designer: jobDesign,
36+
frontend: jobFrontend,
37+
backend: jobBackend,
38+
};
39+
40+
return (
41+
<img
42+
src={iconMap[type]}
43+
alt={`${type} 직무 아이콘`}
44+
aria-hidden="true"
45+
className="h-[10.2rem] w-[10.2rem]"
46+
/>
47+
);
48+
};
49+
50+
const JobStep = ({
51+
selectedJob,
52+
onSelectJob,
53+
agreeChecked,
54+
onAgreeChange,
55+
}: JobStepProps) => {
56+
const defaultJob: JobKey = 'planner';
57+
const [internalJob, setInternalJob] = useState<JobKey>(defaultJob);
58+
const [internalAgree, setInternalAgree] = useState(true);
59+
60+
const activeJob = selectedJob ?? internalJob;
61+
const activeAgree = agreeChecked ?? internalAgree;
62+
63+
const jobs: { key: JobKey; label: string }[] = [
64+
{ key: 'planner', label: '기획자' },
65+
{ key: 'designer', label: '디자이너' },
66+
{ key: 'frontend', label: '프론트엔드' },
67+
{ key: 'backend', label: '백엔드' },
68+
];
69+
70+
const handleSelect = (job: JobKey) => {
71+
onSelectJob?.(job);
72+
if (!onSelectJob || selectedJob === undefined) {
73+
setInternalJob(job);
74+
}
75+
};
76+
77+
const handleAgreeChange = (checked: boolean) => {
78+
onAgreeChange?.(checked);
79+
if (!onAgreeChange || agreeChecked === undefined) {
80+
setInternalAgree(checked);
81+
}
82+
};
83+
84+
return (
85+
<div className="flex w-full flex-col items-center">
86+
<img src={dotori} className="mb-[1.2rem]" alt="dotori" />
87+
<div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]">
88+
<p className="head3 text-font-black-1">직무를 선택해주세요</p>
89+
<p className="body2-m text-font-gray-3 text-center">
90+
직무에 따라 아티클을 추천해드려요
91+
</p>
92+
</div>
93+
94+
<div
95+
role="radiogroup"
96+
aria-label="직무 선택"
97+
className="grid w-full grid-cols-2 justify-items-center gap-[1.4rem] sm:grid-cols-4"
98+
>
99+
{jobs.map((job) => {
100+
const isSelected = activeJob === job.key;
101+
return (
102+
<button
103+
key={job.key}
104+
type="button"
105+
role="radio"
106+
aria-checked={isSelected}
107+
onClick={() => handleSelect(job.key)}
108+
className={jobCardStyle({ selected: isSelected })}
109+
>
110+
<div className="flex flex-col items-center gap-[1.6rem]">
111+
<JobIcon type={job.key} />
112+
<span
113+
className={`sub3-sb ${
114+
isSelected ? 'text-main500' : 'text-font-black-1'
115+
}`}
116+
>
117+
{job.label}
118+
</span>
119+
</div>
120+
</button>
121+
);
122+
})}
123+
</div>
124+
125+
<label className="mt-[2.4rem] flex max-w-[62rem] items-start gap-[1.2rem]">
126+
<Checkbox
127+
size="small"
128+
isSelected={activeAgree}
129+
onSelectedChange={handleAgreeChange}
130+
/>
131+
<span className="body3-r text-font-gray-3">
132+
내가 북마크한 아티클이 내 Google 이름과 함께 다른 사용자에게 추천될 수
133+
있어요.
134+
</span>
135+
</label>
136+
</div>
137+
);
138+
};
139+
140+
export default JobStep;

apps/client/src/pages/onBoarding/constants/onboardingSteps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const Step = {
33
STORY_1: 'STORY_1',
44
STORY_2: 'STORY_2',
55
SOCIAL_LOGIN: 'SOCIAL_LOGIN',
6+
JOB: 'JOB',
67
ALARM: 'ALARM',
78
MAC: 'MAC',
89
FINAL: 'FINAL',
@@ -21,6 +22,7 @@ export const stepOrder: StepType[] = [
2122
Step.STORY_1,
2223
Step.STORY_2,
2324
Step.SOCIAL_LOGIN,
25+
Step.JOB,
2426
Step.ALARM,
2527
Step.MAC,
2628
Step.FINAL,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import Checkbox from './Checkbox';
3+
4+
const meta: Meta<typeof Checkbox> = {
5+
title: 'Components/Checkbox',
6+
component: Checkbox,
7+
tags: ['autodocs'],
8+
parameters: {
9+
layout: 'centered',
10+
docs: {
11+
description: {
12+
component:
13+
'기본 체크박스 컴포넌트입니다. `isSelected`로 제어하고 `onSelectedChange`로 상태를 전달합니다. `size`로 크기를 조절할 수 있습니다.',
14+
},
15+
},
16+
},
17+
argTypes: {
18+
isSelected: { control: 'boolean' },
19+
defaultSelected: { control: 'boolean' },
20+
size: { control: 'inline-radio', options: ['small', 'medium'] },
21+
disabled: { control: 'boolean' },
22+
onSelectedChange: { action: 'selected' },
23+
className: { table: { disable: true } },
24+
},
25+
args: {
26+
size: 'medium',
27+
isSelected: false,
28+
},
29+
};
30+
31+
export default meta;
32+
33+
type Story = StoryObj<typeof Checkbox>;
34+
35+
export const Default: Story = {
36+
args: { isSelected: false },
37+
};
38+
39+
export const Selected: Story = {
40+
args: { isSelected: true },
41+
};
42+
43+
export const Medium: Story = {
44+
args: { size: 'medium', isSelected: true },
45+
};
46+
47+
export const Disabled: Story = {
48+
args: { isSelected: true, disabled: true },
49+
};

0 commit comments

Comments
 (0)