Skip to content

Commit 5ce24db

Browse files
authored
Merge pull request #19 from codeit-2team/feat/13
Feat/13 popup 컴포넌트 구현
2 parents 92a9411 + 4014ae6 commit 5ce24db

File tree

4 files changed

+202
-0
lines changed

4 files changed

+202
-0
lines changed

src/assets/icon/check.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
3+
const CheckIcon = ({ size = 24, ...props }) => (
4+
<svg
5+
xmlns='http://www.w3.org/2000/svg'
6+
width={size}
7+
height={size}
8+
fill='none'
9+
viewBox='0 0 24 24'
10+
>
11+
<circle cx='12' cy='12' r='12' fill='#121'></circle>
12+
<path
13+
stroke='#fff'
14+
strokeLinecap='round'
15+
strokeLinejoin='round'
16+
strokeWidth='1.5'
17+
d='m7.607 12.35 3.08 3.15 5.563-7.143'
18+
/>
19+
</svg>
20+
);
21+
22+
export default CheckIcon;

src/components/popup/Popup.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use client';
2+
3+
import cn from '@/lib/utils';
4+
import { PopupProps } from './types';
5+
import Button from '../button/Button';
6+
import CheckIcon from '@/assets/icon/check';
7+
import { useRef } from 'react';
8+
import useOutsideClick from '@/hooks/useOutsideClick';
9+
10+
/**
11+
* Popup 컴포넌트는 `alert` 또는 `confirm` 타입의 모달을 렌더링합니다.
12+
* - `alert` 타입은 확인 버튼 하나만 있는 단순 알림 용도
13+
* - `confirm` 타입은 취소하기 / 아니오 버튼이 있는 확인 용도
14+
*
15+
* 외부 영역 클릭 또는 ESC 키 입력 시 onClose 콜백이 호출됩니다.
16+
*
17+
* @component
18+
* @param {boolean} isOpen - 팝업의 열림 여부
19+
* @param {'alert' | 'confirm'} type - 팝업의 유형
20+
* @param {ReactNode} children - 팝업 내부의 메시지 또는 JSX 콘텐츠
21+
* @param {() => void} onClose - 팝업을 닫을 때 호출되는 함수
22+
* @param {() => void} [onConfirm] - confirm 타입일 때 확인 버튼 클릭 시 호출되는 함수
23+
*
24+
* @example
25+
* ```tsx
26+
* <Popup
27+
* isOpen={true}
28+
* type="alert"
29+
* onClose={() => setOpen(false)}
30+
* >
31+
* 완료되었습니다!
32+
* </Popup>
33+
* ```
34+
*/
35+
36+
export default function Popup({
37+
isOpen,
38+
type,
39+
children,
40+
onClose,
41+
onConfirm,
42+
}: PopupProps) {
43+
if (!isOpen) return null;
44+
const popupRef = useRef<HTMLDivElement>(null);
45+
useOutsideClick(popupRef, onClose);
46+
47+
return (
48+
<div
49+
className={cn(
50+
'fixed inset-0 z-50 flex items-center justify-center bg-[#000000]/70',
51+
)}
52+
>
53+
{type === 'alert' ? (
54+
<div
55+
ref={popupRef}
56+
className={cn(
57+
'flex h-220 w-327 flex-col items-center gap-43 rounded-lg bg-white pt-81 md:h-250 md:w-540 md:gap-40 md:px-28 md:pt-108',
58+
)}
59+
>
60+
<div className={cn('md:text-2lg text-lg font-medium text-[#333236]')}>
61+
{children}
62+
</div>
63+
<Button
64+
variant='primary'
65+
className={cn(
66+
'text-md h-42 w-138 rounded-lg font-medium md:h-48 md:w-120 md:self-end md:text-lg',
67+
)}
68+
onClick={onClose}
69+
>
70+
확인
71+
</Button>
72+
</div>
73+
) : (
74+
<div
75+
ref={popupRef}
76+
className={cn(
77+
'flex h-184 w-298 flex-col items-center gap-32 rounded-xl bg-white py-24',
78+
)}
79+
>
80+
<div className={cn('flex flex-col items-center gap-16')}>
81+
<CheckIcon />
82+
<div className={cn('font-regular text-lg text-black')}>
83+
{children}
84+
</div>
85+
</div>
86+
<div className={cn('text-md flex h-38 gap-8 font-bold')}>
87+
<Button
88+
variant='secondary'
89+
className={cn('w-80 rounded-md')}
90+
onClick={onClose}
91+
>
92+
아니오
93+
</Button>
94+
<Button
95+
variant='primary'
96+
className={cn('w-80 rounded-md')}
97+
onClick={onConfirm}
98+
>
99+
취소하기
100+
</Button>
101+
</div>
102+
</div>
103+
)}
104+
</div>
105+
);
106+
}

