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..e127be43 --- /dev/null +++ b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md @@ -0,0 +1,228 @@ +# ๐Ÿ“… ๋‹ฌ๋ ฅ ์œ„์ ฏ ๊ตฌํ˜„ ๊ณ„ํš + +## ๊ฐœ์š” + +๋Œ€์‹œ๋ณด๋“œ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ฌ๋ ฅ ์œ„์ ฏ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋‚ ์งœ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ผ์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ๋‹ฌ๋ ฅ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ธฐ๋Šฅ + +### 1. ๋‹ฌ๋ ฅ ๋ทฐ ํƒ€์ž… + +- **์›”๊ฐ„ ๋ทฐ**: ํ•œ ๋‹ฌ ์ „์ฒด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋ณธ ๋ทฐ +- **์ฃผ๊ฐ„ ๋ทฐ**: ์ผ์ฃผ์ผ์„ ์„ธ๋กœ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ทฐ +- **์ผ๊ฐ„ ๋ทฐ**: ํ•˜๋ฃจ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ผ์ • ๋ทฐ + +### 2. ๋‹ฌ๋ ฅ ์„ค์ • + +- **์‹œ์ž‘ ์š”์ผ**: ์›”์š”์ผ ์‹œ์ž‘ / ์ผ์š”์ผ ์‹œ์ž‘ ์„ ํƒ +- **์ฃผ๋ง ๊ฐ•์กฐ**: ์ฃผ๋ง ์ƒ‰์ƒ ๋‹ค๋ฅด๊ฒŒ ํ‘œ์‹œ +- **์˜ค๋Š˜ ๋‚ ์งœ ๊ฐ•์กฐ**: ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ +- **๊ณตํœด์ผ ํ‘œ์‹œ**: ํ•œ๊ตญ ๊ณตํœด์ผ ํ‘œ์‹œ (์„ ํƒ ์‚ฌํ•ญ) + +### 3. ํ…Œ๋งˆ ๋ฐ ์Šคํƒ€์ผ + +- **Light ํ…Œ๋งˆ**: ๋ฐ์€ ๋ฐฐ๊ฒฝ +- **Dark ํ…Œ๋งˆ**: ์–ด๋‘์šด ๋ฐฐ๊ฒฝ +- **์‚ฌ์šฉ์ž ์ง€์ •**: ์ปค์Šคํ…€ ์ƒ‰์ƒ ์„ ํƒ + +### 4. ์ผ์ • ๊ธฐ๋Šฅ (ํ–ฅํ›„ ํ™•์žฅ) + +- ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ ์ถ”๊ฐ€ +- ์ผ์ • ํ‘œ์‹œ (์™ธ๋ถ€ ์—ฐ๋™) + +## ๊ตฌํ˜„ ๋‹จ๊ณ„ + +### โœ… Step 1: ํƒ€์ž… ์ •์˜ + +- [x] `CalendarConfig` ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [x] `types.ts`์— ๋‹ฌ๋ ฅ ์„ค์ • ํƒ€์ž… ์ถ”๊ฐ€ +- [x] ์š”์†Œ ํƒ€์ž…์— 'calendar' subtype ์ถ”๊ฐ€ + +### โœ… Step 2: ๊ธฐ๋ณธ ๋‹ฌ๋ ฅ ์ปดํฌ๋„ŒํŠธ + +- [x] `CalendarWidget.tsx` - ๋ฉ”์ธ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ +- [x] `MonthView.tsx` - ์›”๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ +- [x] ๋‚ ์งœ ๊ณ„์‚ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (`calendarUtils.ts`) +- [ ] `WeekView.tsx` - ์ฃผ๊ฐ„ ๋‹ฌ๋ ฅ ๋ทฐ (ํ–ฅํ›„ ์ถ”๊ฐ€) + +### โœ… Step 3: ๋‹ฌ๋ ฅ ๋„ค๋น„๊ฒŒ์ด์…˜ + +- [x] ์ด์ „/๋‹ค์Œ ์›” ์ด๋™ ๋ฒ„ํŠผ +- [x] ์˜ค๋Š˜๋กœ ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ +- [ ] ์›”/์—ฐ๋„ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด (ํ–ฅํ›„ ์ถ”๊ฐ€) + +### โœ… Step 4: ์„ค์ • UI + +- [x] `CalendarSettings.tsx` - Popover ๋‚ด์žฅ ์„ค์ • ์ปดํฌ๋„ŒํŠธ +- [x] ๋ทฐ ํƒ€์ž… ์„ ํƒ (์›”๊ฐ„ - ํ˜„์žฌ ๊ตฌํ˜„) +- [x] ์‹œ์ž‘ ์š”์ผ ์„ค์ • +- [x] ํ…Œ๋งˆ ์„ ํƒ (light/dark/custom) +- [x] ํ‘œ์‹œ ์˜ต์…˜ (์ฃผ๋ง ๊ฐ•์กฐ, ๊ณตํœด์ผ, ์˜ค๋Š˜ ๊ฐ•์กฐ) + +### โœ… Step 5: ์Šคํƒ€์ผ๋ง + +- [x] ๋‹ฌ๋ ฅ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ +- [x] ๋‚ ์งœ ์…€ ๋””์ž์ธ +- [x] ์˜ค๋Š˜ ๋‚ ์งœ ํ•˜์ด๋ผ์ดํŠธ +- [x] ์ฃผ๋ง/ํ‰์ผ ๊ตฌ๋ถ„ +- [x] ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (ํฌ๊ธฐ๋ณ„ ์ตœ์ ํ™”) + +### โœ… Step 6: ํ†ตํ•ฉ + +- [x] `DashboardSidebar`์— ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ถ”๊ฐ€ +- [x] `CanvasElement`์—์„œ ๋‹ฌ๋ ฅ ์œ„์ ฏ ๋ Œ๋”๋ง +- [x] `DashboardDesigner`์— ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + +### โœ… Step 7: ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ + +- [x] ํ•œ๊ตญ ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ ์ •์˜ +- [x] ๊ณตํœด์ผ ํ‘œ์‹œ ๊ธฐ๋Šฅ +- [x] ๊ณตํœด์ผ ์ด๋ฆ„ ํˆดํŒ + +### โœ… Step 8: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” + +- [ ] ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ (์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ ํ•„์š”) +- [x] ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ๊ฒ€์ฆ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” (ํ•„์š”์‹œ) +- [ ] ์ ‘๊ทผ์„ฑ ๊ฐœ์„  (ํ•„์š”์‹œ) + +## ๊ธฐ์ˆ  ์Šคํƒ + +### 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 ํฌ๊ธฐ์—์„œ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•จ + +--- + +## ๊ตฌํ˜„ ์‹œ์ž‘ + +์ด์ œ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค! diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 293f1790..43ce5163 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -29,6 +29,10 @@ const VehicleMapWidget = dynamic(() => import("@/components/dashboard/widgets/Ve // ์‹œ๊ณ„ ์œ„์ ฏ ์ž„ํฌํŠธ import { ClockWidget } from "./widgets/ClockWidget"; +// ๋‹ฌ๋ ฅ ์œ„์ ฏ ์ž„ํฌํŠธ +import { CalendarWidget } from "./widgets/CalendarWidget"; +// ๊ธฐ์‚ฌ ๊ด€๋ฆฌ ์œ„์ ฏ ์ž„ํฌํŠธ +import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; interface CanvasElementProps { element: DashboardElement; @@ -127,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; @@ -140,46 +148,58 @@ 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; } + // ๊ฐ€๋กœ ๋„ˆ๋น„๊ฐ€ ์บ”๋ฒ„์Šค๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ œํ•œ + 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 }, }); @@ -191,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 }, @@ -205,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(() => { @@ -243,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(() => { @@ -291,6 +314,10 @@ 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"; + case "driver-management": + return "bg-gradient-to-br from-blue-400 to-indigo-600"; default: return "bg-gray-200"; } @@ -320,16 +347,20 @@ 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.subtype === "driver-management") + ) && ( + + )} {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+
+ + + + {/* ํ…Œ๋งˆ ์„ ํƒ */} +
+ +
+ {[ + { + 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/DriverListView.tsx b/frontend/components/admin/dashboard/widgets/DriverListView.tsx new file mode 100644 index 00000000..cddbe6c6 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/DriverListView.tsx @@ -0,0 +1,160 @@ +"use client"; + +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..0f09286e --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx @@ -0,0 +1,139 @@ +"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 ( +
+
+ {/* ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ */} +
+ + +
+ + {/* ์ •๋ ฌ ์„ค์ • */} +
+ +
+ + + +
+
+ + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ */} +
+
+ + +
+
+ {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/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 }; + } +} + 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: "์ง„ํ–‰๋ฅ ", +};