Skip to content

Commit 10a6931

Browse files
committed
feat(#87): AlertModal 컴포넌트 및 스토리북 생성
1 parent b37827c commit 10a6931

File tree

4 files changed

+294
-0
lines changed

4 files changed

+294
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { style } from "@vanilla-extract/css";
2+
3+
import { radius, semantic, typography } from "@/styles";
4+
import { zIndex } from "@/styles/zIndex.css";
5+
6+
export const overlay = style({
7+
position: "fixed",
8+
inset: 0,
9+
backgroundColor: semantic.background.dim,
10+
zIndex: zIndex.overlay,
11+
});
12+
13+
export const content = style({
14+
maxHeight: "80vh",
15+
display: "flex",
16+
flexDirection: "column",
17+
position: "fixed",
18+
top: "50%",
19+
left: "50%",
20+
transform: "translate(-50%, -50%)",
21+
width: "min(30rem, 90vw)",
22+
background: semantic.background.white,
23+
borderRadius: radius[120],
24+
zIndex: zIndex.modal,
25+
});
26+
27+
export const textWrapper = style({
28+
overflowY: "auto",
29+
padding: "4.8rem 1.6rem",
30+
display: "flex",
31+
flexDirection: "column",
32+
alignItems: "center",
33+
});
34+
35+
export const title = style({
36+
...typography.title3Sb,
37+
color: semantic.text.normal,
38+
});
39+
40+
export const description = style({
41+
...typography.body2Rg,
42+
color: semantic.text.alternative,
43+
marginTop: "0.8rem",
44+
});
45+
46+
export const buttonGroup = style({
47+
display: "flex",
48+
width: "100%",
49+
});
50+
51+
export const cancelButton = style({
52+
flex: 1,
53+
minWidth: 0,
54+
overflow: "hidden",
55+
});
56+
57+
export const confirmButton = style({
58+
flex: 1,
59+
minWidth: 0,
60+
overflow: "hidden",
61+
});
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { useState } from "react";
3+
4+
import { Button } from "../Button";
5+
import { AlertModal, type AlertModalProps } from "./AlertModal";
6+
7+
const meta: Meta<typeof AlertModal> = {
8+
title: "Components/AlertModal",
9+
component: AlertModal,
10+
tags: ["autodocs"],
11+
argTypes: {
12+
open: { control: false },
13+
onOpenChange: { control: false },
14+
title: { control: "text", description: "모달의 타이틀" },
15+
description: { control: "text", description: "모달의 서브 설명" },
16+
confirmLabel: { control: "text", description: "확인 버튼 라벨" },
17+
cancelLabel: { control: "text", description: "취소 버튼 라벨" },
18+
onConfirm: { action: "onConfirm", description: "확인 버튼 클릭 시 콜백" },
19+
onCancel: { action: "onCancel", description: "취소 버튼 클릭 시 콜백" },
20+
},
21+
parameters: {
22+
layout: "centered",
23+
docs: {
24+
description: {
25+
component:
26+
"AlertModal은 사용자에게 확인/취소 액션을 요청할 때 사용하는 모달입니다. title, description, confirmLabel, cancelLabel, onConfirm, onCancel 등을 지원합니다.",
27+
},
28+
},
29+
},
30+
};
31+
32+
export default meta;
33+
type Story = StoryObj<typeof AlertModal>;
34+
35+
const AlertModalTemplate = (args: Partial<AlertModalProps>) => {
36+
const [open, setOpen] = useState(false);
37+
38+
return (
39+
<>
40+
<Button onClick={() => setOpen(true)}>모달 열기</Button>
41+
<AlertModal {...args} open={open} onOpenChange={setOpen} />
42+
</>
43+
);
44+
};
45+
46+
export const Default: Story = {
47+
render: args => <AlertModalTemplate {...args} />,
48+
args: {
49+
title: "정말 삭제하시겠어요?",
50+
description: "삭제 후에는 복구할 수 없습니다.",
51+
confirmLabel: "삭제",
52+
cancelLabel: "취소",
53+
},
54+
parameters: {
55+
docs: {
56+
description: {
57+
story:
58+
"기본 AlertModal. 타이틀과 설명, 확인/취소 버튼을 모두 포함합니다.",
59+
},
60+
},
61+
},
62+
};
63+
64+
export const NoDescription: Story = {
65+
render: args => <AlertModalTemplate {...args} />,
66+
args: {
67+
title: "약관을 동의하시겠습니까?",
68+
confirmLabel: "동의",
69+
cancelLabel: "취소",
70+
},
71+
parameters: {
72+
docs: {
73+
description: {
74+
story: "description 없이 타이틀과 버튼만 표시되는 AlertModal입니다.",
75+
},
76+
},
77+
},
78+
};
79+
80+
export const AsyncConfirm: Story = {
81+
render: args => {
82+
const AsyncTemplate = () => {
83+
const [open, setOpen] = useState(false);
84+
const [loading, setLoading] = useState(false);
85+
86+
const handleConfirm = async () => {
87+
setLoading(true);
88+
await new Promise(res => setTimeout(res, 1500));
89+
setLoading(false);
90+
setOpen(false);
91+
alert("비동기 작업 완료!");
92+
};
93+
94+
return (
95+
<>
96+
<Button onClick={() => setOpen(true)}>모달 열기</Button>
97+
<AlertModal
98+
{...args}
99+
open={open}
100+
onOpenChange={setOpen}
101+
onConfirm={handleConfirm}
102+
confirmLabel={loading ? "처리 중..." : args.confirmLabel}
103+
/>
104+
</>
105+
);
106+
};
107+
108+
return <AsyncTemplate />;
109+
},
110+
args: {
111+
title: "비동기 확인 예시",
112+
description: "확인 버튼 클릭 시 비동기 작업을 실행합니다.",
113+
confirmLabel: "확인",
114+
cancelLabel: "취소",
115+
},
116+
parameters: {
117+
docs: {
118+
description: {
119+
story:
120+
"확인 버튼 클릭 시 비동기 작업을 수행하는 AlertModal 예시입니다. 버튼 라벨이 로딩 상태로 변경됩니다.",
121+
},
122+
},
123+
},
124+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { DialogProps } from "@radix-ui/react-dialog";
2+
import * as Dialog from "@radix-ui/react-dialog";
3+
4+
import { Button } from "../Button";
5+
import * as styles from "./AlertModal.css";
6+
7+
export type AlertModalProps = {
8+
/** 모달 제목 */
9+
title?: string;
10+
11+
/** 모달 설명 */
12+
description?: string;
13+
14+
/** 확인 버튼 클릭 시 실행되는 콜백 */
15+
onConfirm?: () => void | Promise<void>;
16+
17+
/** 취소 버튼 클릭 시 실행되는 콜백 */
18+
onCancel?: () => void;
19+
20+
/** 확인 버튼 텍스트 */
21+
confirmLabel?: string;
22+
23+
/** 취소 버튼 텍스트 */
24+
cancelLabel?: string;
25+
} & DialogProps;
26+
27+
/**
28+
* AlertModal 컴포넌트
29+
*
30+
* @description
31+
* Radix UI의 Dialog를 래핑한 모달 컴포넌트입니다.
32+
* 제목과 설명을 표시하고, 확인/취소 버튼을 제공합니다.
33+
*
34+
* @example
35+
* ```tsx
36+
* const [open, setOpen] = useState(false);
37+
*
38+
* <AlertModal
39+
* open={open}
40+
* onOpenChange={setOpen}
41+
* title="로그아웃 하시겠어요?"
42+
* description="로그아웃하면 다시 로그인해야 합니다."
43+
* cancelLabel="취소"
44+
* confirmLabel="로그아웃"
45+
* onCancel={() => console.log("취소")}
46+
* onConfirm={() => console.log("확인")}
47+
* />
48+
* ```
49+
*/
50+
export const AlertModal = ({
51+
open,
52+
onOpenChange,
53+
title,
54+
description,
55+
onConfirm,
56+
onCancel,
57+
confirmLabel = "확인",
58+
cancelLabel = "취소",
59+
}: AlertModalProps) => {
60+
return (
61+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
62+
<Dialog.Portal>
63+
<Dialog.Overlay className={styles.overlay} />
64+
<Dialog.Content className={styles.content}>
65+
{(title || description) && (
66+
<div className={styles.textWrapper}>
67+
{title && (
68+
<Dialog.Title className={styles.title}>{title}</Dialog.Title>
69+
)}
70+
{description && (
71+
<Dialog.Description className={styles.description}>
72+
{description}
73+
</Dialog.Description>
74+
)}
75+
</div>
76+
)}
77+
78+
<div className={styles.buttonGroup}>
79+
<Dialog.Close asChild>
80+
<Button
81+
className={styles.cancelButton}
82+
variant='assistive'
83+
size='large'
84+
onClick={onCancel}
85+
style={{
86+
borderRadius: "0 0 0 1.2rem",
87+
}}
88+
>
89+
{cancelLabel}
90+
</Button>
91+
</Dialog.Close>
92+
<Button
93+
className={styles.confirmButton}
94+
variant='primary'
95+
size='large'
96+
onClick={onConfirm}
97+
style={{
98+
borderRadius: "0 0 1.2rem 0",
99+
}}
100+
>
101+
{confirmLabel}
102+
</Button>
103+
</div>
104+
</Dialog.Content>
105+
</Dialog.Portal>
106+
</Dialog.Root>
107+
);
108+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { AlertModal } from "./AlertModal";

0 commit comments

Comments
 (0)