Skip to content

Commit 7413274

Browse files
authored
Merge pull request #80 from DevKor-github/feat/#77/drawer
[#77] ๋ฐ”ํ…€์‹œํŠธ ๊ตฌํ˜„๊ตฌํ˜„ !
2 parents 3615198 + 4a4c9b9 commit 7413274

9 files changed

Lines changed: 1307 additions & 1032 deletions

File tree

โ€Žpackage.jsonโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"axios": "^1.9.0",
2020
"date-fns": "^4.1.0",
2121
"mingcute_icon": "^2.9.6",
22+
"motion": "^12.23.3",
2223
"react": "^19.1.0",
2324
"react-dom": "^19.1.0",
2425
"react-router": "^7.6.3",

โ€Žpanda.config.tsโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export default defineConfig({
4444
},
4545
zIndex: {
4646
navigator: { value: 500 },
47+
drawerBackLayer: { value: 600 },
48+
drawerBody: { value: 700 },
4749
},
4850
sizes: PANDA_CSS_CONSTANTS,
4951
spacing: PANDA_CSS_CONSTANTS,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { motion } from 'motion/react';
2+
3+
import * as s from './style.css';
4+
5+
interface BackLayerProps {
6+
close: () => void;
7+
hasBackdrop: boolean;
8+
}
9+
const BackLayer = ({ close, hasBackdrop }: BackLayerProps) => {
10+
return (
11+
<motion.div
12+
className={s.BackLayerStyle}
13+
variants={{
14+
opened: { opacity: hasBackdrop ? 1 : 0 },
15+
closed: { opacity: 0 },
16+
}}
17+
initial={'closed'}
18+
animate={'opened'}
19+
exit={'closed'}
20+
onClick={close}
21+
/>
22+
);
23+
};
24+
25+
export default BackLayer;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useCallback, useEffect, useRef, useState, type PropsWithChildren } from 'react';
2+
import { cx } from '@styled-system/css';
3+
import { motion, type PanInfo } from 'framer-motion';
4+
5+
import * as s from './style.css';
6+
7+
interface DrawerBodyProps extends PropsWithChildren {
8+
title: string;
9+
description?: string;
10+
close: () => void;
11+
}
12+
13+
const DrawerBody = ({ children, title, description, close }: DrawerBodyProps) => {
14+
const contentRef = useRef<HTMLDivElement>(null);
15+
const [height, setHeight] = useState(0);
16+
17+
useEffect(() => {
18+
// UI ํŽ˜์ธํŒ… ์ดํ›„ ๊ณ„์‚ฐ
19+
if (contentRef.current) {
20+
setHeight(Math.min(contentRef.current.clientHeight, window.innerHeight));
21+
}
22+
}, [children]);
23+
24+
const onDragEnd = useCallback(
25+
(_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
26+
const offsetThreshold = 150;
27+
const deltaThreshold = 5;
28+
29+
const isOverOffsetThreshold = Math.abs(info.offset.y) > offsetThreshold;
30+
const isOverDeltaThreshold = Math.abs(info.delta.y) > deltaThreshold;
31+
32+
const isOverThreshold = isOverOffsetThreshold || isOverDeltaThreshold;
33+
34+
if (!isOverThreshold) return;
35+
36+
if (info.offset.y >= 0) close();
37+
},
38+
[close],
39+
);
40+
41+
return (
42+
<motion.div
43+
key="drawer_body"
44+
className={s.Container}
45+
drag={'y'}
46+
dragConstraints={{ top: 0, bottom: 0 }}
47+
transition={{ duration: 0.2 }}
48+
dragElastic={0.4}
49+
variants={{
50+
opened: { top: `calc(100dvh - ${height}px)` },
51+
closed: { top: '100dvh' },
52+
}}
53+
initial={'closed'}
54+
animate={'opened'}
55+
exit={'closed'}
56+
onDragEnd={onDragEnd}
57+
>
58+
<header className={s.Header}>
59+
<div className={s.HeaderTitle}>
60+
<span className={s.Title}>{title}</span>
61+
{description && <span className={s.Description}>{description}</span>}
62+
</div>
63+
<button className={cx('mgc_close_line', s.CloseButton)} onClick={close} />
64+
</header>
65+
<div ref={contentRef} className={s.Content}>
66+
{children}
67+
</div>
68+
</motion.div>
69+
);
70+
};
71+
72+
export default DrawerBody;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { AnimatePresence } from 'motion/react';
2+
import { type ReactNode } from 'react';
3+
4+
import BackLayer from '@/common/components/Drawer/BackLayer';
5+
import DrawerBody from '@/common/components/Drawer/DrawerBody';
6+
import type { DrawerState } from '@/common/hooks/useDrawer';
7+
8+
interface DrawerProps {
9+
title: string;
10+
description?: string;
11+
drawerState: DrawerState;
12+
children: ReactNode;
13+
}
14+
const Drawer = ({ drawerState, title, description, children }: DrawerProps) => {
15+
return (
16+
<AnimatePresence>
17+
{drawerState.isOpen && (
18+
<>
19+
<BackLayer close={drawerState.close} hasBackdrop={drawerState.hasBackdrop} />
20+
<DrawerBody close={drawerState.close} title={title} description={description}>
21+
{children}
22+
</DrawerBody>
23+
</>
24+
)}
25+
</AnimatePresence>
26+
);
27+
};
28+
29+
export default Drawer;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { css } from '@styled-system/css';
2+
3+
export const BackLayerStyle = css({
4+
pos: 'fixed',
5+
top: 0,
6+
left: 0,
7+
right: 0,
8+
bottom: 0,
9+
bg: '#0000007D',
10+
zIndex: 'drawerBackLayer',
11+
});
12+
13+
export const Container = css({
14+
pos: 'fixed',
15+
top: '100dvh',
16+
left: 0,
17+
w: 'full',
18+
h: '100dvh',
19+
bgColor: 'systemGray6',
20+
roundedTop: '10px',
21+
willChange: 'transform',
22+
zIndex: 'drawerBody',
23+
display: 'flex',
24+
flexDir: 'column',
25+
borderTop: '1px solid {colors.systemGray4}',
26+
boxShadow: '0px 0px 30px 0px rgba(0, 0, 0, 0.25)',
27+
pt: '1.5rem',
28+
pb: '2.625rem',
29+
});
30+
31+
export const Header = css({
32+
display: 'flex',
33+
alignItems: 'center',
34+
justifyContent: 'space-between',
35+
px: '1rem',
36+
});
37+
38+
export const HeaderTitle = css({
39+
display: 'flex',
40+
alignItems: 'center',
41+
gap: '0.625rem',
42+
lineHeight: 'normal',
43+
});
44+
45+
export const Title = css({
46+
color: '100',
47+
fontSize: '1rem',
48+
fontWeight: 500,
49+
letterSpacing: '-0.04rem',
50+
});
51+
52+
export const Description = css({
53+
color: '54',
54+
fontSize: '0.875rem',
55+
fontWeight: 400,
56+
letterSpacing: '-0.035rem',
57+
});
58+
59+
export const CloseButton = css({
60+
fontSize: '1rem',
61+
color: '80',
62+
cursor: 'pointer',
63+
});
64+
65+
export const Content = css({ w: 'full', h: 'fit-content' });
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react';
2+
3+
interface DrawerOption {
4+
hasBackdrop?: boolean;
5+
}
6+
7+
export interface DrawerState {
8+
isOpen: boolean;
9+
close: () => void;
10+
hasBackdrop: boolean;
11+
}
12+
13+
/**
14+
* ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด useDrawer์—์„œ drawerState๋ฅผ ๋ฐ›์•„์„œ
15+
* Drawer ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•ด์ฃผ์‹œ๋ฉด ๋˜๊ณ 
16+
* ๊ทธ๋‹ด์— open(), close()๋กœ ์ œ์–ดํ•˜๋ฉด ๋จ
17+
*/
18+
const useDrawer = (option: DrawerOption = {}) => {
19+
const [isOpen, setIsOpen] = useState(false);
20+
21+
const open = () => setIsOpen(true);
22+
const close = () => setIsOpen(false);
23+
24+
const drawerState: DrawerState = {
25+
isOpen,
26+
close,
27+
hasBackdrop: option.hasBackdrop ?? true,
28+
};
29+
30+
return {
31+
open,
32+
close,
33+
drawerState,
34+
};
35+
};
36+
37+
export default useDrawer;

โ€Žsrc/features/home/components/SearchControls/index.tsxโ€Ž

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import SelectButton from '@/common/components/SelectButton';
22
import * as s from './style.css';
3+
import useDrawer from '@/common/hooks/useDrawer';
4+
import { css } from '@styled-system/css';
5+
import Drawer from '@/common/components/Drawer';
36

47
interface Props {
58
itemCounts: number;
69
}
710
const SearchControls = ({ itemCounts }: Props) => {
11+
const { open, drawerState } = useDrawer();
12+
813
return (
914
<div className={s.Container}>
1015
<div className={s.ResultBar}>
@@ -13,12 +18,22 @@ const SearchControls = ({ itemCounts }: Props) => {
1318
</div>
1419
<div className={s.SelectButtonContainer}>
1520
{/* TODO: ๋””์ž์ธ ์ ์šฉ, ํ•„ํ„ฐ ๋กœ์ง ์ถ”๊ฐ€ */}
16-
<SelectButton active={false}>์ข…๋ชฉ</SelectButton>
21+
<SelectButton
22+
active={false}
23+
onClick={() => {
24+
open();
25+
}}
26+
>
27+
์ข…๋ชฉ
28+
</SelectButton>
1729
<SelectButton active={false}>์‚ฌ์ด์ฆˆ</SelectButton>
1830
<SelectButton active={false}>๋Œ€์—ฌ/ํŒ๋งค</SelectButton>
1931
<SelectButton active={false}>๊ฐ€๊ฒฉ์ˆœ</SelectButton>
2032
<SelectButton active={false}>์ƒ‰์ƒ</SelectButton>
2133
</div>
34+
<Drawer title="ํ•„ํ„ฐ" description="ํ•„ํ„ฐ๋ฅผ ์„ ํƒํ•ด์ฃผ์‚ผ" drawerState={drawerState}>
35+
<div className={css({ height: '10rem', px: '1rem', pt: '1rem' })}>์•ˆ๋…•ํ•˜์„ธ์š” ํ˜ธํ˜ธ</div>
36+
</Drawer>
2237
</div>
2338
);
2439
};

0 commit comments

Comments
ย (0)