5.2 공통 컴포넌트(버튼/입력/모달/토스트) 제작 o

This commit is contained in:
koreacomp5
2025-10-09 15:26:03 +09:00
parent 37a5a5efde
commit 563b162290
6 changed files with 118 additions and 8 deletions

View File

@@ -0,0 +1,23 @@
"use client";
import React from "react";
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "ghost";
};
export function Button({ variant = "primary", style, ...props }: Props) {
const base: React.CSSProperties = {
padding: "8px 12px",
borderRadius: 6,
border: "1px solid transparent",
cursor: "pointer",
};
const variants: Record<NonNullable<Props["variant"]>, React.CSSProperties> = {
primary: { background: "#111", color: "#fff" },
secondary: { background: "#eee", color: "#111" },
ghost: { background: "transparent", borderColor: "#ddd", color: "#111" },
};
return <button {...props} style={{ ...base, ...variants[variant], ...(style ?? {}) }} />;
}

View File

@@ -0,0 +1,23 @@
"use client";
import React from "react";
type Props = React.InputHTMLAttributes<HTMLInputElement> & { label?: string };
export function Input({ label, style, ...props }: Props) {
return (
<label style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{label && <span>{label}</span>}
<input
{...props}
style={{
padding: "8px 10px",
borderRadius: 6,
border: "1px solid #ddd",
...(style ?? {}),
}}
/>
</label>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import React from "react";
type Props = { open: boolean; onClose: () => void; children: React.ReactNode };
export function Modal({ open, onClose, children }: Props) {
if (!open) return null;
return (
<div
onClick={onClose}
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div onClick={(e) => e.stopPropagation()} style={{ background: "#fff", padding: 16, borderRadius: 8 }}>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
"use client";
import React, { createContext, useContext, useState } from "react";
type Toast = { id: string; message: string };
type Ctx = { show: (message: string) => void };
const ToastCtx = createContext<Ctx>({ show: () => {} });
export function useToast() {
return useContext(ToastCtx);
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const show = (message: string) => {
const t = { id: String(Date.now()), message };
setToasts((x) => [t, ...x]);
setTimeout(() => setToasts((x) => x.filter((i) => i.id !== t.id)), 3000);
};
return (
<ToastCtx.Provider value={{ show }}>
{children}
<div style={{ position: "fixed", bottom: 16, right: 16, display: "flex", flexDirection: "column", gap: 8 }}>
{toasts.map((t) => (
<div key={t.id} style={{ background: "#111", color: "#fff", padding: "8px 12px", borderRadius: 6 }}>
{t.message}
</div>
))}
</div>
</ToastCtx.Provider>
);
}

View File

@@ -4,6 +4,7 @@ import QueryProvider from "@/app/QueryProvider";
import { AppHeader } from "@/app/components/AppHeader";
import { AppSidebar } from "@/app/components/AppSidebar";
import { AppFooter } from "@/app/components/AppFooter";
import { ToastProvider } from "@/app/components/ui/ToastProvider";
export const metadata: Metadata = {
@@ -20,12 +21,14 @@ export default function RootLayout({
<html lang="en">
<body>
<QueryProvider>
<AppHeader />
<div style={{ display: "flex", minHeight: "80vh" }}>
<AppSidebar />
<main style={{ flex: 1, padding: 16 }}>{children}</main>
</div>
<AppFooter />
<ToastProvider>
<AppHeader />
<div style={{ display: "flex", minHeight: "80vh" }}>
<AppSidebar />
<main style={{ flex: 1, padding: 16 }}>{children}</main>
</div>
<AppFooter />
</ToastProvider>
</QueryProvider>
</body>
</html>