5.2 공통 컴포넌트(버튼/입력/모달/토스트) 제작 o
This commit is contained in:
23
src/app/components/ui/Button.tsx
Normal file
23
src/app/components/ui/Button.tsx
Normal 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 ?? {}) }} />;
|
||||
}
|
||||
|
||||
|
||||
23
src/app/components/ui/Input.tsx
Normal file
23
src/app/components/ui/Input.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
27
src/app/components/ui/Modal.tsx
Normal file
27
src/app/components/ui/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
34
src/app/components/ui/ToastProvider.tsx
Normal file
34
src/app/components/ui/ToastProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user