diff --git a/package.json b/package.json index 19875356..1f0c6e94 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ }, "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-dialog": "^1.1.14", "@tanstack/react-query": "^5.77.0", "@tanstack/react-query-devtools": "^5.77.0", "@use-funnel/browser": "^0.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5d56a5a..a67176d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@hookform/resolvers': specifier: ^5.1.1 version: 5.1.1(react-hook-form@7.59.0(react@19.1.0)) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: ^5.77.0 version: 5.77.0(react@19.1.0) @@ -1567,6 +1573,19 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-alert-dialog@1.1.14': + resolution: {integrity: sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -7579,6 +7598,20 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.5)(react@19.1.0)': dependencies: react: 19.1.0 diff --git a/src/components/ui/AlertModal/AlertModal.css.ts b/src/components/ui/AlertModal/AlertModal.css.ts new file mode 100644 index 00000000..eaf761d8 --- /dev/null +++ b/src/components/ui/AlertModal/AlertModal.css.ts @@ -0,0 +1,59 @@ +import { style } from "@vanilla-extract/css"; + +import { radius, semantic, typography } from "@/styles"; +import { zIndex } from "@/styles/zIndex.css"; + +export const overlay = style({ + position: "fixed", + inset: 0, + backgroundColor: semantic.background.dim, + zIndex: zIndex.overlay, +}); + +export const content = style({ + maxHeight: "80vh", + display: "flex", + flexDirection: "column", + position: "fixed", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "min(30rem, 90vw)", + background: semantic.background.white, + borderRadius: radius[120], + zIndex: zIndex.modal, +}); + +export const innerContent = style({ + overflowY: "auto", + padding: "4.8rem 1.6rem", + display: "flex", + flexDirection: "column", + alignItems: "center", +}); + +export const title = style({ + ...typography.title3Sb, + color: semantic.text.normal, +}); + +export const description = style({ + ...typography.body2Rg, + color: semantic.text.alternative, + marginTop: "0.8rem", + width: "100%", + textAlign: "center", +}); + +export const footer = style({ + width: "100%", + display: "flex", +}); + +export const cancelButton = style({ + flex: 1, +}); + +export const confirmButton = style({ + flex: 1, +}); diff --git a/src/components/ui/AlertModal/AlertModal.stories.tsx b/src/components/ui/AlertModal/AlertModal.stories.tsx new file mode 100644 index 00000000..c899bc77 --- /dev/null +++ b/src/components/ui/AlertModal/AlertModal.stories.tsx @@ -0,0 +1,119 @@ +import * as AlertDialog from "@radix-ui/react-alert-dialog"; +import type { Meta, StoryObj } from "@storybook/nextjs"; + +import { Button } from "../Button"; +import { AlertModal } from "./AlertModal"; +import * as styles from "./AlertModal.css"; + +const meta: Meta = { + title: "Components/AlertModal", + component: AlertModal, + tags: ["autodocs"], + argTypes: { + title: { + control: "text", + description: "모달의 제목", + }, + trigger: { + control: false, + description: "모달을 열기 위한 트리거(버튼 등, ReactNode)", + }, + content: { + control: false, + description: "본문(설명 등, ReactNode)", + }, + footer: { + control: false, + description: + "하단 푸터(버튼 영역 등, ReactNode). Radix의 또는 를 조합하여 사용.", + }, + }, + parameters: { + layout: "centered", + docs: { + description: { + component: + "AlertModal은 사용자에게 확인/취소 액션을 요청할 때 사용하는 모달입니다. title, trigger, content, footer props를 지원합니다.", + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: args => , + args: { + title: "정말 삭제하시겠어요?", + trigger: , + content:
삭제하면 복구할 수 없습니다.
, + footer: ( + <> + + + + + + + + ), + }, + parameters: { + docs: { + description: { + story: + "기본 AlertModal. 타이틀과 설명, 확인/취소 버튼을 모두 포함합니다.", + }, + }, + }, +}; + +export const NoDescription: Story = { + render: args => , + args: { + title: "약관을 동의하시겠습니까?", + trigger: , + footer: ( + <> + + + + + + + + ), + }, + parameters: { + docs: { + description: { + story: "description 없이 타이틀과 버튼만 표시되는 AlertModal입니다.", + }, + }, + }, +}; diff --git a/src/components/ui/AlertModal/AlertModal.tsx b/src/components/ui/AlertModal/AlertModal.tsx new file mode 100644 index 00000000..6f823750 --- /dev/null +++ b/src/components/ui/AlertModal/AlertModal.tsx @@ -0,0 +1,75 @@ +import * as AlertDialog from "@radix-ui/react-alert-dialog"; +import { type ReactNode } from "react"; + +import * as styles from "./AlertModal.css"; + +export type AlertModalProps = { + /** 모달의 제목 */ + title?: string; + + /** 모달을 열기 위한 트리거(버튼 등, 선택) */ + trigger?: ReactNode; + + /** 본문(설명 등, ReactNode로 자유롭게 구성) */ + content?: ReactNode; + + /** + * 하단 푸터(버튼 영역 등, ReactNode로 자유롭게 구성) + * Radix의 또는 를 조합하여 사용 가능 + * 예시: + * <> + * + * + * + * + * + * + * + */ + footer?: ReactNode; +}; + +/** + * AlertModal 컴포넌트 + * + * @example + * ```tsx + * 모달 열기} + * content={
삭제하면 복구할 수 없습니다.
} + * footer={ + * <> + * + * + * + * } + * /> + * ``` + */ +export const AlertModal = ({ + title, + trigger, + content, + footer, +}: AlertModalProps) => { + return ( + + {trigger && {trigger}} + + + +
+ {title && ( + + {title} + + )} + {content &&
{content}
} +
+ {footer &&
{footer}
} +
+
+
+ ); +}; diff --git a/src/components/ui/AlertModal/index.ts b/src/components/ui/AlertModal/index.ts new file mode 100644 index 00000000..1c871920 --- /dev/null +++ b/src/components/ui/AlertModal/index.ts @@ -0,0 +1 @@ +export { AlertModal } from "./AlertModal"; diff --git a/src/components/ui/GNB/GNB.css.ts b/src/components/ui/GNB/GNB.css.ts index c64bc002..9ce58698 100644 --- a/src/components/ui/GNB/GNB.css.ts +++ b/src/components/ui/GNB/GNB.css.ts @@ -2,6 +2,7 @@ import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; import { semantic, typography } from "@/styles"; +import { zIndex } from "@/styles/zIndex.css"; export const wrapper = recipe({ base: { @@ -12,7 +13,7 @@ export const wrapper = recipe({ width: "100%", height: "5.6rem", padding: "1.4rem 2rem", - zIndex: 999, + zIndex: zIndex.gnb, }, variants: { background: { diff --git a/src/styles/reset.css.ts b/src/styles/reset.css.ts index dca79a10..8cc2f777 100644 --- a/src/styles/reset.css.ts +++ b/src/styles/reset.css.ts @@ -6,6 +6,10 @@ globalStyle("html", { fontSize: "62.5%", }); +globalStyle("html, body", { + height: "100%", +}); + globalStyle( "html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video", { diff --git a/src/styles/zIndex.css.ts b/src/styles/zIndex.css.ts new file mode 100644 index 00000000..53d69b5e --- /dev/null +++ b/src/styles/zIndex.css.ts @@ -0,0 +1,9 @@ +import { createGlobalTheme } from "@vanilla-extract/css"; + +export const zIndex = createGlobalTheme(":root", { + base: "0", + gnb: "100", + overlay: "900", + modal: "1000", + toast: "1100", +});