Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<body>
<div id="root"></div>
<div id="toast-root"></div>
<div id="modal-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RouterProvider } from "react-router-dom";

import { router } from "./Router";

import ToastContainer from "@/components/Toast/ToastContainer";

function App() {
Expand Down
50 changes: 28 additions & 22 deletions src/components/Modal/AlertModalLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
import Button from "../Button";

import { Check, Notice } from "@/assets/icon";

interface ModalButton {
label: string;
style: "primary" | "white";
onClick: () => void;
}

interface Props {
iconType?: "check" | "warning" | "none";
message: string;
buttons: ModalButton[];
button: ModalButton;
onClose: () => void;
}

const ICONS = {
check: Check,
warning: Notice,
};

export default function AlertModalLayout({
message = "",
buttons = [],
iconType = "none",
message,
button,
onClose,
}: Props) {
const Icon = iconType !== "none" ? ICONS[iconType] : null;

return (
<div
className="relative bg-white rounded-lg p-5
w-[20.625rem] h-[13.75rem] md:w-[33.75rem] md:h-[15.625rem]"
className="bg-white p-7 rounded-lg text-center w-[18.625rem] h-[11.5rem] flex flex-col justify-between items-center"
onClick={(e) => e.stopPropagation()}
>
<p
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
text-black font-normal text-center whitespace-nowrap
text-[1rem] md:text-[1.125rem]"
{Icon && <Icon className="w-6 h-6 mb-3" />}
<p className="text-base text-black font-normal mb-5">{message}</p>
<Button
variant={button.style}
onClick={() => {
button.onClick();
onClose();
}}
textSize="sm"
className="py-2 px-4 cursor-pointer"
>
{message}
</p>

<div className="absolute bottom-6 left-1/2 md:left-auto md:right-6 md:translate-x-0 -translate-x-1/2">
<Button
onClick={buttons[0]?.onClick}
variant="primary"
textSize="md"
className="py-2 px-4 w-[8.625rem] h-[2.625rem] md:w-[7.5rem] md:h-[3rem] cursor-pointer"
>
{buttons[0]?.label}
</Button>
</div>
{button.label}
</Button>
</div>
);
}
71 changes: 23 additions & 48 deletions src/components/Modal/ConfirmModalLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,32 @@
import Button from "../Button";

import CheckIcon from "@/assets/icon/check.svg?react";
import NoticeIcon from "@/assets/icon/notice.svg?react";

interface ModalButton {
label: string;
style: "primary" | "white";
onClick: () => void;
}

interface Props {
iconType: "check" | "warning" | "none";
interface ConfirmModalLayoutProps {
message: string;
buttons: ModalButton[];
onClose: () => void;
onConfirm: () => void;
}

const ICONS = {
check: CheckIcon,
warning: NoticeIcon,
};

export default function ConfirmModalLayout({
iconType,
message = "",
buttons = [],
}: Props) {
const Icon = iconType !== "none" ? ICONS[iconType] : null;

message,
onClose,
onConfirm,
}: ConfirmModalLayoutProps) {
return (
<div
className="bg-white p-7 rounded-lg text-center
w-[18.625rem] h-[11.5rem] flex flex-col justify-between items-center"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-3 flex justify-center">
{Icon && <Icon className="w-6 h-6" />}
</div>

<p className="text-base text-black font-normal mb-5">{message}</p>

<div className="flex justify-center gap-3">
{buttons.map((button, index) => (
<Button
key={index}
onClick={button.onClick}
variant={button.style === "primary" ? "primary" : "white"}
textSize="sm"
className="py-2 px-4 cursor-pointer"
>
{button.label}
</Button>
))}
<div className="w-[18.625rem] md:w-[18.625rem] bg-white rounded-lg p-6 text-center">
<p className="text-gray-900 text-base md:text-lg font-normal mb-6">
{message}
</p>
<div className="flex justify-end gap-3">
<button
onClick={onClose}
className="w-[7.5rem] h-[3rem] border border-red-500 text-red-500 rounded"
>
아니오
</button>
<button
onClick={onConfirm}
className="w-[7.5rem] h-[3rem] bg-red-500 text-white rounded"
>
</button>
</div>
</div>
);
Expand Down
71 changes: 41 additions & 30 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
import AlertModalLayout from "./AlertModalLayout";
import ConfirmModalLayout from "./ConfirmModalLayout";

interface ModalButton {
label: string;
style: "primary" | "white"; // "primary" -> "filledRed", "white" -> "outlinedRed"
onClick: () => void;
}
import { useModalStore } from "@/store/useModalStore";

interface ModalProps {
message: string;
iconType?: "check" | "warning" | "none";
buttons: ModalButton[];
onClose: () => void;
}
export default function Modal() {
const { isOpen, options, closeModal } = useModalStore();

if (!isOpen || !options) return null;

const handleClose = () => {
options.onClose?.();
closeModal();
};

export default function Modal({
message = "",
iconType = "none",
buttons = [],
onClose,
}: ModalProps) {
return (
<div
className="fixed inset-0 bg-black/70 flex justify-center items-center z-50"
onClick={onClose}
>
{iconType === "none" ? (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50">
{options.type === "alert" && (
<AlertModalLayout
message={message}
buttons={buttons}
onClose={onClose}
message={options.message}
onClose={handleClose}
iconType={options.iconType ?? "none"}
button={{
label: "확인",
style: "primary",
onClick: handleClose,
}}
/>
) : (
)}

{options.type === "confirm" && (
<ConfirmModalLayout
iconType={iconType}
message={message}
buttons={buttons}
onClose={onClose}
message={options.message}
onClose={handleClose}
onConfirm={() => {
options.onConfirm?.();
closeModal();
}}
/>
)}

{options.type === "message" && (
<AlertModalLayout
message={options.message}
onClose={handleClose}
button={{
label: "확인",
style: "primary",
onClick: handleClose,
}}
/>
)}
</div>
Expand Down
32 changes: 32 additions & 0 deletions src/store/useModalStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { create } from "zustand";

export type ModalType = "alert" | "confirm" | "message";

export interface ModalButton {
label: string;
style: "primary" | "white";
onClick: () => void;
}

export interface ModalOptions {
type: "alert" | "confirm" | "message";
message: string;
iconType?: "check" | "warning" | "none";
buttons?: ModalButton[];
onClose?: () => void;
onConfirm?: () => void;
}

interface ModalState {
isOpen: boolean;
options: ModalOptions | null;
openModal: (options: ModalOptions) => void;
closeModal: () => void;
}

export const useModalStore = create<ModalState>((set) => ({
isOpen: false,
options: null,
openModal: (options) => set({ isOpen: true, options }),
closeModal: () => set({ isOpen: false, options: null }),
}));