Skip to content

Commit 63d6ffe

Browse files
committed
feat: 퍼널 구조 적용
1 parent 9a70e01 commit 63d6ffe

File tree

8 files changed

+274
-30
lines changed

8 files changed

+274
-30
lines changed

apps/client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"class-variance-authority": "^0.7.1",
14+
"framer-motion": "^12.23.12",
1315
"react": "^19.1.1",
1416
"react-dom": "^19.1.1",
1517
"react-router-dom": "^7.8.2"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import onBoardingBg from '../../assets/onBoarding/background/onBoardingBg.svg';
22
import Header from './components/header/Header';
3-
import MainCard from './components/carousel/MainCard';
3+
import MainCard from './components/funnel/MainCard';
44
const OnBoarding = () => {
55
return (
66
<div

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

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import avatar1 from '../../../../assets/onBoarding/icons/chippi_morning.svg';
2+
import avatar2 from '../../../../assets/onBoarding/icons/chippi_night.svg';
3+
import avatar3 from '../../../../assets/onBoarding/icons/chippi_bell.svg';
4+
import { cva } from 'class-variance-authority';
5+
6+
interface AlarmBoxProps {
7+
select: 1 | 2 | 3;
8+
isDisabled: boolean;
9+
onClick?: () => void;
10+
}
11+
12+
const AlarmsType = [
13+
{ img: avatar1, title: '아침형 치삐', time: '오전 9시' },
14+
{ img: avatar2, title: '저녁형 치삐', time: '오후 8시' },
15+
{ img: avatar3, title: '사용자 설정' },
16+
];
17+
18+
const boxStyle = cva(
19+
'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition',
20+
{
21+
variants: {
22+
disabled: {
23+
true: 'border-main400 bg-main100 border',
24+
false: 'bg-white border border-transparent hover:border-main300',
25+
},
26+
},
27+
defaultVariants: { disabled: false },
28+
}
29+
);
30+
31+
const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
32+
return (
33+
<div className={boxStyle({ disabled: isDisabled })} onClick={onClick}>
34+
<img src={AlarmsType[select - 1].img} alt="chippi" />
35+
<p
36+
className={`sub3-sb ${
37+
isDisabled ? 'text-main500' : 'text-font-black-1'
38+
}`}
39+
>
40+
{AlarmsType[select - 1].title}
41+
</p>
42+
{select <= 2 && (
43+
<p className="caption2-m text-font-gray-3">
44+
{AlarmsType[select - 1].time}
45+
</p>
46+
)}
47+
</div>
48+
);
49+
};
50+
51+
export default AlarmBox;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState } from 'react';
2+
import dotori from '../../../../assets/onBoarding/icons/dotori.svg';
3+
import AlarmBox from './AlarmBox';
4+
5+
const AlarmStep = () => {
6+
const [selected, setSelected] = useState<1 | 2 | 3>(1); // 기본값은 1번 선택
7+
8+
return (
9+
<div className="flex flex-col items-center justify-between">
10+
<img src={dotori} className="mb-[1.2rem]" alt="dotori" />
11+
<p className="head2 text-font-black-1">
12+
도토리 찾으러 갈 시간을 정해볼까요?
13+
</p>
14+
<p className="body2-m text-font-gray-3 mb-[4.3rem] mt-[0.8rem] text-center">
15+
매일 지정한 시간에 저장한 북마크를 리마인드해드려요
16+
</p>
17+
18+
<div className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]">
19+
{[1, 2, 3].map((n) => (
20+
<AlarmBox
21+
key={n}
22+
select={n as 1 | 2 | 3}
23+
isDisabled={selected === n}
24+
onClick={() => setSelected(n as 1 | 2 | 3)}
25+
/>
26+
))}
27+
</div>
28+
</div>
29+
);
30+
};
31+
32+
export default AlarmStep;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Progress, Button } from '@pinback/design-system/ui';
2+
import { useState } from 'react';
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import StoryStep from './StoryStep';
5+
import AlarmStep from './AlarmStep';
6+
7+
const stepProgress = [{ progress: 30 }, { progress: 60 }, { progress: 100 }];
8+
9+
const variants = {
10+
enter: (direction: number) => ({
11+
x: direction > 0 ? 200 : -200,
12+
opacity: 0,
13+
}),
14+
center: { x: 0, opacity: 1 },
15+
exit: (direction: number) => ({
16+
x: direction > 0 ? -200 : 200,
17+
opacity: 0,
18+
}),
19+
};
20+
21+
const MainCard = () => {
22+
const [step, setStep] = useState(0);
23+
const [direction, setDirection] = useState(0);
24+
25+
const nextStep = () => {
26+
if (step < 3) {
27+
// 0,1,2 → story, 3 → alarm
28+
setDirection(1);
29+
setStep((prev) => prev + 1);
30+
}
31+
};
32+
33+
const prevStep = () => {
34+
if (step > 0) {
35+
setDirection(-1);
36+
setStep((prev) => prev - 1);
37+
}
38+
};
39+
40+
return (
41+
<div className="bg-white-bg flex h-[54.8rem] w-[63.2rem] flex-col items-center justify-between overflow-hidden rounded-[2.4rem] pt-[3.2rem]">
42+
{/* ProgressBar는 story 단계에서만 보여줌 */}
43+
{step < 3 && (
44+
<Progress
45+
value={stepProgress[step].progress}
46+
variant="profile"
47+
className="w-[30%]"
48+
/>
49+
)}
50+
51+
<div className="relative flex h-full w-full items-center justify-center">
52+
<AnimatePresence custom={direction} mode="wait">
53+
<motion.div
54+
key={step}
55+
custom={direction}
56+
variants={variants}
57+
initial="enter"
58+
animate="center"
59+
exit="exit"
60+
transition={{ duration: 0.4 }}
61+
className="absolute flex flex-col items-center"
62+
>
63+
{step < 3 ? <StoryStep step={step as 0 | 1 | 2} /> : <AlarmStep />}
64+
</motion.div>
65+
</AnimatePresence>
66+
</div>
67+
68+
<div className="mb-[4.8rem] mt-[1.2rem] flex w-full justify-between px-[3.2rem]">
69+
<Button
70+
variant="primary"
71+
size="medium"
72+
isDisabled={step === 0}
73+
className="w-[4.8rem]"
74+
onClick={prevStep}
75+
>
76+
이전
77+
</Button>
78+
<Button
79+
variant="primary"
80+
size="medium"
81+
isDisabled={step === 3} // 마지막은 alarmStep
82+
className="w-[4.8rem]"
83+
onClick={nextStep}
84+
>
85+
다음
86+
</Button>
87+
</div>
88+
</div>
89+
);
90+
};
91+
92+
export default MainCard;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import story1 from '../../../../assets/onBoarding/story/story1.svg';
2+
import story2 from '../../../../assets/onBoarding/story/story2.svg';
3+
import story3 from '../../../../assets/onBoarding/story/story3.svg';
4+
interface StoryStepProps {
5+
step: 0 | 1 | 2;
6+
}
7+
const steps = [
8+
{
9+
img: story1,
10+
text: (
11+
<>
12+
깊고 신비한 숲에는 지식 나무가 있어요. <br />
13+
지식 나무는 사람들의 잊힌 기록을 도토리 씨앗으로 바꾼답니다.
14+
</>
15+
),
16+
progress: 30,
17+
},
18+
{
19+
img: story2,
20+
text: (
21+
<>
22+
당신이 정보를 읽고 활용하는 것을 양분삼아, <br />
23+
지식 나무에는 맛있는 도토리 열매가 열려요.
24+
</>
25+
),
26+
progress: 60,
27+
},
28+
{
29+
img: story3,
30+
text: (
31+
<>
32+
다람쥐 치삐는 정보를 활용하지 못해 아직 도토리 만개 숲에 도착하지 못하고
33+
있어요.
34+
<br />
35+
도토리를 모아 치삐가 숲에 닿을 수 있도록 도와주세요!
36+
</>
37+
),
38+
progress: 100,
39+
},
40+
];
41+
42+
const StoryStep = ({ step }: StoryStepProps) => {
43+
return (
44+
<div className="flex flex-col items-center">
45+
<img
46+
src={steps[step].img}
47+
className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]"
48+
alt="onboarding"
49+
/>
50+
<p className="sub4-sb text-center text-black">{steps[step].text}</p>
51+
</div>
52+
);
53+
};
54+
55+
export default StoryStep;

pnpm-lock.yaml

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)