diff --git a/apps/client/public/assets/jobSelectionFunnel/pinStep_description.svg b/apps/client/public/assets/jobSelectionFunnel/pinStep_description.svg new file mode 100644 index 00000000..7faec79d --- /dev/null +++ b/apps/client/public/assets/jobSelectionFunnel/pinStep_description.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/public/assets/jobSelectionFunnel/shareStep_description.svg b/apps/client/public/assets/jobSelectionFunnel/shareStep_description.svg new file mode 100644 index 00000000..c01de09f --- /dev/null +++ b/apps/client/public/assets/jobSelectionFunnel/shareStep_description.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/src/pages/onBoarding/GoogleCallback.tsx b/apps/client/src/pages/onBoarding/GoogleCallback.tsx index db9f6344..66e96989 100644 --- a/apps/client/src/pages/onBoarding/GoogleCallback.tsx +++ b/apps/client/src/pages/onBoarding/GoogleCallback.tsx @@ -21,7 +21,8 @@ const GoogleCallback = () => { const handleUserLogin = ( isUser: boolean, - accessToken: string | undefined + accessToken: string | undefined, + hasJob?: boolean ) => { if (isUser) { if (accessToken) { @@ -38,6 +39,9 @@ const GoogleCallback = () => { }; sendTokenToExtension(accessToken); } + if (typeof hasJob === 'boolean') { + localStorage.setItem('hasJob', String(hasJob)); + } navigate('/'); } else { navigate('/onboarding?step=ALARM'); @@ -54,12 +58,12 @@ const GoogleCallback = () => { code, uri: redirectUri, }); - const { isUser, userId, email, accessToken } = res.data.data; + const { isUser, userId, email, accessToken, hasJob } = res.data.data; localStorage.setItem('email', email); localStorage.setItem('userId', userId); - handleUserLogin(isUser, accessToken); + handleUserLogin(isUser, accessToken, hasJob); } catch (error) { console.error('로그인 오류:', error); navigate('/onboarding?step=SOCIAL_LOGIN'); diff --git a/apps/client/src/pages/remind/Remind.tsx b/apps/client/src/pages/remind/Remind.tsx index 1ad210df..da16360a 100644 --- a/apps/client/src/pages/remind/Remind.tsx +++ b/apps/client/src/pages/remind/Remind.tsx @@ -24,6 +24,7 @@ import FetchCard from './components/fetchCard/FetchCard'; import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; import Tooltip from '@shared/components/tooltip/Tooltip'; import Footer from './components/footer/Footer'; +import JobSelectionFunnel from '@shared/components/jobSelectionFunnel/JobSelectionFunnel'; const Remind = () => { useEffect(() => { @@ -34,6 +35,9 @@ const Remind = () => { const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead'); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); + const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState( + () => localStorage.getItem('hasJob') !== 'true' + ); const scrollContainerRef = useRef(null); const formattedDate = useMemo(() => { @@ -242,6 +246,17 @@ const Remind = () => { )} + + {showJobSelectionFunnel && ( +
+ { + // TODO: 관심 직무 핀 API 연동 필요 + setShowJobSelectionFunnel(false); + }} + /> +
+ )} ); }; diff --git a/apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx b/apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx new file mode 100644 index 00000000..974b90ed --- /dev/null +++ b/apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx @@ -0,0 +1,42 @@ +import { cn } from '@pinback/design-system/utils'; + +interface FunnelProgressProps { + currentIndex: number; + totalSteps: number; +} + +const FunnelProgress = ({ currentIndex, totalSteps }: FunnelProgressProps) => { + const maxIndex = Math.max(1, totalSteps - 1); + const percent = Math.max(0, Math.min(100, (currentIndex / maxIndex) * 100)); + + return ( +
+
+
+
+
+ {Array.from({ length: totalSteps }).map((_, index) => { + const isActive = index <= currentIndex; + return ( +
+ {index + 1} +
+ ); + })} +
+
+ ); +}; + +export default FunnelProgress; diff --git a/apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx b/apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx new file mode 100644 index 00000000..ccb4b48d --- /dev/null +++ b/apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx @@ -0,0 +1,69 @@ +import { Button } from '@pinback/design-system/ui'; +import { useFunnel } from '@shared/hooks/useFunnel'; +import { useState } from 'react'; +import FunnelProgress from './FunnelProgress'; +import JobStep, { JobKey } from './step/JobStep'; +import PinStep from './step/PinStep'; +import ShareStep from './step/ShareStep'; + +const funnelSteps = ['job', 'pin', 'share'] as const; +type FunnelStep = (typeof funnelSteps)[number]; + +interface JobSelectionFunnelProps { + onComplete?: () => void; +} + +export default function JobSelectionFunnel({ + onComplete, +}: JobSelectionFunnelProps) { + const { currentStep, currentIndex, goNext, isLastStep } = + useFunnel({ + steps: funnelSteps, + initialStep: 'job', + }); + + const [selectedJob, setSelectedJob] = useState('planner'); + const [jobShareAgree, setJobShareAgree] = useState(false); + + const handleNext = () => { + if (isLastStep) { + onComplete?.(); + return; + } + goNext(); + }; + + return ( +
+ + +
+ {currentStep === 'job' && ( + + )} + {currentStep === 'pin' && } + {currentStep === 'share' && } +
+ +
+ +
+
+ ); +} diff --git a/apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx b/apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx new file mode 100644 index 00000000..e37ad31c --- /dev/null +++ b/apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx @@ -0,0 +1,113 @@ +import { Checkbox } from '@pinback/design-system/ui'; +import { cn } from '@pinback/design-system/utils'; +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'; + +export type JobKey = 'planner' | 'designer' | 'frontend' | 'backend'; + +export interface JobStepProps { + selectedJob: JobKey; + onSelectJob: (job: JobKey) => void; + agreeChecked: boolean; + onAgreeChange: (checked: boolean) => void; +} + +const jobCardStyle = (selected: boolean) => + cn( + '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', + selected + ? 'border-main400 bg-main0' + : 'border-transparent bg-white-bg hover:border-main300' + ); + +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 jobs: { key: JobKey; label: string }[] = [ + { key: 'planner', label: '기획자' }, + { key: 'designer', label: '디자이너' }, + { key: 'frontend', label: '프론트엔드 개발자' }, + { key: 'backend', label: '백엔드 개발자' }, + ]; + + return ( +
+
+

직무를 선택해주세요

+

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

+
+ +
+ {jobs.map((job) => { + const isSelected = selectedJob === job.key; + return ( + + ); + })} +
+ + +
+ ); +}; + +export default JobStep; diff --git a/apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx b/apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx new file mode 100644 index 00000000..c3985e1f --- /dev/null +++ b/apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx @@ -0,0 +1,26 @@ +import pinStepImage from '/assets/jobSelectionFunnel/pinStep_description.svg'; + +const PinStep = () => { + return ( +
+
+

+ 관심 직무 핀이 새롭게 개설되었어요! +

+

+ 우측 사이드바를 통해 탐색해보세요 +

+
+ +
+ 관심 직무 핀 안내 +
+
+ ); +}; + +export default PinStep; diff --git a/apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx b/apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx new file mode 100644 index 00000000..67de9563 --- /dev/null +++ b/apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx @@ -0,0 +1,27 @@ +import shareStepImage from '/assets/jobSelectionFunnel/shareStep_description.svg'; + +const ShareStep = () => { + return ( +
+
+

+ 내가 저장한 아티클을 공유할 수 있어요 +

+

+ 카테고리를 공유해 다른 사용자들에게 내가 저장한 좋은 아티클을 보여줄 + 수 있어요 +

+
+ +
+ 카테고리 공유 안내 +
+
+ ); +}; + +export default ShareStep;