From 4dbb55f6e11c547a8ac487ef2db608eb5db2490a Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 10:29:56 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=EB=8B=AC=EB=A0=A5=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20md=ED=8C=8C=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CALENDAR_WIDGET_PLAN.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md diff --git a/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md new file mode 100644 index 00000000..84f2a4dc --- /dev/null +++ b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md @@ -0,0 +1,228 @@ +# ๐Ÿ“… ๋‹ฌ๋ ฅ ์œ„์ ฏ ๊ตฌํ˜„ ๊ณ„ํš + +## ๊ฐœ์š” + +๋Œ€์‹œ๋ณด๋“œ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ฌ๋ ฅ ์œ„์ ฏ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋‚ ์งœ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ผ์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ๋‹ฌ๋ ฅ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ธฐ๋Šฅ + +### 1. ๋‹ฌ๋ ฅ ๋ทฐ ํƒ€์ž… + +- **์›”๊ฐ„ ๋ทฐ**: ํ•œ ๋‹ฌ ์ „์ฒด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋ณธ ๋ทฐ +- **์ฃผ๊ฐ„ ๋ทฐ**: ์ผ์ฃผ์ผ์„ ์„ธ๋กœ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ทฐ +- **์ผ๊ฐ„ ๋ทฐ**: ํ•˜๋ฃจ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ผ์ • ๋ทฐ + +### 2. ๋‹ฌ๋ ฅ ์„ค์ • + +- **์‹œ์ž‘ ์š”์ผ**: ์›”์š”์ผ ์‹œ์ž‘ / ์ผ์š”์ผ ์‹œ์ž‘ ์„ ํƒ +- **์ฃผ๋ง ๊ฐ•์กฐ**: ์ฃผ๋ง ์ƒ‰์ƒ ๋‹ค๋ฅด๊ฒŒ ํ‘œ์‹œ +- **์˜ค๋Š˜ ๋‚ ์งœ ๊ฐ•์กฐ**: ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ +- **๊ณตํœด์ผ ํ‘œ์‹œ**: ํ•œ๊ตญ ๊ณตํœด์ผ ํ‘œ์‹œ (์„ ํƒ ์‚ฌํ•ญ) + +### 3. ํ…Œ๋งˆ ๋ฐ ์Šคํƒ€์ผ + +- **Light ํ…Œ๋งˆ**: ๋ฐ์€ ๋ฐฐ๊ฒฝ +- **Dark ํ…Œ๋งˆ**: ์–ด๋‘์šด ๋ฐฐ๊ฒฝ +- **์‚ฌ์šฉ์ž ์ง€์ •**: ์ปค์Šคํ…€ ์ƒ‰์ƒ ์„ ํƒ + +### 4. ์ผ์ • ๊ธฐ๋Šฅ (ํ–ฅํ›„ ํ™•์žฅ) + +- ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ ์ถ”๊ฐ€ +- ์ผ์ • ํ‘œ์‹œ (์™ธ๋ถ€ ์—ฐ๋™) + +## ๊ตฌํ˜„ ๋‹จ๊ณ„ + +### โœ… Step 1: ํƒ€์ž… ์ •์˜ + +- [ ] `CalendarConfig` ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [ ] `types.ts`์— ๋‹ฌ๋ ฅ ์„ค์ • ํƒ€์ž… ์ถ”๊ฐ€ +- [ ] ์š”์†Œ ํƒ€์ž…์— 'calendar' subtype ์ถ”๊ฐ€ + +### โœ… Step 2: ๊ธฐ๋ณธ ๋‹ฌ๋ ฅ ์ปดํฌ๋„ŒํŠธ + +- [ ] `CalendarWidget.tsx` - ๋ฉ”์ธ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ +- [ ] `MonthView.tsx` - ์›”๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ +- [ ] `WeekView.tsx` - ์ฃผ๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ (์„ ํƒ) +- [ ] ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ + +### โœ… Step 3: ๋‹ฌ๋ ฅ ๋„ค๋น„๊ฒŒ์ด์…˜ + +- [ ] ์ด์ „/๋‹ค์Œ ์›” ์ด๋™ ๋ฒ„ํŠผ +- [ ] ์˜ค๋Š˜๋กœ ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ +- [ ] ์›”/์—ฐ๋„ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด + +### โœ… Step 4: ์„ค์ • UI + +- [ ] `CalendarSettings.tsx` - Popover ๋‚ด์žฅ ์„ค์ • ์ปดํฌ๋„ŒํŠธ +- [ ] ๋ทฐ ํƒ€์ž… ์„ ํƒ (์›”๊ฐ„/์ฃผ๊ฐ„/์ผ๊ฐ„) +- [ ] ์‹œ์ž‘ ์š”์ผ ์„ค์ • +- [ ] ํ…Œ๋งˆ ์„ ํƒ +- [ ] ํ‘œ์‹œ ์˜ต์…˜ (์ฃผ๋ง ๊ฐ•์กฐ, ๊ณตํœด์ผ ๋“ฑ) + +### โœ… Step 5: ์Šคํƒ€์ผ๋ง + +- [ ] ๋‹ฌ๋ ฅ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ +- [ ] ๋‚ ์งœ ์…€ ๋””์ž์ธ +- [ ] ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ +- [ ] ์ฃผ๋ง/ํ‰์ผ ๊ตฌ๋ถ„ +- [ ] ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (ํฌ๊ธฐ๋ณ„ ์ตœ์ ํ™”) + +### โœ… Step 6: ํ†ตํ•ฉ + +- [ ] `DashboardSidebar`์— ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ถ”๊ฐ€ +- [ ] `CanvasElement`์—์„œ ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ Œ๋”๋ง +- [ ] `DashboardDesigner`์— ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + +### โœ… Step 7: ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ + +- [ ] ํ•œ๊ตญ ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ ์ •์˜ +- [ ] ๊ณตํœด์ผ ํ‘œ์‹œ ๊ธฐ๋Šฅ +- [ ] ๊ณตํœด์ผ ์ด๋ฆ„ ํˆดํŒ + +### โœ… Step 8: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” + +- [ ] ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ +- [ ] ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” +- [ ] ์ ‘๊ทผ์„ฑ ๊ฐœ์„  + +## ๊ธฐ์ˆ  ์Šคํƒ + +### UI ์ปดํฌ๋„ŒํŠธ + +- **shadcn/ui**: Button, Select, Switch, Popover, Card +- **lucide-react**: Settings, ChevronLeft, ChevronRight, Calendar + +### ๋‚ ์งœ ์ฒ˜๋ฆฌ + +- **JavaScript Date API**: ๊ธฐ๋ณธ ๋‚ ์งœ ๊ณ„์‚ฐ +- **Intl.DateTimeFormat**: ๋‚ ์งœ ํ˜•์‹ํ™” +- ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ์ˆœ์ˆ˜ ๊ตฌํ˜„ + +### ์Šคํƒ€์ผ๋ง + +- **Tailwind CSS**: ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ +- **CSS Grid**: ๋‹ฌ๋ ฅ ๋ ˆ์ด์•„์›ƒ + +## ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ + +``` +widgets/ +โ”œโ”€โ”€ CalendarWidget.tsx # ๋ฉ”์ธ ์œ„์ ฏ (์„ค์ • ๋ฒ„ํŠผ ํฌํ•จ) +โ”œโ”€โ”€ CalendarSettings.tsx # ์„ค์ • UI (Popover ๋‚ด๋ถ€) +โ”œโ”€โ”€ MonthView.tsx # ์›”๊ฐ„ ๋ทฐ +โ”œโ”€โ”€ WeekView.tsx # ์ฃผ๊ฐ„ ๋ทฐ (์„ ํƒ) +โ”œโ”€โ”€ DayView.tsx # ์ผ๊ฐ„ ๋ทฐ (์„ ํƒ) +โ””โ”€โ”€ calendarUtils.ts # ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ +``` + +## ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + +```typescript +interface CalendarConfig { + view: "month" | "week" | "day"; // ๋ทฐ ํƒ€์ž… + startWeekOn: "monday" | "sunday"; // ์ฃผ ์‹œ์ž‘ ์š”์ผ + highlightWeekends: boolean; // ์ฃผ๋ง ๊ฐ•์กฐ + highlightToday: boolean; // ์˜ค๋Š˜ ๊ฐ•์กฐ + showHolidays: boolean; // ๊ณตํœด์ผ ํ‘œ์‹œ + theme: "light" | "dark" | "custom"; // ํ…Œ๋งˆ + customColor?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ + showWeekNumbers?: boolean; // ์ฃผ์ฐจ ํ‘œ์‹œ (์„ ํƒ) +} +``` + +## UI/UX ๊ณ ๋ ค์‚ฌํ•ญ + +### ๋ฐ˜์‘ํ˜• ๋””์ž์ธ + +- **2x2**: ๋ฏธ๋‹ˆ ๋‹ฌ๋ ฅ (์›”๊ฐ„ ๋ทฐ๋งŒ, ๋‚ ์งœ๋งŒ ํ‘œ์‹œ) +- **3x3**: ๊ธฐ๋ณธ ๋‹ฌ๋ ฅ (์›”๊ฐ„ ๋ทฐ, ์š”์ผ ํ—ค๋” ํฌํ•จ) +- **4x4 ์ด์ƒ**: ํ’€ ๋‹ฌ๋ ฅ (๋ชจ๋“  ๊ธฐ๋Šฅ, ์ผ์ • ํ‘œ์‹œ ๊ฐ€๋Šฅ) + +### ์ธํ„ฐ๋ž™์…˜ + +- ๋‚ ์งœ ํด๋ฆญ ์‹œ ํ•ด๋‹น ๋‚ ์งœ ์ •๋ณด ํ‘œ์‹œ (์„ ํƒ) +- ๋“œ๋ž˜๊ทธ๋กœ ์›” ๋ณ€๊ฒฝ (์„ ํƒ) +- ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› + +### ์ ‘๊ทผ์„ฑ + +- ๋‚ ์งœ ์…€์— ์ ์ ˆํ•œ aria-label +- ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› +- ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ํ˜ธํ™˜ + +## ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + +```typescript +interface Holiday { + date: string; // 'MM-DD' ํ˜•์‹ + name: string; // ๊ณตํœด์ผ ์ด๋ฆ„ + isRecurring: boolean; // ๋งค๋…„ ๋ฐ˜๋ณต ์—ฌ๋ถ€ +} + +// 2025๋…„ ํ•œ๊ตญ ๊ณตํœด์ผ ์˜ˆ์‹œ +const KOREAN_HOLIDAYS: Holiday[] = [ + { date: "01-01", name: "์‹ ์ •", isRecurring: true }, + { date: "01-28", name: "์„ค๋‚  ์—ฐํœด", isRecurring: false }, + { date: "01-29", name: "์„ค๋‚ ", isRecurring: false }, + { date: "01-30", name: "์„ค๋‚  ์—ฐํœด", isRecurring: false }, + { date: "03-01", name: "์‚ผ์ผ์ ˆ", isRecurring: true }, + { date: "05-05", name: "์–ด๋ฆฐ์ด๋‚ ", isRecurring: true }, + { date: "06-06", name: "ํ˜„์ถฉ์ผ", isRecurring: true }, + { date: "08-15", name: "๊ด‘๋ณต์ ˆ", isRecurring: true }, + { date: "10-03", name: "๊ฐœ์ฒœ์ ˆ", isRecurring: true }, + { date: "10-09", name: "ํ•œ๊ธ€๋‚ ", isRecurring: true }, + { date: "12-25", name: "ํฌ๋ฆฌ์Šค๋งˆ์Šค", isRecurring: true }, +]; +``` + +## ํ–ฅํ›„ ํ™•์žฅ ๊ธฐ๋Šฅ + +### Phase 2 (์„ ํƒ) + +- [ ] ์ผ์ • ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ +- [ ] ๋ฐ˜๋ณต ์ผ์ • ์„ค์ • +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒ‰์ƒ ๊ตฌ๋ถ„ +- [ ] ๋‹ค๋ฅธ ๋‹ฌ๋ ฅ ์„œ๋น„์Šค ์—ฐ๋™ (Google Calendar, Outlook ๋“ฑ) +- [ ] ์ผ์ • ์•Œ๋ฆผ ๊ธฐ๋Šฅ +- [ ] ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ผ์ • ์ด๋™ + +### Phase 3 (์„ ํƒ) + +- [ ] ์—ฌ๋Ÿฌ ๋‹ฌ๋ ฅ ๋ ˆ์ด์–ด ์ง€์› +- [ ] ์ผ์ • ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ +- [ ] ์›”๋ณ„ ํ†ต๊ณ„ (์ผ์ • ๊ฐœ์ˆ˜ ๋“ฑ) +- [ ] CSV/iCal ๋‚ด๋ณด๋‚ด๊ธฐ + +## ์ฐธ๊ณ ์‚ฌํ•ญ + +### ์žฅ์  + +- ์ˆœ์ˆ˜ JavaScript๋กœ ๊ตฌํ˜„ (์™ธ๋ถ€ ์˜์กด์„ฑ ์ตœ์†Œํ™”) +- shadcn/ui ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉ์œผ๋กœ ์ผ๊ด€๋œ ๋””์ž์ธ +- ์‹œ๊ณ„ ์œ„์ ฏ๊ณผ ๋™์ผํ•œ ํŒจํ„ด (๋‚ด์žฅ ์„ค์ • UI) + +### ์ฃผ์˜์‚ฌํ•ญ + +- ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ์ •ํ™•์„ฑ ๊ฒ€์ฆ ํ•„์š” +- ์œค๋…„ ์ฒ˜๋ฆฌ +- ํƒ€์ž„์กด ๊ณ ๋ ค (ํ•„์š”์‹œ) +- ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์—์„œ์˜ ๊ฐ€๋…์„ฑ + +## ์™„๋ฃŒ ๊ธฐ์ค€ + +- [x] ์›”๊ฐ„ ๋ทฐ ๋‹ฌ๋ ฅ์ด ์ •ํ™•ํ•˜๊ฒŒ ํ‘œ์‹œ๋จ +- [x] ์ด์ „/๋‹ค์Œ ์›” ๋„ค๋น„๊ฒŒ์ด์…˜์ด ์ž‘๋™ํ•จ +- [x] ์˜ค๋Š˜ ๋‚ ์งœ๊ฐ€ ํ•˜์ด๋ผ์ดํŠธ๋จ +- [x] ์ฃผ๋ง์ด ๋‹ค๋ฅธ ์ƒ‰์ƒ์œผ๋กœ ํ‘œ์‹œ๋จ +- [x] ๊ณตํœด์ผ์ด ํ‘œ์‹œ๋˜๊ณ  ์ด๋ฆ„์ด ๋ณด์ž„ +- [x] ์„ค์ • UI์—์„œ ๋ชจ๋“  ์˜ต์…˜์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Œ +- [x] ํ…Œ๋งˆ ๋ณ€๊ฒฝ์ด ์ฆ‰์‹œ ๋ฐ˜์˜๋จ +- [x] 2x2 ํฌ๊ธฐ์—์„œ๋„ ๊น”๋”ํ•˜๊ฒŒ ํ‘œ์‹œ๋จ +- [x] 4x4 ํฌ๊ธฐ์—์„œ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•จ + +--- + +## ๊ตฌํ˜„ ์‹œ์ž‘ + +์ด์ œ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค! -- 2.43.0 From 2311729338eee57f5dd2742ca85c87b66ce11275 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 10:48:17 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=EB=8B=AC=EB=A0=A5=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CALENDAR_WIDGET_PLAN.md | 60 ++--- .../admin/dashboard/CanvasElement.tsx | 57 +++-- .../admin/dashboard/DashboardDesigner.tsx | 17 +- .../admin/dashboard/DashboardSidebar.tsx | 8 + .../admin/dashboard/ElementConfigModal.tsx | 39 +--- frontend/components/admin/dashboard/types.ts | 16 +- .../dashboard/widgets/CalendarSettings.tsx | 207 ++++++++++++++++++ .../dashboard/widgets/CalendarWidget.tsx | 121 ++++++++++ .../dashboard/widgets/ClockConfigModal.tsx | 205 ----------------- .../admin/dashboard/widgets/MonthView.tsx | 117 ++++++++++ .../admin/dashboard/widgets/calendarUtils.ts | 162 ++++++++++++++ 11 files changed, 715 insertions(+), 294 deletions(-) create mode 100644 frontend/components/admin/dashboard/widgets/CalendarSettings.tsx create mode 100644 frontend/components/admin/dashboard/widgets/CalendarWidget.tsx delete mode 100644 frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx create mode 100644 frontend/components/admin/dashboard/widgets/MonthView.tsx create mode 100644 frontend/components/admin/dashboard/widgets/calendarUtils.ts diff --git a/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md index 84f2a4dc..e127be43 100644 --- a/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md +++ b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md @@ -34,57 +34,57 @@ ### โœ… Step 1: ํƒ€์ž… ์ •์˜ -- [ ] `CalendarConfig` ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ -- [ ] `types.ts`์— ๋‹ฌ๋ ฅ ์„ค์ • ํƒ€์ž… ์ถ”๊ฐ€ -- [ ] ์š”์†Œ ํƒ€์ž…์— 'calendar' subtype ์ถ”๊ฐ€ +- [x] `CalendarConfig` ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [x] `types.ts`์— ๋‹ฌ๋ ฅ ์„ค์ • ํƒ€์ž… ์ถ”๊ฐ€ +- [x] ์š”์†Œ ํƒ€์ž…์— 'calendar' subtype ์ถ”๊ฐ€ ### โœ… Step 2: ๊ธฐ๋ณธ ๋‹ฌ๋ ฅ ์ปดํฌ๋„ŒํŠธ -- [ ] `CalendarWidget.tsx` - ๋ฉ”์ธ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ -- [ ] `MonthView.tsx` - ์›”๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ -- [ ] `WeekView.tsx` - ์ฃผ๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ (์„ ํƒ) -- [ ] ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +- [x] `CalendarWidget.tsx` - ๋ฉ”์ธ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ +- [x] `MonthView.tsx` - ์›”๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ +- [x] ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (`calendarUtils.ts`) +- [ ] `WeekView.tsx` - ์ฃผ๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ (ํ–ฅํ›„ ์ถ”๊ฐ€) ### โœ… Step 3: ๋‹ฌ๋ ฅ ๋„ค๋น„๊ฒŒ์ด์…˜ -- [ ] ์ด์ „/๋‹ค์Œ ์›” ์ด๋™ ๋ฒ„ํŠผ -- [ ] ์˜ค๋Š˜๋กœ ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ -- [ ] ์›”/์—ฐ๋„ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด +- [x] ์ด์ „/๋‹ค์Œ ์›” ์ด๋™ ๋ฒ„ํŠผ +- [x] ์˜ค๋Š˜๋กœ ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ +- [ ] ์›”/์—ฐ๋„ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด (ํ–ฅํ›„ ์ถ”๊ฐ€) ### โœ… Step 4: ์„ค์ • UI -- [ ] `CalendarSettings.tsx` - Popover ๋‚ด์žฅ ์„ค์ • ์ปดํฌ๋„ŒํŠธ -- [ ] ๋ทฐ ํƒ€์ž… ์„ ํƒ (์›”๊ฐ„/์ฃผ๊ฐ„/์ผ๊ฐ„) -- [ ] ์‹œ์ž‘ ์š”์ผ ์„ค์ • -- [ ] ํ…Œ๋งˆ ์„ ํƒ -- [ ] ํ‘œ์‹œ ์˜ต์…˜ (์ฃผ๋ง ๊ฐ•์กฐ, ๊ณตํœด์ผ ๋“ฑ) +- [x] `CalendarSettings.tsx` - Popover ๋‚ด์žฅ ์„ค์ • ์ปดํฌ๋„ŒํŠธ +- [x] ๋ทฐ ํƒ€์ž… ์„ ํƒ (์›”๊ฐ„ - ํ˜„์žฌ ๊ตฌํ˜„) +- [x] ์‹œ์ž‘ ์š”์ผ ์„ค์ • +- [x] ํ…Œ๋งˆ ์„ ํƒ (light/dark/custom) +- [x] ํ‘œ์‹œ ์˜ต์…˜ (์ฃผ๋ง ๊ฐ•์กฐ, ๊ณตํœด์ผ, ์˜ค๋Š˜ ๊ฐ•์กฐ) ### โœ… Step 5: ์Šคํƒ€์ผ๋ง -- [ ] ๋‹ฌ๋ ฅ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ -- [ ] ๋‚ ์งœ ์…€ ๋””์ž์ธ -- [ ] ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ -- [ ] ์ฃผ๋ง/ํ‰์ผ ๊ตฌ๋ถ„ -- [ ] ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (ํฌ๊ธฐ๋ณ„ ์ตœ์ ํ™”) +- [x] ๋‹ฌ๋ ฅ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ +- [x] ๋‚ ์งœ ์…€ ๋””์ž์ธ +- [x] ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ +- [x] ์ฃผ๋ง/ํ‰์ผ ๊ตฌ๋ถ„ +- [x] ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (ํฌ๊ธฐ๋ณ„ ์ตœ์ ํ™”) ### โœ… Step 6: ํ†ตํ•ฉ -- [ ] `DashboardSidebar`์— ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ถ”๊ฐ€ -- [ ] `CanvasElement`์—์„œ ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ Œ๋”๋ง -- [ ] `DashboardDesigner`์— ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • +- [x] `DashboardSidebar`์— ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ถ”๊ฐ€ +- [x] `CanvasElement`์—์„œ ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ Œ๋”๋ง +- [x] `DashboardDesigner`์— ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • ### โœ… Step 7: ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ -- [ ] ํ•œ๊ตญ ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ ์ •์˜ -- [ ] ๊ณตํœด์ผ ํ‘œ์‹œ ๊ธฐ๋Šฅ -- [ ] ๊ณตํœด์ผ ์ด๋ฆ„ ํˆดํŒ +- [x] ํ•œ๊ตญ ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ ์ •์˜ +- [x] ๊ณตํœด์ผ ํ‘œ์‹œ ๊ธฐ๋Šฅ +- [x] ๊ณตํœด์ผ ์ด๋ฆ„ ํˆดํŒ ### โœ… Step 8: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” -- [ ] ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ -- [ ] ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ -- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” -- [ ] ์ ‘๊ทผ์„ฑ ๊ฐœ์„  +- [ ] ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ (์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ ํ•„์š”) +- [x] ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” (ํ•„์š”์‹œ) +- [ ] ์ ‘๊ทผ์„ฑ ๊ฐœ์„  (ํ•„์š”์‹œ) ## ๊ธฐ์ˆ  ์Šคํƒ diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index d830263d..aced2eb9 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -19,6 +19,8 @@ const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/Exch // ์‹œ๊ณ„ ์œ„์ ฏ ์ž„ํฌํŠธ import { ClockWidget } from "./widgets/ClockWidget"; +// ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ž„ํฌํŠธ +import { CalendarWidget } from "./widgets/CalendarWidget"; interface CanvasElementProps { element: DashboardElement; @@ -130,26 +132,30 @@ export function CanvasElement({ let newX = resizeStart.elementX; let newY = resizeStart.elementY; - const minSize = GRID_CONFIG.CELL_SIZE * 2; // ์ตœ์†Œ 2์…€ + // ์ตœ์†Œ ํฌ๊ธฐ ์„ค์ •: ๋‹ฌ๋ ฅ์€ 2x3, ๋‚˜๋จธ์ง€๋Š” 2x2 + const minWidthCells = 2; + const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2; + const minWidth = GRID_CONFIG.CELL_SIZE * minWidthCells; + const minHeight = GRID_CONFIG.CELL_SIZE * minHeightCells; switch (resizeStart.handle) { case "se": // ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); break; case "sw": // ์™ผ์ชฝ ์•„๋ž˜ - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width - deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); newX = resizeStart.elementX + deltaX; break; case "ne": // ์˜ค๋ฅธ์ชฝ ์œ„ - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height - deltaY); newY = resizeStart.elementY + deltaY; break; case "nw": // ์™ผ์ชฝ ์œ„ - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + newWidth = Math.max(minWidth, resizeStart.width - deltaX); + newHeight = Math.max(minHeight, resizeStart.height - deltaY); newX = resizeStart.elementX + deltaX; newY = resizeStart.elementY + deltaY; break; @@ -281,6 +287,8 @@ export function CanvasElement({ return "bg-gradient-to-br from-cyan-400 to-indigo-800"; case "clock": return "bg-gradient-to-br from-teal-400 to-cyan-600"; + case "calendar": + return "bg-gradient-to-br from-indigo-400 to-purple-600"; default: return "bg-gray-200"; } @@ -310,16 +318,17 @@ export function CanvasElement({
{element.title}
- {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} - {onConfigure && !(element.type === "widget" && element.subtype === "clock") && ( - - )} + {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„, ๋‹ฌ๋ ฅ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} + {onConfigure && + !(element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) && ( + + )} {/* ์‚ญ์ œ ๋ฒ„ํŠผ */}
+ ) : element.type === "widget" && element.subtype === "calendar" ? ( + // ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ Œ๋”๋ง +
+ { + onUpdate(element.id, { calendarConfig: newConfig }); + }} + /> +
) : ( // ๊ธฐํƒ€ ์œ„์ ฏ ๋ Œ๋”๋ง
{ - // ๊ธฐ๋ณธ ํฌ๊ธฐ: ์ฐจํŠธ๋Š” 4x3 ์…€, ์œ„์ ฏ์€ 2x2 ์…€ - const defaultCells = type === "chart" ? { width: 4, height: 3 } : { width: 2, height: 2 }; + // ๊ธฐ๋ณธ ํฌ๊ธฐ ์„ค์ • + let defaultCells = { width: 2, height: 2 }; // ๊ธฐ๋ณธ ์œ„์ ฏ ํฌ๊ธฐ + + if (type === "chart") { + defaultCells = { width: 4, height: 3 }; // ์ฐจํŠธ + } else if (type === "widget" && subtype === "calendar") { + defaultCells = { width: 2, height: 3 }; // ๋‹ฌ๋ ฅ ์ตœ์†Œ ํฌ๊ธฐ + } + const cellWithGap = GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP; const defaultWidth = defaultCells.width * cellWithGap - GRID_CONFIG.GAP; @@ -232,7 +239,7 @@ export default function DashboardDesigner() {
{/* ํŽธ์ง‘ ์ค‘์ธ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ */} {dashboardTitle && ( -
+
๐Ÿ“ ํŽธ์ง‘ ์ค‘: {dashboardTitle}
)} @@ -291,6 +298,8 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string { return "โ˜๏ธ ๋‚ ์”จ ์œ„์ ฏ"; case "clock": return "โฐ ์‹œ๊ณ„ ์œ„์ ฏ"; + case "calendar": + return "๐Ÿ“… ๋‹ฌ๋ ฅ ์œ„์ ฏ"; default: return "๐Ÿ”ง ์œ„์ ฏ"; } @@ -319,6 +328,8 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string { return "์„œ์šธ\n23ยฐC\n๊ตฌ๋ฆ„ ๋งŽ์Œ"; case "clock": return "clock"; + case "calendar": + return "calendar"; default: return "์œ„์ ฏ ๋‚ด์šฉ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค"; } diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx index 41888172..82ce27c3 100644 --- a/frontend/components/admin/dashboard/DashboardSidebar.tsx +++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx @@ -111,6 +111,14 @@ export function DashboardSidebar() { onDragStart={handleDragStart} className="border-l-4 border-teal-500" /> +
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index dc9d3f32..31fdee8b 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -1,10 +1,9 @@ "use client"; import React, { useState, useCallback } from "react"; -import { DashboardElement, ChartDataSource, ChartConfig, QueryResult, ClockConfig } from "./types"; +import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types"; import { QueryEditor } from "./QueryEditor"; import { ChartConfigPanel } from "./ChartConfigPanel"; -import { ClockConfigModal } from "./widgets/ClockConfigModal"; interface ElementConfigModalProps { element: DashboardElement; @@ -57,46 +56,14 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element onClose(); }, [element, dataSource, chartConfig, onSave, onClose]); - // ์‹œ๊ณ„ ์œ„์ ฏ ์„ค์ • ์ €์žฅ - const handleClockConfigSave = useCallback( - (clockConfig: ClockConfig) => { - const updatedElement: DashboardElement = { - ...element, - clockConfig, - }; - onSave(updatedElement); - }, - [element, onSave], - ); - // ๋ชจ๋‹ฌ์ด ์—ด๋ ค์žˆ์ง€ ์•Š์œผ๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ if (!isOpen) return null; - // ์‹œ๊ณ„ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋ชจ๋‹ฌ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ - if (element.type === "widget" && element.subtype === "clock") { + // ์‹œ๊ณ„, ๋‹ฌ๋ ฅ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋ชจ๋‹ฌ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ + if (element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) { return null; } - // ์ด์ „ ์ฝ”๋“œ ํ˜ธํ™˜์„ฑ ์œ ์ง€ (์•„๋ž˜ ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ๋Š” ์ œ๊ฑฐ ์˜ˆ์ •) - if (false && element.type === "widget" && element.subtype === "clock") { - return ( - - ); - } - return (
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index d304c9f3..2ac0bb6d 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -14,7 +14,8 @@ export type ElementSubtype = | "combo" // ์ฐจํŠธ ํƒ€์ž… | "exchange" | "weather" - | "clock"; // ์œ„์ ฏ ํƒ€์ž… + | "clock" + | "calendar"; // ์œ„์ ฏ ํƒ€์ž… export interface Position { x: number; @@ -37,6 +38,7 @@ export interface DashboardElement { dataSource?: ChartDataSource; // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • chartConfig?: ChartConfig; // ์ฐจํŠธ ์„ค์ • clockConfig?: ClockConfig; // ์‹œ๊ณ„ ์„ค์ • + calendarConfig?: CalendarConfig; // ๋‹ฌ๋ ฅ ์„ค์ • } export interface DragData { @@ -86,3 +88,15 @@ export interface ClockConfig { theme: "light" | "dark" | "custom"; // ํ…Œ๋งˆ customColor?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ (custom ํ…Œ๋งˆ์ผ ๋•Œ) } + +// ๋‹ฌ๋ ฅ ์œ„์ ฏ ์„ค์ • +export interface CalendarConfig { + view: "month" | "week" | "day"; // ๋ทฐ ํƒ€์ž… + startWeekOn: "monday" | "sunday"; // ์ฃผ ์‹œ์ž‘ ์š”์ผ + highlightWeekends: boolean; // ์ฃผ๋ง ๊ฐ•์กฐ + highlightToday: boolean; // ์˜ค๋Š˜ ๊ฐ•์กฐ + showHolidays: boolean; // ๊ณตํœด์ผ ํ‘œ์‹œ + theme: "light" | "dark" | "custom"; // ํ…Œ๋งˆ + customColor?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ + showWeekNumbers?: boolean; // ์ฃผ์ฐจ ํ‘œ์‹œ (์„ ํƒ) +} diff --git a/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx b/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx new file mode 100644 index 00000000..89633cc8 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useState } from "react"; +import { CalendarConfig } from "../types"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; + +interface CalendarSettingsProps { + config: CalendarConfig; + onSave: (config: CalendarConfig) => void; + onClose: () => void; +} + +/** + * ๋‹ฌ๋ ฅ ์œ„์ ฏ ์„ค์ • UI (Popover ๋‚ด๋ถ€์šฉ) + */ +export function CalendarSettings({ config, onSave, onClose }: CalendarSettingsProps) { + const [localConfig, setLocalConfig] = useState(config); + + const handleSave = () => { + onSave(localConfig); + }; + + return ( +
+ {/* ํ—ค๋” */} +
+

+ ๐Ÿ“… + ๋‹ฌ๋ ฅ ์„ค์ • +

+
+ + {/* ๋‚ด์šฉ - ์Šคํฌ๋กค ๊ฐ€๋Šฅ */} +
+ {/* ๋ทฐ ํƒ€์ž… ์„ ํƒ (ํ˜„์žฌ๋Š” month๋งŒ) */} +
+ + +
+ + + + {/* ์‹œ์ž‘ ์š”์ผ ์„ ํƒ */} +
+ +
+ + +
+
+ + + + {/* ํ…Œ๋งˆ ์„ ํƒ */} +
+ +
+ {[ + { + value: "light", + label: "Light", + gradient: "bg-gradient-to-br from-white to-gray-100", + text: "text-gray-900", + }, + { + value: "dark", + label: "Dark", + gradient: "bg-gradient-to-br from-gray-800 to-gray-900", + text: "text-white", + }, + { + value: "custom", + label: "์‚ฌ์šฉ์ž", + gradient: "bg-gradient-to-br from-blue-400 to-purple-600", + text: "text-white", + }, + ].map((theme) => ( + + ))} +
+ + {/* ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ */} + {localConfig.theme === "custom" && ( + + +
+ setLocalConfig({ ...localConfig, customColor: e.target.value })} + className="h-10 w-16 cursor-pointer" + /> + setLocalConfig({ ...localConfig, customColor: e.target.value })} + placeholder="#3b82f6" + className="flex-1 font-mono text-xs" + /> +
+
+ )} +
+ + + + {/* ํ‘œ์‹œ ์˜ต์…˜ */} +
+ +
+ {/* ์˜ค๋Š˜ ๊ฐ•์กฐ */} +
+
+ ๐Ÿ“ + +
+ setLocalConfig({ ...localConfig, highlightToday: checked })} + /> +
+ + {/* ์ฃผ๋ง ๊ฐ•์กฐ */} +
+
+ ๐ŸŽจ + +
+ setLocalConfig({ ...localConfig, highlightWeekends: checked })} + /> +
+ + {/* ๊ณตํœด์ผ ํ‘œ์‹œ */} +
+
+ ๐ŸŽ‰ + +
+ setLocalConfig({ ...localConfig, showHolidays: checked })} + /> +
+
+
+
+ + {/* ํ‘ธํ„ฐ */} +
+ + +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx b/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx new file mode 100644 index 00000000..4f54ac65 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { DashboardElement, CalendarConfig } from "../types"; +import { MonthView } from "./MonthView"; +import { CalendarSettings } from "./CalendarSettings"; +import { generateCalendarDays, getMonthName, navigateMonth } from "./calendarUtils"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Settings, ChevronLeft, ChevronRight, Calendar } from "lucide-react"; + +interface CalendarWidgetProps { + element: DashboardElement; + onConfigUpdate?: (config: CalendarConfig) => void; +} + +/** + * ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ + * - ์›”๊ฐ„/์ฃผ๊ฐ„/์ผ๊ฐ„ ๋ทฐ ์ง€์› + * - ๋„ค๋น„๊ฒŒ์ด์…˜ (์ด์ „/๋‹ค์Œ ์›”, ์˜ค๋Š˜) + * - ๋‚ด์žฅ ์„ค์ • UI + */ +export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps) { + // ํ˜„์žฌ ํ‘œ์‹œ ์ค‘์ธ ๋…„/์›” + const today = new Date(); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [currentMonth, setCurrentMonth] = useState(today.getMonth()); + const [settingsOpen, setSettingsOpen] = useState(false); + + // ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ + const config = element.calendarConfig || { + view: "month", + startWeekOn: "sunday", + highlightWeekends: true, + highlightToday: true, + showHolidays: true, + theme: "light", + }; + + // ์„ค์ • ์ €์žฅ ํ•ธ๋“ค๋Ÿฌ + const handleSaveSettings = (newConfig: CalendarConfig) => { + onConfigUpdate?.(newConfig); + setSettingsOpen(false); + }; + + // ์ด์ „ ์›”๋กœ ์ด๋™ + const handlePrevMonth = () => { + const { year, month } = navigateMonth(currentYear, currentMonth, "prev"); + setCurrentYear(year); + setCurrentMonth(month); + }; + + // ๋‹ค์Œ ์›”๋กœ ์ด๋™ + const handleNextMonth = () => { + const { year, month } = navigateMonth(currentYear, currentMonth, "next"); + setCurrentYear(year); + setCurrentMonth(month); + }; + + // ์˜ค๋Š˜๋กœ ๋Œ์•„๊ฐ€๊ธฐ + const handleToday = () => { + setCurrentYear(today.getFullYear()); + setCurrentMonth(today.getMonth()); + }; + + // ๋‹ฌ๋ ฅ ๋‚ ์งœ ์ƒ์„ฑ + const calendarDays = generateCalendarDays(currentYear, currentMonth, config.startWeekOn); + + // ํฌ๊ธฐ์— ๋”ฐ๋ฅธ ์ปดํŒฉํŠธ ๋ชจ๋“œ ํŒ๋‹จ + const isCompact = element.size.width < 400 || element.size.height < 400; + + return ( +
+ {/* ํ—ค๋” - ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+ {/* ์ด์ „ ์›” ๋ฒ„ํŠผ */} + + + {/* ํ˜„์žฌ ๋…„์›” ํ‘œ์‹œ */} +
+ + {currentYear}๋…„ {getMonthName(currentMonth)} + + {!isCompact && ( + + )} +
+ + {/* ๋‹ค์Œ ์›” ๋ฒ„ํŠผ */} + +
+ + {/* ๋‹ฌ๋ ฅ ์ฝ˜ํ…์ธ  */} +
+ {config.view === "month" && } + {/* ์ถ”ํ›„ WeekView, DayView ์ถ”๊ฐ€ ๊ฐ€๋Šฅ */} +
+ + {/* ์„ค์ • ๋ฒ„ํŠผ - ์šฐ์ธก ํ•˜๋‹จ */} +
+ + + + + + setSettingsOpen(false)} /> + + +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx b/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx deleted file mode 100644 index 26067b48..00000000 --- a/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx +++ /dev/null @@ -1,205 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ClockConfig } from "../types"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { Input } from "@/components/ui/input"; -import { Card } from "@/components/ui/card"; -import { X } from "lucide-react"; - -interface ClockConfigModalProps { - config: ClockConfig; - onSave: (config: ClockConfig) => void; - onClose: () => void; -} - -/** - * ์‹œ๊ณ„ ์œ„์ ฏ ์„ค์ • ๋ชจ๋‹ฌ - * - ์Šคํƒ€์ผ ์„ ํƒ (์•„๋‚ ๋กœ๊ทธ/๋””์ง€ํ„ธ/๋‘˜๋‹ค) - * - ํƒ€์ž„์กด ์„ ํƒ - * - ํ…Œ๋งˆ ์„ ํƒ - * - ์˜ต์…˜ ํ† ๊ธ€ (๋‚ ์งœ, ์ดˆ, 24์‹œ๊ฐ„) - */ -export function ClockConfigModal({ config, onSave, onClose }: ClockConfigModalProps) { - const [localConfig, setLocalConfig] = useState(config); - - const handleSave = () => { - onSave(localConfig); - onClose(); - }; - - return ( - - - - - โฐ - ์‹œ๊ณ„ ์œ„์ ฏ ์„ค์ • - - - - {/* ๋‚ด์šฉ - ์Šคํฌ๋กค ๊ฐ€๋Šฅ */} -
- {/* ์Šคํƒ€์ผ ์„ ํƒ */} -
- -
- {[ - { value: "digital", label: "๋””์ง€ํ„ธ", icon: "๐Ÿ”ข" }, - { value: "analog", label: "์•„๋‚ ๋กœ๊ทธ", icon: "๐Ÿ•" }, - { value: "both", label: "๋‘˜ ๋‹ค", icon: "โฐ" }, - ].map((style) => ( - - ))} -
-
- - {/* ํƒ€์ž„์กด ์„ ํƒ */} -
- - -
- - {/* ํ…Œ๋งˆ ์„ ํƒ */} -
- -
- {[ - { - value: "light", - label: "Light", - gradient: "bg-gradient-to-br from-white to-gray-100", - text: "text-gray-900", - }, - { - value: "dark", - label: "Dark", - gradient: "bg-gradient-to-br from-gray-800 to-gray-900", - text: "text-white", - }, - { - value: "custom", - label: "์‚ฌ์šฉ์ž ์ง€์ •", - gradient: "bg-gradient-to-br from-blue-400 to-purple-600", - text: "text-white", - }, - ].map((theme) => ( - - ))} -
- - {/* ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ ์„ ํƒ */} - {localConfig.theme === "custom" && ( - - -
- setLocalConfig({ ...localConfig, customColor: e.target.value })} - className="h-12 w-20 cursor-pointer" - /> -
- setLocalConfig({ ...localConfig, customColor: e.target.value })} - placeholder="#3b82f6" - className="font-mono" - /> -

์‹œ๊ณ„์˜ ๋ฐฐ๊ฒฝ์ƒ‰์ด๋‚˜ ๊ฐ•์กฐ์ƒ‰์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค

-
-
-
- )} -
- - {/* ์˜ต์…˜ ํ† ๊ธ€ */} -
- -
- {/* ๋‚ ์งœ ํ‘œ์‹œ */} - - ๐Ÿ“… - - setLocalConfig({ ...localConfig, showDate: checked })} - /> - - - {/* ์ดˆ ํ‘œ์‹œ */} - - โฑ๏ธ - - setLocalConfig({ ...localConfig, showSeconds: checked })} - /> - - - {/* 24์‹œ๊ฐ„ ํ˜•์‹ */} - - ๐Ÿ• - - setLocalConfig({ ...localConfig, format24h: checked })} - /> - -
-
-
- - - - - -
-
- ); -} diff --git a/frontend/components/admin/dashboard/widgets/MonthView.tsx b/frontend/components/admin/dashboard/widgets/MonthView.tsx new file mode 100644 index 00000000..c0fd3871 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/MonthView.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { CalendarConfig } from "../types"; +import { CalendarDay, getWeekDayNames } from "./calendarUtils"; + +interface MonthViewProps { + days: CalendarDay[]; + config: CalendarConfig; + isCompact?: boolean; // ์ž‘์€ ํฌ๊ธฐ (2x2, 3x3) +} + +/** + * ์›”๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ ์ปดํฌ๋„ŒํŠธ + */ +export function MonthView({ days, config, isCompact = false }: MonthViewProps) { + const weekDayNames = getWeekDayNames(config.startWeekOn); + + // ํ…Œ๋งˆ๋ณ„ ์Šคํƒ€์ผ + const getThemeStyles = () => { + if (config.theme === "custom" && config.customColor) { + return { + todayBg: config.customColor, + holidayText: config.customColor, + weekendText: "#dc2626", + }; + } + + if (config.theme === "dark") { + return { + todayBg: "#3b82f6", + holidayText: "#f87171", + weekendText: "#f87171", + }; + } + + // light ํ…Œ๋งˆ + return { + todayBg: "#3b82f6", + holidayText: "#dc2626", + weekendText: "#dc2626", + }; + }; + + const themeStyles = getThemeStyles(); + + // ๋‚ ์งœ ์…€ ์Šคํƒ€์ผ ํด๋ž˜์Šค + const getDayCellClass = (day: CalendarDay) => { + const baseClass = "flex aspect-square items-center justify-center rounded-lg transition-colors"; + const sizeClass = isCompact ? "text-xs" : "text-sm"; + + let colorClass = "text-gray-700"; + + // ํ˜„์žฌ ์›”์ด ์•„๋‹Œ ๋‚ ์งœ + if (!day.isCurrentMonth) { + colorClass = "text-gray-300"; + } + // ์˜ค๋Š˜ + else if (config.highlightToday && day.isToday) { + colorClass = "text-white font-bold"; + } + // ๊ณตํœด์ผ + else if (config.showHolidays && day.isHoliday) { + colorClass = "font-semibold"; + } + // ์ฃผ๋ง + else if (config.highlightWeekends && day.isWeekend) { + colorClass = "text-red-600"; + } + + const bgClass = config.highlightToday && day.isToday ? "" : "hover:bg-gray-100"; + + return `${baseClass} ${sizeClass} ${colorClass} ${bgClass}`; + }; + + return ( +
+ {/* ์š”์ผ ํ—ค๋” */} + {!isCompact && ( +
+ {weekDayNames.map((name, index) => { + const isWeekend = config.startWeekOn === "sunday" ? index === 0 || index === 6 : index === 5 || index === 6; + return ( +
+ {name} +
+ ); + })} +
+ )} + + {/* ๋‚ ์งœ ๊ทธ๋ฆฌ๋“œ */} +
+ {days.map((day, index) => ( +
+ {day.day} +
+ ))} +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/calendarUtils.ts b/frontend/components/admin/dashboard/widgets/calendarUtils.ts new file mode 100644 index 00000000..4bdb8deb --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/calendarUtils.ts @@ -0,0 +1,162 @@ +/** + * ๋‹ฌ๋ ฅ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ + */ + +// ํ•œ๊ตญ ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ (2025๋…„ ๊ธฐ์ค€) +export interface Holiday { + date: string; // 'MM-DD' ํ˜•์‹ + name: string; + isRecurring: boolean; +} + +export const KOREAN_HOLIDAYS: Holiday[] = [ + { date: "01-01", name: "์‹ ์ •", isRecurring: true }, + { date: "01-28", name: "์„ค๋‚  ์—ฐํœด", isRecurring: false }, + { date: "01-29", name: "์„ค๋‚ ", isRecurring: false }, + { date: "01-30", name: "์„ค๋‚  ์—ฐํœด", isRecurring: false }, + { date: "03-01", name: "์‚ผ์ผ์ ˆ", isRecurring: true }, + { date: "05-05", name: "์–ด๋ฆฐ์ด๋‚ ", isRecurring: true }, + { date: "06-06", name: "ํ˜„์ถฉ์ผ", isRecurring: true }, + { date: "08-15", name: "๊ด‘๋ณต์ ˆ", isRecurring: true }, + { date: "10-03", name: "๊ฐœ์ฒœ์ ˆ", isRecurring: true }, + { date: "10-09", name: "ํ•œ๊ธ€๋‚ ", isRecurring: true }, + { date: "12-25", name: "ํฌ๋ฆฌ์Šค๋งˆ์Šค", isRecurring: true }, +]; + +/** + * ํŠน์ • ์›”์˜ ์ฒซ ๋‚  Date ๊ฐ์ฒด ๋ฐ˜ํ™˜ + */ +export function getFirstDayOfMonth(year: number, month: number): Date { + return new Date(year, month, 1); +} + +/** + * ํŠน์ • ์›”์˜ ๋งˆ์ง€๋ง‰ ๋‚ ์งœ ๋ฐ˜ํ™˜ + */ +export function getLastDateOfMonth(year: number, month: number): number { + return new Date(year, month + 1, 0).getDate(); +} + +/** + * ํŠน์ • ์›”์˜ ์ฒซ ๋‚ ์˜ ์š”์ผ ๋ฐ˜ํ™˜ (0=์ผ์š”์ผ, 1=์›”์š”์ผ, ...) + */ +export function getFirstDayOfWeek(year: number, month: number): number { + return new Date(year, month, 1).getDay(); +} + +/** + * ๋‹ฌ๋ ฅ ๊ทธ๋ฆฌ๋“œ์— ํ‘œ์‹œํ•  ๋‚ ์งœ ๋ฐฐ์—ด ์ƒ์„ฑ + * @param year ๋…„๋„ + * @param month ์›” (0-11) + * @param startWeekOn ์ฃผ ์‹œ์ž‘ ์š”์ผ ('monday' | 'sunday') + * @returns 6์ฃผ * 7์ผ = 42๊ฐœ์˜ ๋‚ ์งœ ์ •๋ณด ๋ฐฐ์—ด + */ +export interface CalendarDay { + date: Date; + day: number; + isCurrentMonth: boolean; + isToday: boolean; + isWeekend: boolean; + isHoliday: boolean; + holidayName?: string; +} + +export function generateCalendarDays( + year: number, + month: number, + startWeekOn: "monday" | "sunday" = "sunday", +): CalendarDay[] { + const days: CalendarDay[] = []; + const firstDay = getFirstDayOfWeek(year, month); + const lastDate = getLastDateOfMonth(year, month); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // ์‹œ์ž‘ ์˜คํ”„์…‹ ๊ณ„์‚ฐ + let startOffset = firstDay; + if (startWeekOn === "monday") { + startOffset = firstDay === 0 ? 6 : firstDay - 1; + } + + // ์ด์ „ ๋‹ฌ ๋‚ ์งœ๋“ค + const prevMonthLastDate = getLastDateOfMonth(year, month - 1); + for (let i = startOffset - 1; i >= 0; i--) { + const date = new Date(year, month - 1, prevMonthLastDate - i); + days.push(createCalendarDay(date, false, today)); + } + + // ํ˜„์žฌ ๋‹ฌ ๋‚ ์งœ๋“ค + for (let day = 1; day <= lastDate; day++) { + const date = new Date(year, month, day); + days.push(createCalendarDay(date, true, today)); + } + + // ๋‹ค์Œ ๋‹ฌ ๋‚ ์งœ๋“ค (42๊ฐœ ์ฑ„์šฐ๊ธฐ) + const remainingDays = 42 - days.length; + for (let day = 1; day <= remainingDays; day++) { + const date = new Date(year, month + 1, day); + days.push(createCalendarDay(date, false, today)); + } + + return days; +} + +/** + * CalendarDay ๊ฐ์ฒด ์ƒ์„ฑ + */ +function createCalendarDay(date: Date, isCurrentMonth: boolean, today: Date): CalendarDay { + const dayOfWeek = date.getDay(); + const isToday = date.getTime() === today.getTime(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + // ๊ณตํœด์ผ ์ฒดํฌ + const monthStr = String(date.getMonth() + 1).padStart(2, "0"); + const dayStr = String(date.getDate()).padStart(2, "0"); + const dateKey = `${monthStr}-${dayStr}`; + const holiday = KOREAN_HOLIDAYS.find((h) => h.date === dateKey); + + return { + date, + day: date.getDate(), + isCurrentMonth, + isToday, + isWeekend, + isHoliday: !!holiday, + holidayName: holiday?.name, + }; +} + +/** + * ์š”์ผ ์ด๋ฆ„ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ + */ +export function getWeekDayNames(startWeekOn: "monday" | "sunday" = "sunday"): string[] { + const sundayFirst = ["์ผ", "์›”", "ํ™”", "์ˆ˜", "๋ชฉ", "๊ธˆ", "ํ† "]; + const mondayFirst = ["์›”", "ํ™”", "์ˆ˜", "๋ชฉ", "๊ธˆ", "ํ† ", "์ผ"]; + return startWeekOn === "monday" ? mondayFirst : sundayFirst; +} + +/** + * ์›” ์ด๋ฆ„ ๋ฐ˜ํ™˜ + */ +export function getMonthName(month: number): string { + const months = ["1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]; + return months[month]; +} + +/** + * ์ด์ „/๋‹ค์Œ ์›”๋กœ ์ด๋™ + */ +export function navigateMonth(year: number, month: number, direction: "prev" | "next"): { year: number; month: number } { + if (direction === "prev") { + if (month === 0) { + return { year: year - 1, month: 11 }; + } + return { year, month: month - 1 }; + } else { + if (month === 11) { + return { year: year + 1, month: 0 }; + } + return { year, month: month + 1 }; + } +} + -- 2.43.0 From 0d4b985d5aad1eaec64f9a3fed0a8eff8206ed4c Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 11:26:53 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=EA=B8=B0=EC=82=AC=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 21 +- .../DRIVER_MANAGEMENT_WIDGET_PLAN.md | 345 ++++++++++++++++++ .../admin/dashboard/DashboardDesigner.tsx | 6 +- .../admin/dashboard/DashboardSidebar.tsx | 8 + .../admin/dashboard/ElementConfigModal.tsx | 9 +- frontend/components/admin/dashboard/types.ts | 31 +- .../dashboard/widgets/DriverListView.tsx | 161 ++++++++ .../widgets/DriverManagementSettings.tsx | 195 ++++++++++ .../widgets/DriverManagementWidget.tsx | 159 ++++++++ .../admin/dashboard/widgets/driverMockData.ts | 181 +++++++++ .../admin/dashboard/widgets/driverUtils.ts | 256 +++++++++++++ 11 files changed, 1365 insertions(+), 7 deletions(-) create mode 100644 frontend/components/admin/dashboard/DRIVER_MANAGEMENT_WIDGET_PLAN.md create mode 100644 frontend/components/admin/dashboard/widgets/DriverListView.tsx create mode 100644 frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx create mode 100644 frontend/components/admin/dashboard/widgets/DriverManagementWidget.tsx create mode 100644 frontend/components/admin/dashboard/widgets/driverMockData.ts create mode 100644 frontend/components/admin/dashboard/widgets/driverUtils.ts diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 393f3141..589edb0f 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -26,6 +26,8 @@ const CalculatorWidget = dynamic(() => import("@/components/dashboard/widgets/Ca import { ClockWidget } from "./widgets/ClockWidget"; // ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ž„ํฌํŠธ import { CalendarWidget } from "./widgets/CalendarWidget"; +// ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ ์ž„ํฌํŠธ +import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; interface CanvasElementProps { element: DashboardElement; @@ -294,6 +296,8 @@ export function CanvasElement({ return "bg-gradient-to-br from-teal-400 to-cyan-600"; case "calendar": return "bg-gradient-to-br from-indigo-400 to-purple-600"; + case "driver-management": + return "bg-gradient-to-br from-blue-400 to-indigo-600"; default: return "bg-gray-200"; } @@ -323,9 +327,12 @@ export function CanvasElement({
{element.title}
- {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„, ๋‹ฌ๋ ฅ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} + {/* ์„ค์ • ๋ฒ„ํŠผ (์‹œ๊ณ„, ๋‹ฌ๋ ฅ, ๊ธฐ์‚ฌ๊ด€๋ฆฌ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI ์‚ฌ์šฉ) */} {onConfigure && - !(element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) && ( + !( + element.type === "widget" && + (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") + ) && (
+ ) : element.type === "widget" && element.subtype === "driver-management" ? ( + // ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ ๋ Œ๋”๋ง +
+ { + onUpdate(element.id, { driverManagementConfig: newConfig }); + }} + /> +
) : ( // ๊ธฐํƒ€ ์œ„์ ฏ ๋ Œ๋”๋ง
{/* ํŽธ์ง‘ ์ค‘์ธ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ */} {dashboardTitle && ( -
+
๐Ÿ“ ํŽธ์ง‘ ์ค‘: {dashboardTitle}
)} @@ -302,6 +302,8 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string { return "๐Ÿงฎ ๊ณ„์‚ฐ๊ธฐ ์œ„์ ฏ"; case "calendar": return "๐Ÿ“… ๋‹ฌ๋ ฅ ์œ„์ ฏ"; + case "driver-management": + return "๐Ÿšš ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ"; default: return "๐Ÿ”ง ์œ„์ ฏ"; } @@ -334,6 +336,8 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string { return "calculator"; case "calendar": return "calendar"; + case "driver-management": + return "driver-management"; default: return "์œ„์ ฏ ๋‚ด์šฉ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค"; } diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx index 7946815d..ee8fa13d 100644 --- a/frontend/components/admin/dashboard/DashboardSidebar.tsx +++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx @@ -127,6 +127,14 @@ export function DashboardSidebar() { onDragStart={handleDragStart} className="border-l-4 border-indigo-500" /> +
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index 31fdee8b..8bcacd2c 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -59,13 +59,16 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // ๋ชจ๋‹ฌ์ด ์—ด๋ ค์žˆ์ง€ ์•Š์œผ๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ if (!isOpen) return null; - // ์‹œ๊ณ„, ๋‹ฌ๋ ฅ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋ชจ๋‹ฌ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ - if (element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) { + // ์‹œ๊ณ„, ๋‹ฌ๋ ฅ, ๊ธฐ์‚ฌ๊ด€๋ฆฌ ์œ„์ ฏ์€ ์ž์ฒด ์„ค์ • UI๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋ชจ๋‹ฌ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ + if ( + element.type === "widget" && + (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") + ) { return null; } return ( -
+
{/* ๋ชจ๋‹ฌ ํ—ค๋” */}
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 2e753d1b..6d01fc01 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -16,7 +16,8 @@ export type ElementSubtype = | "weather" | "clock" | "calendar" - | "calculator"; // ์œ„์ ฏ ํƒ€์ž… + | "calculator" + | "driver-management"; // ์œ„์ ฏ ํƒ€์ž… export interface Position { x: number; @@ -40,6 +41,7 @@ export interface DashboardElement { chartConfig?: ChartConfig; // ์ฐจํŠธ ์„ค์ • clockConfig?: ClockConfig; // ์‹œ๊ณ„ ์„ค์ • calendarConfig?: CalendarConfig; // ๋‹ฌ๋ ฅ ์„ค์ • + driverManagementConfig?: DriverManagementConfig; // ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์„ค์ • } export interface DragData { @@ -101,3 +103,30 @@ export interface CalendarConfig { customColor?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ showWeekNumbers?: boolean; // ์ฃผ์ฐจ ํ‘œ์‹œ (์„ ํƒ) } + +// ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ ์„ค์ • +export interface DriverManagementConfig { + viewType: "list"; // ๋ทฐ ํƒ€์ž… (ํ˜„์žฌ๋Š” ๋ฆฌ์ŠคํŠธ๋งŒ) + autoRefreshInterval: number; // ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ ๊ฐ„๊ฒฉ (์ดˆ) + visibleColumns: string[]; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก + theme: "light" | "dark" | "custom"; // ํ…Œ๋งˆ + customColor?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ + statusFilter: "all" | "driving" | "standby" | "resting" | "maintenance"; // ์ƒํƒœ ํ•„ํ„ฐ + sortBy: "name" | "vehicleNumber" | "status" | "departureTime"; // ์ •๋ ฌ ๊ธฐ์ค€ + sortOrder: "asc" | "desc"; // ์ •๋ ฌ ์ˆœ์„œ +} + +// ๊ธฐ์‚ฌ ์ •๋ณด +export interface DriverInfo { + id: string; // ๊ธฐ์‚ฌ ๊ณ ์œ  ID + name: string; // ๊ธฐ์‚ฌ ์ด๋ฆ„ + vehicleNumber: string; // ์ฐจ๋Ÿ‰ ๋ฒˆํ˜ธ + vehicleType: string; // ์ฐจ๋Ÿ‰ ์œ ํ˜• + phone: string; // ์—ฐ๋ฝ์ฒ˜ + status: "standby" | "driving" | "resting" | "maintenance"; // ์šดํ–‰ ์ƒํƒœ + departure?: string; // ์ถœ๋ฐœ์ง€ + destination?: string; // ๋ชฉ์ ์ง€ + departureTime?: string; // ์ถœ๋ฐœ ์‹œ๊ฐ„ + estimatedArrival?: string; // ์˜ˆ์ƒ ๋„์ฐฉ ์‹œ๊ฐ„ + progress?: number; // ์šดํ–‰ ์ง„ํ–‰๋ฅ  (0-100) +} diff --git a/frontend/components/admin/dashboard/widgets/DriverListView.tsx b/frontend/components/admin/dashboard/widgets/DriverListView.tsx new file mode 100644 index 00000000..f5df6944 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/DriverListView.tsx @@ -0,0 +1,161 @@ +"use client"; + +import React from "react"; +import { DriverInfo, DriverManagementConfig } from "../types"; +import { getStatusColor, getStatusLabel, formatTime, COLUMN_LABELS } from "./driverUtils"; +import { Progress } from "@/components/ui/progress"; + +interface DriverListViewProps { + drivers: DriverInfo[]; + config: DriverManagementConfig; + isCompact?: boolean; // ์ž‘์€ ํฌ๊ธฐ (2x2 ๋“ฑ) +} + +export function DriverListView({ drivers, config, isCompact = false }: DriverListViewProps) { + const { visibleColumns } = config; + + // ์ปดํŒฉํŠธ ๋ชจ๋“œ: ์š”์•ฝ ์ •๋ณด๋งŒ ํ‘œ์‹œ + if (isCompact) { + const stats = { + driving: drivers.filter((d) => d.status === "driving").length, + standby: drivers.filter((d) => d.status === "standby").length, + resting: drivers.filter((d) => d.status === "resting").length, + maintenance: drivers.filter((d) => d.status === "maintenance").length, + }; + + return ( +
+
+
{drivers.length}
+
์ „์ฒด ๊ธฐ์‚ฌ
+
+
+
+
{stats.driving}
+
์šดํ–‰์ค‘
+
+
+
{stats.standby}
+
๋Œ€๊ธฐ์ค‘
+
+
+
{stats.resting}
+
ํœด์‹์ค‘
+
+
+
{stats.maintenance}
+
์ ๊ฒ€์ค‘
+
+
+
+ ); + } + + // ๋นˆ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + if (drivers.length === 0) { + return ( +
์กฐํšŒ๋œ ๊ธฐ์‚ฌ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
+ ); + } + + return ( +
+ + + + {visibleColumns.includes("status") && ( + + )} + {visibleColumns.includes("name") && ( + + )} + {visibleColumns.includes("vehicleNumber") && ( + + )} + {visibleColumns.includes("vehicleType") && ( + + )} + {visibleColumns.includes("departure") && ( + + )} + {visibleColumns.includes("destination") && ( + + )} + {visibleColumns.includes("departureTime") && ( + + )} + {visibleColumns.includes("estimatedArrival") && ( + + )} + {visibleColumns.includes("phone") && ( + + )} + {visibleColumns.includes("progress") && ( + + )} + + + + {drivers.map((driver) => { + const statusColors = getStatusColor(driver.status); + return ( + + {visibleColumns.includes("status") && ( + + )} + {visibleColumns.includes("name") && ( + + )} + {visibleColumns.includes("vehicleNumber") && ( + + )} + {visibleColumns.includes("vehicleType") && ( + + )} + {visibleColumns.includes("departure") && ( + + )} + {visibleColumns.includes("destination") && ( + + )} + {visibleColumns.includes("departureTime") && ( + + )} + {visibleColumns.includes("estimatedArrival") && ( + + )} + {visibleColumns.includes("phone") && ( + + )} + {visibleColumns.includes("progress") && ( + + )} + + ); + })} + +
{COLUMN_LABELS.status}{COLUMN_LABELS.name}{COLUMN_LABELS.vehicleNumber}{COLUMN_LABELS.vehicleType}{COLUMN_LABELS.departure}{COLUMN_LABELS.destination}{COLUMN_LABELS.departureTime} + {COLUMN_LABELS.estimatedArrival} + {COLUMN_LABELS.phone}{COLUMN_LABELS.progress}
+ + {getStatusLabel(driver.status)} + + {driver.name}{driver.vehicleNumber}{driver.vehicleType} + {driver.departure || -} + + {driver.destination || -} + {formatTime(driver.departureTime)}{formatTime(driver.estimatedArrival)}{driver.phone} + {driver.progress !== undefined ? ( +
+ + {driver.progress}% +
+ ) : ( + - + )} +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx b/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx new file mode 100644 index 00000000..a77dfda5 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useState } from "react"; +import { DriverManagementConfig } from "../types"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Card } from "@/components/ui/card"; +import { COLUMN_LABELS, DEFAULT_VISIBLE_COLUMNS } from "./driverUtils"; + +interface DriverManagementSettingsProps { + config: DriverManagementConfig; + onSave: (config: DriverManagementConfig) => void; + onClose: () => void; +} + +export function DriverManagementSettings({ config, onSave, onClose }: DriverManagementSettingsProps) { + const [localConfig, setLocalConfig] = useState(config); + + const handleSave = () => { + onSave(localConfig); + }; + + // ์ปฌ๋Ÿผ ํ† ๊ธ€ + const toggleColumn = (column: string) => { + const newColumns = localConfig.visibleColumns.includes(column) + ? localConfig.visibleColumns.filter((c) => c !== column) + : [...localConfig.visibleColumns, column]; + setLocalConfig({ ...localConfig, visibleColumns: newColumns }); + }; + + return ( +
+
+ {/* ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ */} +
+ + +
+ + {/* ์ •๋ ฌ ์„ค์ • */} +
+ +
+ + + +
+
+ + {/* ํ…Œ๋งˆ ์„ค์ • */} +
+ +
+ + + +
+ + {/* ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ */} + {localConfig.theme === "custom" && ( + + +
+ setLocalConfig({ ...localConfig, customColor: e.target.value })} + className="h-12 w-20 cursor-pointer" + /> +
+ setLocalConfig({ ...localConfig, customColor: e.target.value })} + placeholder="#3b82f6" + className="font-mono" + /> +

ํ…Œ์ด๋ธ” ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค

+
+
+
+ )} +
+ + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ */} +
+
+ + +
+
+ {Object.entries(COLUMN_LABELS).map(([key, label]) => ( + toggleColumn(key)} + > +
+ + toggleColumn(key)} + /> +
+
+ ))} +
+
+
+ + {/* ํ‘ธํ„ฐ - ๊ณ ์ • */} +
+ + +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/widgets/DriverManagementWidget.tsx b/frontend/components/admin/dashboard/widgets/DriverManagementWidget.tsx new file mode 100644 index 00000000..60d5c615 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/DriverManagementWidget.tsx @@ -0,0 +1,159 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { DashboardElement, DriverManagementConfig, DriverInfo } from "../types"; +import { DriverListView } from "./DriverListView"; +import { DriverManagementSettings } from "./DriverManagementSettings"; +import { MOCK_DRIVERS } from "./driverMockData"; +import { filterDrivers, sortDrivers, DEFAULT_VISIBLE_COLUMNS } from "./driverUtils"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Settings, Search, RefreshCw } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface DriverManagementWidgetProps { + element: DashboardElement; + onConfigUpdate?: (config: DriverManagementConfig) => void; +} + +export function DriverManagementWidget({ element, onConfigUpdate }: DriverManagementWidgetProps) { + const [drivers, setDrivers] = useState(MOCK_DRIVERS); + const [searchTerm, setSearchTerm] = useState(""); + const [settingsOpen, setSettingsOpen] = useState(false); + const [lastRefresh, setLastRefresh] = useState(new Date()); + + // ๊ธฐ๋ณธ ์„ค์ • + const config = element.driverManagementConfig || { + viewType: "list", + autoRefreshInterval: 30, + visibleColumns: DEFAULT_VISIBLE_COLUMNS, + theme: "light", + statusFilter: "all", + sortBy: "name", + sortOrder: "asc", + }; + + // ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ + useEffect(() => { + if (config.autoRefreshInterval <= 0) return; + + const interval = setInterval(() => { + // ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” API ํ˜ธ์ถœ + setDrivers(MOCK_DRIVERS); + setLastRefresh(new Date()); + }, config.autoRefreshInterval * 1000); + + return () => clearInterval(interval); + }, [config.autoRefreshInterval]); + + // ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ + const handleRefresh = () => { + setDrivers(MOCK_DRIVERS); + setLastRefresh(new Date()); + }; + + // ์„ค์ • ์ €์žฅ + const handleSaveSettings = (newConfig: DriverManagementConfig) => { + onConfigUpdate?.(newConfig); + setSettingsOpen(false); + }; + + // ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ + const filteredDrivers = sortDrivers( + filterDrivers(drivers, config.statusFilter, searchTerm), + config.sortBy, + config.sortOrder, + ); + + // ์ปดํŒฉํŠธ ๋ชจ๋“œ ํŒ๋‹จ (์œ„์ ฏ ํฌ๊ธฐ๊ฐ€ ์ž‘์„ ๋•Œ) + const isCompact = element.size.width < 400 || element.size.height < 300; + + return ( +
+ {/* ํ—ค๋” - ์ปดํŒฉํŠธ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ */} + {!isCompact && ( +
+
+ {/* ๊ฒ€์ƒ‰ */} +
+ + setSearchTerm(e.target.value)} + className="h-8 pl-8 text-xs" + /> +
+ + {/* ์ƒํƒœ ํ•„ํ„ฐ */} + + + {/* ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ */} + + + {/* ์„ค์ • ๋ฒ„ํŠผ */} + + + + + + setSettingsOpen(false)} + /> + + +
+ + {/* ํ†ต๊ณ„ ์ •๋ณด */} +
+ + ์ „์ฒด {filteredDrivers.length}๋ช… + + | + + ์šดํ–‰์ค‘{" "} + + {filteredDrivers.filter((d) => d.status === "driving").length} + + ๋ช… + + | + ์ตœ๊ทผ ์—…๋ฐ์ดํŠธ: {lastRefresh.toLocaleTimeString("ko-KR")} +
+
+ )} + + {/* ๋ฆฌ์ŠคํŠธ ๋ทฐ */} +
+ +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/widgets/driverMockData.ts b/frontend/components/admin/dashboard/widgets/driverMockData.ts new file mode 100644 index 00000000..85271e16 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/driverMockData.ts @@ -0,0 +1,181 @@ +import { DriverInfo } from "../types"; + +/** + * ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ๋ชฉ์—… ๋ฐ์ดํ„ฐ + * ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” REST API๋กœ ๋Œ€์ฒด๋จ + */ +export const MOCK_DRIVERS: DriverInfo[] = [ + { + id: "DRV001", + name: "ํ™๊ธธ๋™", + vehicleNumber: "12๊ฐ€ 3456", + vehicleType: "1ํ†ค ํŠธ๋Ÿญ", + phone: "010-1234-5678", + status: "driving", + departure: "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + destination: "๊ฒฝ๊ธฐ๋„ ์„ฑ๋‚จ์‹œ", + departureTime: "2025-10-14T09:00:00", + estimatedArrival: "2025-10-14T11:30:00", + progress: 65, + }, + { + id: "DRV002", + name: "๊น€์ฒ ์ˆ˜", + vehicleNumber: "34๋‚˜ 7890", + vehicleType: "2.5ํ†ค ํŠธ๋Ÿญ", + phone: "010-2345-6789", + status: "standby", + }, + { + id: "DRV003", + name: "์ด์˜ํฌ", + vehicleNumber: "56๋‹ค 1234", + vehicleType: "5ํ†ค ํŠธ๋Ÿญ", + phone: "010-3456-7890", + status: "driving", + departure: "์ธ์ฒœ๊ด‘์—ญ์‹œ", + destination: "์ถฉ์ฒญ๋‚จ๋„ ์ฒœ์•ˆ์‹œ", + departureTime: "2025-10-14T08:30:00", + estimatedArrival: "2025-10-14T10:00:00", + progress: 85, + }, + { + id: "DRV004", + name: "๋ฐ•๋ฏผ์ˆ˜", + vehicleNumber: "78๋ผ 5678", + vehicleType: "์นด๊ณ ", + phone: "010-4567-8901", + status: "resting", + }, + { + id: "DRV005", + name: "์ •์ˆ˜์ง„", + vehicleNumber: "90๋งˆ 9012", + vehicleType: "๋ƒ‰๋™์ฐจ", + phone: "010-5678-9012", + status: "maintenance", + }, + { + id: "DRV006", + name: "์ตœ๋™์šฑ", + vehicleNumber: "11์•„ 3344", + vehicleType: "1ํ†ค ํŠธ๋Ÿญ", + phone: "010-6789-0123", + status: "driving", + departure: "๋ถ€์‚ฐ๊ด‘์—ญ์‹œ", + destination: "์šธ์‚ฐ๊ด‘์—ญ์‹œ", + departureTime: "2025-10-14T07:45:00", + estimatedArrival: "2025-10-14T09:15:00", + progress: 92, + }, + { + id: "DRV007", + name: "๊ฐ•๋ฏธ์„ ", + vehicleNumber: "22์ž 5566", + vehicleType: "ํƒ‘์ฐจ", + phone: "010-7890-1234", + status: "standby", + }, + { + id: "DRV008", + name: "์œค์„ฑํ˜ธ", + vehicleNumber: "33์ฐจ 7788", + vehicleType: "2.5ํ†ค ํŠธ๋Ÿญ", + phone: "010-8901-2345", + status: "driving", + departure: "๋Œ€์ „๊ด‘์—ญ์‹œ", + destination: "์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ", + departureTime: "2025-10-14T10:20:00", + estimatedArrival: "2025-10-14T11:00:00", + progress: 45, + }, + { + id: "DRV009", + name: "์žฅํ˜œ์ง„", + vehicleNumber: "44์นด 9900", + vehicleType: "๋ƒ‰๋™์ฐจ", + phone: "010-9012-3456", + status: "resting", + }, + { + id: "DRV010", + name: "์ž„ํƒœ์–‘", + vehicleNumber: "55ํƒ€ 1122", + vehicleType: "5ํ†ค ํŠธ๋Ÿญ", + phone: "010-0123-4567", + status: "driving", + departure: "๊ด‘์ฃผ๊ด‘์—ญ์‹œ", + destination: "์ „๋ผ๋‚จ๋„ ๋ชฉํฌ์‹œ", + departureTime: "2025-10-14T06:30:00", + estimatedArrival: "2025-10-14T08:45:00", + progress: 78, + }, + { + id: "DRV011", + name: "์˜ค์ค€์„", + vehicleNumber: "66ํŒŒ 3344", + vehicleType: "์นด๊ณ ", + phone: "010-1111-2222", + status: "standby", + }, + { + id: "DRV012", + name: "ํ•œ์†Œํฌ", + vehicleNumber: "77ํ•˜ 5566", + vehicleType: "1ํ†ค ํŠธ๋Ÿญ", + phone: "010-2222-3333", + status: "maintenance", + }, + { + id: "DRV013", + name: "์†ก๋ฏผ์žฌ", + vehicleNumber: "88๊ฑฐ 7788", + vehicleType: "ํƒ‘์ฐจ", + phone: "010-3333-4444", + status: "driving", + departure: "๊ฒฝ๊ธฐ๋„ ์ˆ˜์›์‹œ", + destination: "๊ฒฝ๊ธฐ๋„ ํ‰ํƒ์‹œ", + departureTime: "2025-10-14T09:50:00", + estimatedArrival: "2025-10-14T11:20:00", + progress: 38, + }, + { + id: "DRV014", + name: "๋ฐฐ์ˆ˜์ง€", + vehicleNumber: "99๋„ˆ 9900", + vehicleType: "2.5ํ†ค ํŠธ๋Ÿญ", + phone: "010-4444-5555", + status: "driving", + departure: "๊ฐ•์›๋„ ์ถ˜์ฒœ์‹œ", + destination: "๊ฐ•์›๋„ ์›์ฃผ์‹œ", + departureTime: "2025-10-14T08:00:00", + estimatedArrival: "2025-10-14T09:30:00", + progress: 72, + }, + { + id: "DRV015", + name: "์‹ ๋™์—ฝ", + vehicleNumber: "00๋” 1122", + vehicleType: "5ํ†ค ํŠธ๋Ÿญ", + phone: "010-5555-6666", + status: "standby", + }, +]; + +/** + * ์ฐจ๋Ÿ‰ ์œ ํ˜• ๋ชฉ๋ก + */ +export const VEHICLE_TYPES = ["1ํ†ค ํŠธ๋Ÿญ", "2.5ํ†ค ํŠธ๋Ÿญ", "5ํ†ค ํŠธ๋Ÿญ", "์นด๊ณ ", "ํƒ‘์ฐจ", "๋ƒ‰๋™์ฐจ"]; + +/** + * ์šดํ–‰ ์ƒํƒœ๋ณ„ ํ†ต๊ณ„ ๊ณ„์‚ฐ + */ +export function getDriverStatistics(drivers: DriverInfo[]) { + return { + total: drivers.length, + driving: drivers.filter((d) => d.status === "driving").length, + standby: drivers.filter((d) => d.status === "standby").length, + resting: drivers.filter((d) => d.status === "resting").length, + maintenance: drivers.filter((d) => d.status === "maintenance").length, + }; +} diff --git a/frontend/components/admin/dashboard/widgets/driverUtils.ts b/frontend/components/admin/dashboard/widgets/driverUtils.ts new file mode 100644 index 00000000..bd2ddbd3 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/driverUtils.ts @@ -0,0 +1,256 @@ +import { DriverInfo, DriverManagementConfig } from "../types"; + +/** + * ์šดํ–‰ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ ๋ฐ˜ํ™˜ + */ +export function getStatusColor(status: DriverInfo["status"]) { + switch (status) { + case "driving": + return { + bg: "bg-green-100", + text: "text-green-800", + border: "border-green-300", + badge: "bg-green-500", + }; + case "standby": + return { + bg: "bg-gray-100", + text: "text-gray-800", + border: "border-gray-300", + badge: "bg-gray-500", + }; + case "resting": + return { + bg: "bg-orange-100", + text: "text-orange-800", + border: "border-orange-300", + badge: "bg-orange-500", + }; + case "maintenance": + return { + bg: "bg-red-100", + text: "text-red-800", + border: "border-red-300", + badge: "bg-red-500", + }; + default: + return { + bg: "bg-gray-100", + text: "text-gray-800", + border: "border-gray-300", + badge: "bg-gray-500", + }; + } +} + +/** + * ์šดํ–‰ ์ƒํƒœ ํ•œ๊ธ€ ๋ณ€ํ™˜ + */ +export function getStatusLabel(status: DriverInfo["status"]) { + switch (status) { + case "driving": + return "์šดํ–‰์ค‘"; + case "standby": + return "๋Œ€๊ธฐ์ค‘"; + case "resting": + return "ํœด์‹์ค‘"; + case "maintenance": + return "์ ๊ฒ€์ค‘"; + default: + return "์•Œ ์ˆ˜ ์—†์Œ"; + } +} + +/** + * ์‹œ๊ฐ„ ํฌ๋งทํŒ… (HH:MM) + */ +export function formatTime(dateString?: string): string { + if (!dateString) return "-"; + const date = new Date(dateString); + return date.toLocaleTimeString("ko-KR", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} + +/** + * ๋‚ ์งœ ์‹œ๊ฐ„ ํฌ๋งทํŒ… (MM/DD HH:MM) + */ +export function formatDateTime(dateString?: string): string { + if (!dateString) return "-"; + const date = new Date(dateString); + return date.toLocaleString("ko-KR", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} + +/** + * ์šดํ–‰ ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ (์‹ค์ œ๋กœ๋Š” GPS ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜) + */ +export function calculateProgress(driver: DriverInfo): number { + if (!driver.departureTime || !driver.estimatedArrival) return 0; + + const now = new Date(); + const departure = new Date(driver.departureTime); + const arrival = new Date(driver.estimatedArrival); + + const totalTime = arrival.getTime() - departure.getTime(); + const elapsedTime = now.getTime() - departure.getTime(); + + const progress = Math.min(100, Math.max(0, (elapsedTime / totalTime) * 100)); + return Math.round(progress); +} + +/** + * ๊ธฐ์‚ฌ ํ•„ํ„ฐ๋ง + */ +export function filterDrivers( + drivers: DriverInfo[], + statusFilter: DriverManagementConfig["statusFilter"], + searchTerm: string, +): DriverInfo[] { + let filtered = drivers; + + // ์ƒํƒœ ํ•„ํ„ฐ + if (statusFilter !== "all") { + filtered = filtered.filter((driver) => driver.status === statusFilter); + } + + // ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ + if (searchTerm.trim()) { + const term = searchTerm.toLowerCase(); + filtered = filtered.filter( + (driver) => + driver.name.toLowerCase().includes(term) || + driver.vehicleNumber.toLowerCase().includes(term) || + driver.phone.includes(term), + ); + } + + return filtered; +} + +/** + * ๊ธฐ์‚ฌ ์ •๋ ฌ + */ +export function sortDrivers( + drivers: DriverInfo[], + sortBy: DriverManagementConfig["sortBy"], + sortOrder: DriverManagementConfig["sortOrder"], +): DriverInfo[] { + const sorted = [...drivers]; + + sorted.sort((a, b) => { + let compareResult = 0; + + switch (sortBy) { + case "name": + compareResult = a.name.localeCompare(b.name, "ko-KR"); + break; + case "vehicleNumber": + compareResult = a.vehicleNumber.localeCompare(b.vehicleNumber); + break; + case "status": + const statusOrder = { driving: 0, resting: 1, standby: 2, maintenance: 3 }; + compareResult = statusOrder[a.status] - statusOrder[b.status]; + break; + case "departureTime": + const timeA = a.departureTime ? new Date(a.departureTime).getTime() : 0; + const timeB = b.departureTime ? new Date(b.departureTime).getTime() : 0; + compareResult = timeA - timeB; + break; + } + + return sortOrder === "asc" ? compareResult : -compareResult; + }); + + return sorted; +} + +/** + * ํ…Œ๋งˆ๋ณ„ ์ƒ‰์ƒ ๋ฐ˜ํ™˜ + */ +export function getThemeColors(theme: string, customColor?: string) { + if (theme === "custom" && customColor) { + const lighterColor = adjustColor(customColor, 40); + const darkerColor = adjustColor(customColor, -40); + + return { + background: lighterColor, + text: darkerColor, + border: customColor, + hover: customColor, + }; + } + + if (theme === "dark") { + return { + background: "#1f2937", + text: "#f3f4f6", + border: "#374151", + hover: "#374151", + }; + } + + // light theme (default) + return { + background: "#ffffff", + text: "#1f2937", + border: "#e5e7eb", + hover: "#f3f4f6", + }; +} + +/** + * ์ƒ‰์ƒ ๋ฐ๊ธฐ ์กฐ์ • + */ +function adjustColor(color: string, amount: number): string { + const clamp = (num: number) => Math.min(255, Math.max(0, num)); + + const hex = color.replace("#", ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + const newR = clamp(r + amount); + const newG = clamp(g + amount); + const newB = clamp(b + amount); + + return `#${newR.toString(16).padStart(2, "0")}${newG.toString(16).padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`; +} + +/** + * ๊ธฐ๋ณธ ํ‘œ์‹œ ์ปฌ๋Ÿผ ๋ชฉ๋ก + */ +export const DEFAULT_VISIBLE_COLUMNS = [ + "status", + "name", + "vehicleNumber", + "vehicleType", + "departure", + "destination", + "departureTime", + "estimatedArrival", + "phone", +]; + +/** + * ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋งคํ•‘ + */ +export const COLUMN_LABELS: Record = { + status: "์ƒํƒœ", + name: "๊ธฐ์‚ฌ๋ช…", + vehicleNumber: "์ฐจ๋Ÿ‰๋ฒˆํ˜ธ", + vehicleType: "์ฐจ๋Ÿ‰์œ ํ˜•", + departure: "์ถœ๋ฐœ์ง€", + destination: "๋ชฉ์ ์ง€", + departureTime: "์ถœ๋ฐœ์‹œ๊ฐ„", + estimatedArrival: "๋„์ฐฉ์˜ˆ์ •", + phone: "์—ฐ๋ฝ์ฒ˜", + progress: "์ง„ํ–‰๋ฅ ", +}; -- 2.43.0 From d149f0baaa9e3e4dbfb24207c6936f8c840a7b24 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 11:46:14 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=EA=B8=B0=EC=82=AC=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=EC=97=90=EC=84=9C=20=ED=85=8C=EB=A7=88=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/DriverManagementSettings.tsx | 62 +------------------ 1 file changed, 3 insertions(+), 59 deletions(-) diff --git a/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx b/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx index a77dfda5..0f09286e 100644 --- a/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx +++ b/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx @@ -44,7 +44,7 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana - + ์‚ฌ์šฉ ์•ˆ ํ•จ 10์ดˆ๋งˆ๋‹ค 30์ดˆ๋งˆ๋‹ค @@ -67,7 +67,7 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana - + ๊ธฐ์‚ฌ๋ช… ์ฐจ๋Ÿ‰๋ฒˆํ˜ธ ์šดํ–‰์ƒํƒœ @@ -84,7 +84,7 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana - + ์˜ค๋ฆ„์ฐจ์ˆœ ๋‚ด๋ฆผ์ฐจ์ˆœ @@ -92,62 +92,6 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana
- {/* ํ…Œ๋งˆ ์„ค์ • */} -
- -
- - - -
- - {/* ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ */} - {localConfig.theme === "custom" && ( - - -
- setLocalConfig({ ...localConfig, customColor: e.target.value })} - className="h-12 w-20 cursor-pointer" - /> -
- setLocalConfig({ ...localConfig, customColor: e.target.value })} - placeholder="#3b82f6" - className="font-mono" - /> -

ํ…Œ์ด๋ธ” ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค

-
-
-
- )} -
- {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ */}
-- 2.43.0 From 2b104b8455f4723ee156dfb20cc23fe826d722f9 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 11:53:40 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/admin/dashboard/widgets/DriverListView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/components/admin/dashboard/widgets/DriverListView.tsx b/frontend/components/admin/dashboard/widgets/DriverListView.tsx index f5df6944..cddbe6c6 100644 --- a/frontend/components/admin/dashboard/widgets/DriverListView.tsx +++ b/frontend/components/admin/dashboard/widgets/DriverListView.tsx @@ -1,6 +1,5 @@ "use client"; -import React from "react"; import { DriverInfo, DriverManagementConfig } from "../types"; import { getStatusColor, getStatusLabel, formatTime, COLUMN_LABELS } from "./driverUtils"; import { Progress } from "@/components/ui/progress"; -- 2.43.0 From 5cd5ad6c49ded17a39f19d018e14a59d8c8c4f10 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 13:20:17 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EB=84=88?= =?UTF-8?q?=EB=B9=84=20=EC=B4=88=EA=B3=BC=ED=95=98=EB=8A=94=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EB=A7=89=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 104 +++++------------- .../admin/dashboard/DashboardCanvas.tsx | 8 +- 2 files changed, 31 insertions(+), 81 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 3decd573..43ce5163 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -131,9 +131,13 @@ export function CanvasElement({ const deltaY = e.clientY - dragStart.y; // ์ž„์‹œ ์œ„์น˜ ๊ณ„์‚ฐ (์Šค๋ƒ… ์•ˆ ๋จ) - const rawX = Math.max(0, dragStart.elementX + deltaX); + let rawX = Math.max(0, dragStart.elementX + deltaX); const rawY = Math.max(0, dragStart.elementY + deltaY); + // X ์ขŒํ‘œ๊ฐ€ ์บ”๋ฒ„์Šค ๋„ˆ๋น„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ œํ•œ + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + rawX = Math.min(rawX, maxX); + setTempPosition({ x: rawX, y: rawY }); } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; @@ -173,21 +177,29 @@ export function CanvasElement({ break; } + // ๊ฐ€๋กœ ๋„ˆ๋น„๊ฐ€ ์บ”๋ฒ„์Šค๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ œํ•œ + const maxWidth = GRID_CONFIG.CANVAS_WIDTH - newX; + newWidth = Math.min(newWidth, maxWidth); + // ์ž„์‹œ ํฌ๊ธฐ/์œ„์น˜ ์ €์žฅ (์Šค๋ƒ… ์•ˆ ๋จ) setTempPosition({ x: Math.max(0, newX), y: Math.max(0, newY) }); setTempSize({ width: newWidth, height: newHeight }); } }, - [isDragging, isResizing, dragStart, resizeStart], + [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype], ); // ๋งˆ์šฐ์Šค ์—… ์ฒ˜๋ฆฌ (๊ทธ๋ฆฌ๋“œ ์Šค๋ƒ… ์ ์šฉ) const handleMouseUp = useCallback(() => { if (isDragging && tempPosition) { // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ์‹œ ๊ทธ๋ฆฌ๋“œ์— ์Šค๋ƒ… (๋™์  ์…€ ํฌ๊ธฐ ์‚ฌ์šฉ) - const snappedX = snapToGrid(tempPosition.x, cellSize); + let snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); + // X ์ขŒํ‘œ๊ฐ€ ์บ”๋ฒ„์Šค ๋„ˆ๋น„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ตœ์ข… ์ œํ•œ + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + snappedX = Math.min(snappedX, maxX); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, }); @@ -199,9 +211,13 @@ export function CanvasElement({ // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ข…๋ฃŒ ์‹œ ๊ทธ๋ฆฌ๋“œ์— ์Šค๋ƒ… (๋™์  ์…€ ํฌ๊ธฐ ์‚ฌ์šฉ) const snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); - const snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); + let snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); const snappedHeight = snapSizeToGrid(tempSize.height, 2, cellSize); + // ๊ฐ€๋กœ ๋„ˆ๋น„๊ฐ€ ์บ”๋ฒ„์Šค๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ตœ์ข… ์ œํ•œ + const maxWidth = GRID_CONFIG.CANVAS_WIDTH - snappedX; + snappedWidth = Math.min(snappedWidth, maxWidth); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, size: { width: snappedWidth, height: snappedHeight }, @@ -213,7 +229,7 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - }, [isDragging, isResizing, tempPosition, tempSize, element.id, onUpdate, cellSize]); + }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize]); // ์ „์—ญ ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ ๋“ฑ๋ก React.useEffect(() => { @@ -251,12 +267,11 @@ export function CanvasElement({ executionTime: 0, }); } catch (error) { - // console.error('โŒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์˜ค๋ฅ˜:', error); setChartData(null); } finally { setIsLoadingData(false); } - }, [element.dataSource?.query, element.type, element.subtype]); + }, [element.dataSource?.query, element.type]); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ๋ฐ ์ฟผ๋ฆฌ ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ useEffect(() => { @@ -372,7 +387,7 @@ export function CanvasElement({ ) : ( @@ -381,16 +396,12 @@ export function CanvasElement({ ) : element.type === "widget" && element.subtype === "weather" ? ( // ๋‚ ์”จ ์œ„์ ฏ ๋ Œ๋”๋ง
- +
) : element.type === "widget" && element.subtype === "exchange" ? ( // ํ™˜์œจ ์œ„์ ฏ ๋ Œ๋”๋ง
- +
) : element.type === "widget" && element.subtype === "clock" ? ( // ์‹œ๊ณ„ ์œ„์ ฏ ๋ Œ๋”๋ง @@ -487,68 +498,3 @@ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) { /> ); } - -/** - * ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ•จ์ˆ˜ (์‹ค์ œ API ํ˜ธ์ถœ ๋Œ€์‹  ์‚ฌ์šฉ) - */ -function generateSampleData(query: string, chartType: string): QueryResult { - // ์ฟผ๋ฆฌ์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœํ•˜์—ฌ ์ ์ ˆํ•œ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - const isMonthly = query.toLowerCase().includes("month"); - const isSales = query.toLowerCase().includes("sales") || query.toLowerCase().includes("๋งค์ถœ"); - const isUsers = query.toLowerCase().includes("users") || query.toLowerCase().includes("์‚ฌ์šฉ์ž"); - const isProducts = query.toLowerCase().includes("product") || query.toLowerCase().includes("์ƒํ’ˆ"); - - let columns: string[]; - let rows: Record[]; - - if (isMonthly && isSales) { - // ์›”๋ณ„ ๋งค์ถœ ๋ฐ์ดํ„ฐ - columns = ["month", "sales", "order_count"]; - rows = [ - { month: "2024-01", sales: 1200000, order_count: 45 }, - { month: "2024-02", sales: 1350000, order_count: 52 }, - { month: "2024-03", sales: 1180000, order_count: 41 }, - { month: "2024-04", sales: 1420000, order_count: 58 }, - { month: "2024-05", sales: 1680000, order_count: 67 }, - { month: "2024-06", sales: 1540000, order_count: 61 }, - ]; - } else if (isUsers) { - // ์‚ฌ์šฉ์ž ๊ฐ€์ž… ์ถ”์ด - columns = ["week", "new_users"]; - rows = [ - { week: "2024-W10", new_users: 23 }, - { week: "2024-W11", new_users: 31 }, - { week: "2024-W12", new_users: 28 }, - { week: "2024-W13", new_users: 35 }, - { week: "2024-W14", new_users: 42 }, - { week: "2024-W15", new_users: 38 }, - ]; - } else if (isProducts) { - // ์ƒํ’ˆ๋ณ„ ํŒ๋งค๋Ÿ‰ - columns = ["product_name", "total_sold", "revenue"]; - rows = [ - { product_name: "์Šค๋งˆํŠธํฐ", total_sold: 156, revenue: 234000000 }, - { product_name: "๋…ธํŠธ๋ถ", total_sold: 89, revenue: 178000000 }, - { product_name: "ํƒœ๋ธ”๋ฆฟ", total_sold: 134, revenue: 67000000 }, - { product_name: "์ด์–ดํฐ", total_sold: 267, revenue: 26700000 }, - { product_name: "์Šค๋งˆํŠธ์›Œ์น˜", total_sold: 98, revenue: 49000000 }, - ]; - } else { - // ๊ธฐ๋ณธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ - columns = ["category", "value", "count"]; - rows = [ - { category: "A", value: 100, count: 10 }, - { category: "B", value: 150, count: 15 }, - { category: "C", value: 120, count: 12 }, - { category: "D", value: 180, count: 18 }, - { category: "E", value: 90, count: 9 }, - ]; - } - - return { - columns, - rows, - totalRows: rows.length, - executionTime: Math.floor(Math.random() * 100) + 50, // 50-150ms - }; -} diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index d8b7007e..1a4ec333 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -33,7 +33,7 @@ export const DashboardCanvas = forwardRef( onRemoveElement, onSelectElement, onConfigureElement, - backgroundColor = '#f9fafb', + backgroundColor = "#f9fafb", }, ref, ) => { @@ -72,9 +72,13 @@ export const DashboardCanvas = forwardRef( const rawY = e.clientY - rect.top + (ref.current?.scrollTop || 0); // ๊ทธ๋ฆฌ๋“œ์— ์Šค๋ƒ… (๊ณ ์ • ์…€ ํฌ๊ธฐ ์‚ฌ์šฉ) - const snappedX = snapToGrid(rawX, GRID_CONFIG.CELL_SIZE); + let snappedX = snapToGrid(rawX, GRID_CONFIG.CELL_SIZE); const snappedY = snapToGrid(rawY, GRID_CONFIG.CELL_SIZE); + // X ์ขŒํ‘œ๊ฐ€ ์บ”๋ฒ„์Šค ๋„ˆ๋น„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ œํ•œ + const maxX = GRID_CONFIG.CANVAS_WIDTH - GRID_CONFIG.CELL_SIZE * 2; // ์ตœ์†Œ 2์นธ ๋„ˆ๋น„ ๋ณด์žฅ + snappedX = Math.max(0, Math.min(snappedX, maxX)); + onCreateElement(dragData.type, dragData.subtype, snappedX, snappedY); } catch (error) { // console.error('๋“œ๋กญ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', error); -- 2.43.0