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 { AppHeader } from "@/app/components/AppHeader";
|
||||||
import { AppSidebar } from "@/app/components/AppSidebar";
|
import { AppSidebar } from "@/app/components/AppSidebar";
|
||||||
import { AppFooter } from "@/app/components/AppFooter";
|
import { AppFooter } from "@/app/components/AppFooter";
|
||||||
|
import { ToastProvider } from "@/app/components/ui/ToastProvider";
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -20,12 +21,14 @@ export default function RootLayout({
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
|
<ToastProvider>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<div style={{ display: "flex", minHeight: "80vh" }}>
|
<div style={{ display: "flex", minHeight: "80vh" }}>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main style={{ flex: 1, padding: 16 }}>{children}</main>
|
<main style={{ flex: 1, padding: 16 }}>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
</ToastProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
4.4 낙관적 업데이트 패턴 적용(작성/수정) o
|
4.4 낙관적 업데이트 패턴 적용(작성/수정) o
|
||||||
|
|
||||||
[공통/레이아웃]
|
[공통/레이아웃]
|
||||||
5.1 앱 레이아웃/헤더/푸터/사이드바 구성
|
5.1 앱 레이아웃/헤더/푸터/사이드바 구성 o
|
||||||
5.2 공통 컴포넌트(버튼/입력/모달/토스트) 제작
|
5.2 공통 컴포넌트(버튼/입력/모달/토스트) 제작 o
|
||||||
5.3 글로벌 로딩/스켈레톤/에러 경계 패턴 정립
|
5.3 글로벌 로딩/스켈레톤/에러 경계 패턴 정립
|
||||||
5.4 테마/다크모드 및 반응형 설정
|
5.4 테마/다크모드 및 반응형 설정
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user