src/components/popup/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ReactNode } from 'react';
2+
3+
type PopupType = 'alert' | 'confirm';
4+
5+
/**
6+
* 팝업 컴포넌트에 전달되는 props입니다.
7+
*
8+
* @property {boolean} isOpen - 팝업의 열림 여부를 나타냅니다. true일 경우 팝업이 화면에 표시됩니다.
9+
* @property {'alert' | 'confirm'} type - 팝업의 유형을 지정합니다.
10+
* - 'alert': 확인 버튼만 있는 단순 알림 팝업
11+
* - 'confirm': 예/아니오 또는 확인/취소 버튼이 있는 확인용 팝업
12+
* @property {ReactNode} children - 팝업 내부에 표시할 내용입니다. 텍스트 또는 JSX 요소를 전달할 수 있습니다.
13+
* @property {() => void} onClose - 팝업을 닫을 때 호출되는 콜백 함수입니다.
14+
* - 보통 확인 버튼 또는 '아니오' 버튼에 사용됩니다.
15+
* @property {() => void} [onConfirm] - 확인 또는 '예' 버튼 클릭 시 호출되는 선택적 콜백 함수입니다.
16+
* - type이 'confirm'일 때 사용되며, 'alert'일 경우 생략 가능합니다.
17+
*/
18+
19+
export interface PopupProps {
20+
isOpen: boolean;
21+
type: PopupType;
22+
children: ReactNode;
23+
onClose: () => void;
24+
onConfirm?: () => void;
25+
}

src/hooks/useOutsideClick.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useEffect } from 'react';
2+
3+
/**
4+
* 모달 외부 클릭 또는 ESC 키 입력 시 모달을 닫는 커스텀 훅입니다.
5+
*
6+
* @param {React.RefObject<HTMLElement>} ref - 모달 DOM 요소를 참조하는 ref 객체
7+
* @param {() => void} onClose - 모달을 닫는 콜백 함수
8+
*
9+
* @example
10+
* const modalRef = useRef(null);
11+
* useModalClose(modalRef, () => setIsOpen(false));
12+
*
13+
* @description
14+
* 이 훅은 다음 두 가지 동작을 처리합니다:
15+
* 1. 모달 외부를 클릭하면 onClose가 호출됨
16+
* 2. ESC 키를 누르면 onClose가 호출됨
17+
*
18+
* 이 훅은 모달, 드롭다운, 팝오버 등 닫기 트리거가 필요한 컴포넌트에 사용할 수 있습니다.
19+
*/
20+
21+
export default function useOutsideClick(
22+
ref: React.RefObject<HTMLElement | null>,
23+
onClose: () => void,
24+
) {
25+
useEffect(() => {
26+
// 바깥 클릭 처리
27+
const handleClickOutside = (e: MouseEvent) => {
28+
const target = e?.target as Node;
29+
if (ref.current && !ref.current.contains(target)) {
30+
onClose();
31+
}
32+
};
33+
34+
// ESC 키 처리
35+
const handleKeyDown = (e: KeyboardEvent) => {
36+
if (e.key === 'Escape') {
37+
onClose();
38+
}
39+
};
40+
41+
document.addEventListener('click', handleClickOutside);
42+
document.addEventListener('keydown', handleKeyDown);
43+
44+
return () => {
45+
document.removeEventListener('click', handleClickOutside);
46+
document.removeEventListener('keydown', handleKeyDown);
47+
};
48+
}, [ref, onClose]);
49+
}

0 commit comments

Comments
 (0)