diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index b36baff5..8a943b96 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -229,14 +229,6 @@ export class DynamicFormService { ...actualData } = data; - console.log("๐Ÿ” [dynamicFormService] ๋ฐ›์€ ๋ฐ์ดํ„ฐ:", { - ์ „์ฒด๋ฐ์ดํ„ฐ: data, - writer, - company_code, - created_by, - updated_by, - }); - // ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์ค€๋น„ const dataToInsert: any = { ...actualData }; @@ -259,21 +251,12 @@ export class DynamicFormService { // ์ž‘์„ฑ์ž ์ •๋ณด ์ถ”๊ฐ€ (writer ์ปฌ๋Ÿผ ์šฐ์„ , ์—†์œผ๋ฉด created_by/updated_by) if (writer && tableColumns.includes("writer")) { - console.log(`โœ… writer ์ถ”๊ฐ€: ${writer}`); dataToInsert.writer = writer; - } else { - console.log(`โŒ writer ์ถ”๊ฐ€ ์‹คํŒจ:`, { - hasWriter: !!writer, - writerValue: writer, - hasColumn: tableColumns.includes("writer"), - }); } if (created_by && tableColumns.includes("created_by")) { - console.log(`โœ… created_by ์ถ”๊ฐ€: ${created_by}`); dataToInsert.created_by = created_by; } if (updated_by && tableColumns.includes("updated_by")) { - console.log(`โœ… updated_by ์ถ”๊ฐ€: ${updated_by}`); dataToInsert.updated_by = updated_by; } if (company_code && tableColumns.includes("company_code")) { @@ -299,18 +282,9 @@ export class DynamicFormService { `โš ๏ธ company_code ๊ธธ์ด ์ œํ•œ: ์•ž์˜ 32์ž๋กœ ์ž๋ฆ„ -> "${processedCompanyCode}"` ); } - console.log(`โœ… company_code ์ถ”๊ฐ€: ${processedCompanyCode}`); dataToInsert.company_code = processedCompanyCode; - } else { - console.log(`โŒ company_code ์ถ”๊ฐ€ ์‹คํŒจ:`, { - hasCompanyCode: !!company_code, - companyCodeValue: company_code, - hasColumn: tableColumns.includes("company_code"), - }); } - console.log("๐Ÿ” [dynamicFormService] ์ตœ์ข… ์ €์žฅ ๋ฐ์ดํ„ฐ:", dataToInsert); - // ๋‚ ์งœ/์‹œ๊ฐ„ ๋ฌธ์ž์—ด์„ ์ ์ ˆํ•œ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ Object.keys(dataToInsert).forEach((key) => { const value = dataToInsert[key]; diff --git a/docs/shadcn-ui-์™„์ „๊ฐ€์ด๋“œ.md b/docs/shadcn-ui-์™„์ „๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..0c575e76 --- /dev/null +++ b/docs/shadcn-ui-์™„์ „๊ฐ€์ด๋“œ.md @@ -0,0 +1,1215 @@ +# shadcn/ui ์™„์ „ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> ์ด ๋ฌธ์„œ๋Š” shadcn/ui ๊ณต์‹ ๋ฌธ์„œ(https://ui.shadcn.com)๋ฅผ ์ฒ ์ €ํžˆ ๋ถ„์„ํ•˜์—ฌ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +## ๋ชฉ์ฐจ + +1. [์„ค์น˜ ๋ฐ ์ดˆ๊ธฐ ์„ค์ •](#1-์„ค์น˜-๋ฐ-์ดˆ๊ธฐ-์„ค์ •) +2. [CSS ๋ณ€์ˆ˜ ๋ฐ ํ…Œ๋งˆ ์„ค์ •](#2-css-๋ณ€์ˆ˜-๋ฐ-ํ…Œ๋งˆ-์„ค์ •) +3. [์ปดํฌ๋„ŒํŠธ๋ณ„ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ](#3-์ปดํฌ๋„ŒํŠธ๋ณ„-๊ตฌํ˜„-๊ฐ€์ด๋“œ) +4. [๋ฌธ์ œ ํ•ด๊ฒฐ (Troubleshooting)](#4-๋ฌธ์ œ-ํ•ด๊ฒฐ-troubleshooting) + +--- + +## 1. ์„ค์น˜ ๋ฐ ์ดˆ๊ธฐ ์„ค์ • + +### 1.1 ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜ + +```bash +npm install tailwindcss@latest +npm install class-variance-authority clsx tailwind-merge +npm install @radix-ui/react-* # ํ•„์š”ํ•œ Radix UI ์ปดํฌ๋„ŒํŠธ๋“ค +npm install lucide-react # ์•„์ด์ฝ˜ +``` + +### 1.2 Tailwind CSS ์„ค์ • + +**์ค‘์š”**: shadcn/ui๋Š” Tailwind CSS v4๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ธฐ๋ณธ์ ์œผ๋กœ v3 ์Šคํƒ€์ผ์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. + +```js +// tailwind.config.js (v3 ์Šคํƒ€์ผ) +module.exports = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; +``` + +**Tailwind CSS v4 ์Šคํƒ€์ผ (@theme ์‚ฌ์šฉ):** + +```css +/* app/globals.css */ +@import "tailwindcss"; + +@theme { + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + /* ... ๋‚˜๋จธ์ง€ ์ƒ‰์ƒ */ +} +``` + +### 1.3 globals.css ํ•„์ˆ˜ ์„ค์ • + +```css +/* app/globals.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} +``` + +**์ค‘์š”ํ•œ ์ :** + +1. CSS ๋ณ€์ˆ˜๋Š” **HSL ํ˜•์‹**์œผ๋กœ ์ž‘์„ฑ (์˜ˆ: `222.2 47.4% 11.2%`) +2. `hsl()` ํ•จ์ˆ˜๋Š” **์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ** (Tailwind๊ฐ€ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€) +3. ๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„ (์‰ผํ‘œ ์—†์Œ) + +### 1.4 ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (lib/utils.ts) + +```typescript +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +``` + +์ด ํ•จ์ˆ˜๋Š” **๋ชจ๋“  shadcn/ui ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•„์ˆ˜**์ž…๋‹ˆ๋‹ค. + +--- + +## 2. CSS ๋ณ€์ˆ˜ ๋ฐ ํ…Œ๋งˆ ์„ค์ • + +### 2.1 ์ƒ‰์ƒ ์‹œ์Šคํ…œ ์ดํ•ด + +shadcn/ui๋Š” **์‹œ๋งจํ‹ฑ ์ƒ‰์ƒ ์‹œ์Šคํ…œ**์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค: + +| ์ƒ‰์ƒ ๋ณ€์ˆ˜ | ์šฉ๋„ | ์˜ˆ์‹œ | +| ---------------------- | ------------------------- | ---------------------------- | +| `--background` | ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ | ํฐ์ƒ‰ (๋ผ์ดํŠธ), ๊ฒ€์€์ƒ‰ (๋‹คํฌ) | +| `--foreground` | ๊ธฐ๋ณธ ํ…์ŠคํŠธ | ๊ฒ€์€์ƒ‰ (๋ผ์ดํŠธ), ํฐ์ƒ‰ (๋‹คํฌ) | +| `--primary` | ์ฃผ์š” ์•ก์…˜ (๋ฒ„ํŠผ, ์„ ํƒ ๋“ฑ) | ์ง„ํ•œ ๋„ค์ด๋น„/๊ฒ€์€์ƒ‰ | +| `--primary-foreground` | Primary ์œ„์˜ ํ…์ŠคํŠธ | ํฐ์ƒ‰ | +| `--secondary` | ๋ณด์กฐ ์•ก์…˜ | ์—ฐํ•œ ํšŒ์ƒ‰ | +| `--accent` | ๊ฐ•์กฐ, ํ˜ธ๋ฒ„ ํšจ๊ณผ | ์—ฐํ•œ ํŒŒ๋ž€์ƒ‰ | +| `--muted` | ๋น„ํ™œ์„ฑ/๋ณด์กฐ ๋ฐฐ๊ฒฝ | ๋ฐ์€ ํšŒ์ƒ‰ | +| `--muted-foreground` | ๋ณด์กฐ ํ…์ŠคํŠธ (์„ค๋ช… ๋“ฑ) | ์ค‘๊ฐ„ ํšŒ์ƒ‰ | +| `--destructive` | ์‚ญ์ œ/์—๋Ÿฌ ์•ก์…˜ | ๋นจ๊ฐ„์ƒ‰ | +| `--border` | ํ…Œ๋‘๋ฆฌ | ๋ฐ์€ ํšŒ์ƒ‰ | +| `--input` | ์ž…๋ ฅ ํ•„๋“œ ํ…Œ๋‘๋ฆฌ | ๋ฐ์€ ํšŒ์ƒ‰ | +| `--ring` | ํฌ์ปค์Šค ๋ง | ๊ฒ€์€์ƒ‰ | + +### 2.2 HSL vs OKLCH ์ฃผ์˜์‚ฌํ•ญ + +**๊ณต์‹ shadcn/ui๋Š” HSL ํ˜•์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:** + +```css +/* โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• (HSL) */ +--primary: 222.2 47.4% 11.2%; + +/* โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• (OKLCH) */ +--primary: oklch(0.205 0 0); +``` + +๋งŒ์•ฝ OKLCH๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด: + +1. ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ `hsl()` โ†’ `oklch()` ๋ณ€๊ฒฝ ํ•„์š” +2. Tailwind ์„ค์ • ์ˆ˜์ • ํ•„์š” +3. ๊ณต์‹ ๋ฌธ์„œ์™€ ๋‹ค๋ฅธ ์ƒ‰์ƒ์œผ๋กœ ๋ณด์ผ ์ˆ˜ ์žˆ์Œ + +**๊ถŒ์žฅ: ๊ณต์‹ HSL ํ˜•์‹ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ** + +### 2.3 Border Radius ์„ค์ • + +```css +:root { + --radius: 0.5rem; /* 8px, ๊ณต์‹ ๊ธฐ๋ณธ๊ฐ’ */ +} +``` + +์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ: + +- `rounded-lg`: `var(--radius)` = 8px +- `rounded-md`: `calc(var(--radius) - 2px)` = 6px +- `rounded-sm`: `calc(var(--radius) - 4px)` = 4px + +--- + +## 3. ์ปดํฌ๋„ŒํŠธ๋ณ„ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +### 3.1 Button ์ปดํฌ๋„ŒํŠธ + +```tsx +// components/ui/button.tsx +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; +``` + +**์‚ฌ์šฉ๋ฒ•:** + +```tsx +import { Button } from "@/components/ui/button"; + + + + + + + +``` + +### 3.2 Calendar ์ปดํฌ๋„ŒํŠธ (์ž์ฒด ์ œ์ž‘ - ์™„๋ฒฝ ๊ตฌํ˜„) + +#### 3.2.1 ๋ฌธ์ œ์  ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +**react-day-picker v9 ๋ฌธ์ œ:** + +- ์š”์ผ ํ—ค๋” ๊ฐ„๊ฒฉ์ด ์ด์ƒํ•˜๊ฒŒ ๋ณด์ž„ (MoTuWeThFrSa) +- ํด๋ž˜์Šค๋ช…์ด ๋ณ€๊ฒฝ๋˜์–ด ์Šคํƒ€์ผ ์ ์šฉ์ด ์–ด๋ ค์›€ +- `captionLayout="dropdown"`์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ + +**ํ•ด๊ฒฐ์ฑ…: ์ž์ฒด Calendar ์ปดํฌ๋„ŒํŠธ ์ œ์ž‘** + +- shadcn/ui ์Šคํƒ€์ผ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์žฌํ˜„ +- ์›”/์—ฐ๋„ ๋“œ๋กญ๋‹ค์šด ์„ ํƒ ๊ธฐ๋Šฅ ๋‚ด์žฅ +- ํฌ๊ธฐ ์กฐ์ ˆ ๊ฐ€๋Šฅ (`sm`, `default`, `lg`) + +#### 3.2.2 ํŒจํ‚ค์ง€ ์„ค์น˜ + +```bash +# shadcn/ui ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ•„์š” +npm install lucide-react +# react-day-picker๋Š” ๋ถˆํ•„์š”! +``` + +#### 3.2.2 ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ + +**ํŒŒ์ผ ์œ„์น˜:** `components/ui/custom-calendar.tsx` + +```tsx +"use client"; + +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface CustomCalendarProps { + selected?: Date; + onSelect?: (date: Date | undefined) => void; + className?: string; + mode?: "single"; + size?: "sm" | "default" | "lg"; +} + +const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +export function CustomCalendar({ + selected, + onSelect, + className, + mode = "single", + size = "default", +}: CustomCalendarProps) { + // ํฌ๊ธฐ๋ณ„ ํด๋ž˜์Šค ์ •์˜ + const sizeClasses = { + sm: { + cell: "h-7 w-7 text-xs", + header: "text-xs", + day: "text-[0.7rem]", + nav: "h-6 w-6", + }, + default: { + cell: "h-9 w-9 text-sm", + header: "text-[0.8rem]", + day: "text-sm", + nav: "h-7 w-7", + }, + lg: { + cell: "h-11 w-11 text-base", + header: "text-sm", + day: "text-base", + nav: "h-8 w-8", + }, + }; + + const currentSize = sizeClasses[size]; + const [currentDate, setCurrentDate] = React.useState(selected || new Date()); + const [viewYear, setViewYear] = React.useState(currentDate.getFullYear()); + const [viewMonth, setViewMonth] = React.useState(currentDate.getMonth()); + + const getDaysInMonth = (year: number, month: number) => { + return new Date(year, month + 1, 0).getDate(); + }; + + const getFirstDayOfMonth = (year: number, month: number) => { + return new Date(year, month, 1).getDay(); + }; + + const generateCalendarDays = () => { + const daysInMonth = getDaysInMonth(viewYear, viewMonth); + const firstDay = getFirstDayOfMonth(viewYear, viewMonth); + const daysInPrevMonth = getDaysInMonth(viewYear, viewMonth - 1); + + const days: Array<{ + date: number; + month: "prev" | "current" | "next"; + fullDate: Date; + }> = []; + + // Previous month days + for (let i = firstDay - 1; i >= 0; i--) { + const date = daysInPrevMonth - i; + days.push({ + date, + month: "prev", + fullDate: new Date(viewYear, viewMonth - 1, date), + }); + } + + // Current month days + for (let i = 1; i <= daysInMonth; i++) { + days.push({ + date: i, + month: "current", + fullDate: new Date(viewYear, viewMonth, i), + }); + } + + // Next month days + const remainingDays = 42 - days.length; + for (let i = 1; i <= remainingDays; i++) { + days.push({ + date: i, + month: "next", + fullDate: new Date(viewYear, viewMonth + 1, i), + }); + } + + return days; + }; + + const handlePrevMonth = () => { + if (viewMonth === 0) { + setViewMonth(11); + setViewYear(viewYear - 1); + } else { + setViewMonth(viewMonth - 1); + } + }; + + const handleNextMonth = () => { + if (viewMonth === 11) { + setViewMonth(0); + setViewYear(viewYear + 1); + } else { + setViewMonth(viewMonth + 1); + } + }; + + const handleDateClick = (date: Date) => { + if (onSelect) { + onSelect(date); + } + }; + + const isToday = (date: Date) => { + const today = new Date(); + return ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ); + }; + + const isSelected = (date: Date) => { + if (!selected) return false; + return ( + date.getDate() === selected.getDate() && + date.getMonth() === selected.getMonth() && + date.getFullYear() === selected.getFullYear() + ); + }; + + const calendarDays = generateCalendarDays(); + + return ( +
+ {/* Header with Month/Year Dropdowns */} +
+ + +
+ {/* Month Select */} + + + {/* Year Select */} + +
+ + +
+ + {/* Days of week */} +
+ {DAYS.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* Calendar grid */} +
+ {calendarDays.map((day, index) => { + const isOutside = day.month !== "current"; + const isTodayDate = isToday(day.fullDate); + const isSelectedDate = isSelected(day.fullDate); + + return ( + + ); + })} +
+
+ ); +} + +CustomCalendar.displayName = "CustomCalendar"; +``` + +#### 3.2.3 ์ฃผ์š” ๊ธฐ๋Šฅ + +**1. ์›”/์—ฐ๋„ ๋“œ๋กญ๋‹ค์šด ์„ ํƒ** + +- ์›”: January ~ December ์ „์ฒด ์„ ํƒ ๊ฐ€๋Šฅ +- ์—ฐ๋„: ํ˜„์žฌ ์—ฐ๋„ ยฑ50๋…„ ๋ฒ”์œ„ (์ด 100๋…„) +- shadcn/ui Select ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ + +**2. ํฌ๊ธฐ ์กฐ์ ˆ (`size` prop)** + +- `sm`: 28px ร— 28px ์…€ (์ž‘์€ ํฌ๊ธฐ) +- `default`: 36px ร— 36px ์…€ (๊ธฐ๋ณธ ํฌ๊ธฐ) +- `lg`: 44px ร— 44px ์…€ (ํฐ ํฌ๊ธฐ) + +**3. ์™„๋ฒฝํ•œ ์š”์ผ ๊ฐ„๊ฒฉ** + +- `grid grid-cols-7`๋กœ ์ •ํ™•ํžˆ 7๊ฐœ ์ปฌ๋Ÿผ +- Su Mo Tu We Th Fr Sa ๊ฐ„๊ฒฉ ์™„๋ฒฝ + +**4. shadcn/ui ์Šคํƒ€์ผ** + +- ์„ ํƒ๋œ ๋‚ ์งœ: Primary ๋ฐฐ๊ฒฝ (๊ฒ€์€์ƒ‰) +- ์˜ค๋Š˜ ๋‚ ์งœ: Accent ๋ฐฐ๊ฒฝ (์—ฐํ•œ ํŒŒ๋ž€์ƒ‰) +- ์™ธ๋ถ€ ๋‚ ์งœ: Muted ์ƒ‰์ƒ, 50% ํˆฌ๋ช…๋„ +- ํ˜ธ๋ฒ„ ํšจ๊ณผ: Ghost ๋ฒ„ํŠผ ์Šคํƒ€์ผ + +#### 3.2.4 ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• + +```tsx +import { CustomCalendar } from "@/components/ui/custom-calendar"; +import { useState } from "react"; + +export function CalendarDemo() { + const [date, setDate] = useState(new Date()); + + return ( + + ); +} +``` + +#### 3.2.5 ํฌ๊ธฐ ์กฐ์ ˆ + +```tsx +{ + /* ์ž‘์€ ํฌ๊ธฐ */ +} +; + +{ + /* ๊ธฐ๋ณธ ํฌ๊ธฐ */ +} +; + +{ + /* ํฐ ํฌ๊ธฐ */ +} +; +``` + +#### 3.2.6 Date Picker (Popover ๊ฒฐํ•ฉ) + +```tsx +import { CustomCalendar } from "@/components/ui/custom-calendar"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { useState } from "react"; + +export function DatePickerDemo() { + const [date, setDate] = useState(); + + return ( + + + + + + + + + ); +} +``` + +**์žฅ์ :** + +- โœ… ์›”/์—ฐ๋„ ๋“œ๋กญ๋‹ค์šด ์„ ํƒ ๊ฐ€๋Šฅ +- โœ… ์™„๋ฒฝํ•œ ์š”์ผ ๊ฐ„๊ฒฉ +- โœ… shadcn/ui ์Šคํƒ€์ผ ์™„๋ฒฝ ์žฌํ˜„ +- โœ… `date-fns` ๋ถˆํ•„์š” (์ˆœ์ˆ˜ JavaScript) + +### 3.3 Dialog (Modal) ์ปดํฌ๋„ŒํŠธ + +#### 3.3.1 ํŒจํ‚ค์ง€ ์„ค์น˜ + +```bash +npm install @radix-ui/react-dialog +``` + +#### 3.3.2 ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ + +```tsx +// components/ui/dialog.tsx +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogPortal = DialogPrimitive.Portal; +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; +``` + +#### 3.3.3 ํ‘œ์ค€ ์‚ฌ์šฉ ํŒจํ„ด + +```tsx +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function DialogDemo() { + return ( + + + + + + + ํ”„๋กœํ•„ ์ˆ˜์ • + + ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ๋˜๋ฉด ์ €์žฅ์„ ํด๋ฆญํ•˜์„ธ์š”. + + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +} +``` + +--- + +## 4. ๋ฌธ์ œ ํ•ด๊ฒฐ (Troubleshooting) + +### 4.1 Calendar๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ๋ณด์ด๋Š” ๊ฒฝ์šฐ + +#### ์ฆ์ƒ 1: ๋“œ๋กญ๋‹ค์šด์ด ๋‘ ๊ฐœ๋กœ ๋ณด์ž„ + +- **์›์ธ**: `react-day-picker` v8 ์Šคํƒ€์ผ๊ณผ v9 ์Šคํƒ€์ผ ํ˜ผ์šฉ +- **ํ•ด๊ฒฐ**: + ```tsx + // ์˜ฌ๋ฐ”๋ฅธ v9 ์Šคํƒ€์ผ classNames ์‚ฌ์šฉ + classNames={{ + caption: "flex justify-center pt-1 relative items-center", + caption_label: "text-sm font-medium", + // ... v9 ํด๋ž˜์Šค๋ช… ์‚ฌ์šฉ + }} + ``` + +#### ์ฆ์ƒ 2: ์„ ํƒ๋œ ๋‚ ์งœ๊ฐ€ ํŒŒ๋ž€ ํ…Œ๋‘๋ฆฌ๋กœ ๋ณด์ž„ + +- **์›์ธ**: `react-day-picker/style.css`๋ฅผ importํ•จ +- **ํ•ด๊ฒฐ**: + + ```css + /* โŒ ์ œ๊ฑฐํ•ด์•ผ ํ•จ */ + @import "react-day-picker/style.css"; + + /* โœ… shadcn/ui๋Š” ์ž์ฒด Tailwind ์Šคํƒ€์ผ๋งŒ ์‚ฌ์šฉ */ + ``` + +#### ์ฆ์ƒ 3: ์ƒ‰์ƒ์ด ๊ณต์‹ ๋ฌธ์„œ์™€ ๋‹ค๋ฆ„ + +- **์›์ธ**: CSS ๋ณ€์ˆ˜๊ฐ€ ์ž˜๋ชป ์„ค์ •๋จ +- **ํ•ด๊ฒฐ**: + + ```css + /* โœ… ์˜ฌ๋ฐ”๋ฅธ HSL ํ˜•์‹ */ + --primary: 222.2 47.4% 11.2%; + + /* โŒ ์ž˜๋ชป๋œ OKLCH ํ˜•์‹ */ + --primary: oklch(0.205 0 0); + ``` + +#### ์ฆ์ƒ 4: ๋“œ๋กญ๋‹ค์šด ํ™”์‚ดํ‘œ๊ฐ€ ์—†์Œ + +- **์›์ธ**: `captionLayout="dropdown"`์„ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ ์ปค์Šคํ…€ CSS๊ฐ€ ์—†์Œ +- **ํ•ด๊ฒฐ**: `react-day-picker` v9๋Š” ์ž๋™์œผ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ` +
+ + {/* Textarea */} +
+ +