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")
+ ) && (
+
+ )}
{/* ์ญ์ ๋ฒํผ */}
@@ -365,16 +396,12 @@ export function CanvasElement({
) : element.type === "widget" && element.subtype === "weather" ? (
// ๋ ์จ ์์ ฏ ๋ ๋๋ง
-
+
) : element.type === "widget" && element.subtype === "exchange" ? (
// ํ์จ ์์ ฏ ๋ ๋๋ง
-
+
) : element.type === "widget" && element.subtype === "clock" ? (
// ์๊ณ ์์ ฏ ๋ ๋๋ง
@@ -396,6 +423,26 @@ export function CanvasElement({
+ ) : element.type === "widget" && element.subtype === "calendar" ? (
+ // ๋ฌ๋ ฅ ์์ ฏ ๋ ๋๋ง
+
+ {
+ onUpdate(element.id, { calendarConfig: newConfig });
+ }}
+ />
+
+ ) : element.type === "widget" && element.subtype === "driver-management" ? (
+ // ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ ๋ ๋๋ง
+
+ {
+ onUpdate(element.id, { driverManagementConfig: newConfig });
+ }}
+ />
+
) : (
// ๊ธฐํ ์์ ฏ ๋ ๋๋ง
);
}
-
-/**
- * ์ํ ๋ฐ์ดํฐ ์์ฑ ํจ์ (์ค์ 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/DRIVER_MANAGEMENT_WIDGET_PLAN.md b/frontend/components/admin/dashboard/DRIVER_MANAGEMENT_WIDGET_PLAN.md
new file mode 100644
index 00000000..e1643fd1
--- /dev/null
+++ b/frontend/components/admin/dashboard/DRIVER_MANAGEMENT_WIDGET_PLAN.md
@@ -0,0 +1,345 @@
+# ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ ๊ตฌํ ๊ณํ
+
+## ๊ฐ์
+
+๋์๋ณด๋์ ์ถ๊ฐํ ์ ์๋ ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ์ ๊ตฌํํฉ๋๋ค. ์ค์๊ฐ์ผ๋ก ๊ธฐ์ฌ์ ์ฐจ๋์ ์ดํ ์ํ๋ฅผ ํ์ธํ๊ณ ๊ด๋ฆฌํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
+
+## ์ฃผ์ ๊ธฐ๋ฅ
+
+### 1. ๊ธฐ์ฌ ์ ๋ณด ํ์
+
+- **์ฐจ๋ ๋ฒํธ**: ์) 12๊ฐ 3456
+- **๊ธฐ์ฌ ์ด๋ฆ**: ์) ํ๊ธธ๋
+- **์ถ๋ฐ์ง**: ์) ์์ธ์ ๊ฐ๋จ๊ตฌ
+- **๋ชฉ์ ์ง**: ์) ๊ฒฝ๊ธฐ๋ ์ฑ๋จ์
+- **์ฐจ๋ ์ ํ**: ์) 1ํค ํธ๋ญ, 2.5ํค ํธ๋ญ, 5ํค ํธ๋ญ, ์นด๊ณ , ํ์ฐจ, ๋๋์ฐจ ๋ฑ
+- **์ดํ ์ํ**: ๋๊ธฐ์ค, ์ดํ์ค, ํด์์ค, ์ ๊ฒ์ค
+- **์ฐ๋ฝ์ฒ**: ๊ธฐ์ฌ ์ ํ๋ฒํธ
+- **์ดํ ์์ ์๊ฐ**: ์ถ๋ฐ ์๊ฐ
+- **์์ ๋์ฐฉ ์๊ฐ**: ๋ชฉ์ ์ง ๋์ฐฉ ์์ ์๊ฐ
+
+### 2. ์ดํ ์ํ ๊ตฌ๋ถ
+
+- **๋๊ธฐ์ค** (ํ์): ์ถ๋ฐ์ง/๋ชฉ์ ์ง๊ฐ ์๋ ์ํ
+- **์ดํ์ค** (์ด๋ก์): ์ถ๋ฐ์ง/๋ชฉ์ ์ง๊ฐ ์๊ณ ์ดํ ์ค
+- **ํด์์ค** (์ฃผํฉ์): ํด๊ฒ ์ค
+- **์ ๊ฒ์ค** (๋นจ๊ฐ์): ์ฐจ๋ ์ ๊ฒ ๋๋ ์๋ฆฌ ์ค
+
+### 3. ๋ทฐ ํ์
+
+- **๋ฆฌ์คํธ ๋ทฐ**: ํ
์ด๋ธ ํ์์ผ๋ก ์ ์ฒด ๊ธฐ์ฌ ๋ชฉ๋ก ํ์
+- **๋งต ๋ทฐ** (ํฅํ ํ์ฅ): ์ง๋์ ๊ธฐ์ฌ ์์น ํ์
+
+### 4. ํํฐ๋ง ๋ฐ ๊ฒ์
+
+- **์ํ๋ณ ํํฐ**: ์ดํ์ค, ๋๊ธฐ์ค, ํด์์ค, ์ ๊ฒ์ค
+- **์ฐจ๋ ์ ํ๋ณ ํํฐ**: 1ํค, 2.5ํค, 5ํค ๋ฑ
+- **๊ฒ์**: ๊ธฐ์ฌ ์ด๋ฆ, ์ฐจ๋ ๋ฒํธ๋ก ๊ฒ์
+
+### 5. ์ ๋ ฌ ๊ธฐ๋ฅ
+
+- ๊ธฐ์ฌ ์ด๋ฆ์
+- ์ฐจ๋ ๋ฒํธ์
+- ์ถ๋ฐ ์๊ฐ์
+- ์ดํ ์ํ๋ณ
+
+### 6. ์ค์ ์ต์
+
+- **๋ทฐ ํ์
**: ๋ฆฌ์คํธ
+- **์๋ ์๋ก๊ณ ์นจ**: ์ค์๊ฐ ๋ฐ์ดํฐ ๊ฐฑ์ (10์ด, 30์ด, 1๋ถ)
+- **ํ์ ํญ๋ชฉ**: ์ฌ์ฉ์๊ฐ ์ํ๋ ์ปฌ๋ผ๋ง ํ์
+- **ํ
๋ง**: Light, Dark, ์ฌ์ฉ์ ์ง์
+
+## ๋ฐ์ดํฐ ๊ตฌ์กฐ
+
+```typescript
+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)
+}
+```
+
+## ๋ชฉ์
๋ฐ์ดํฐ
+
+```typescript
+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",
+ },
+];
+```
+
+## ๊ตฌํ ๋จ๊ณ
+
+### โ
Step 1: ํ์
์ ์
+
+- [x] `DriverManagementConfig` ์ธํฐํ์ด์ค ์ ์
+- [x] `DriverInfo` ์ธํฐํ์ด์ค ์ ์
+- [x] `types.ts`์ ๊ธฐ์ฌ ๊ด๋ฆฌ ์ค์ ํ์
์ถ๊ฐ
+- [x] ์์ ํ์
์ 'driver-management' subtype ์ถ๊ฐ
+
+### โ
Step 2: ๋ชฉ์
๋ฐ์ดํฐ ์์ฑ
+
+- [x] `driverMockData.ts` - ๊ธฐ์ฌ ๋ชฉ์
๋ฐ์ดํฐ ์์ฑ
+- [x] ๋ค์ํ ์ดํ ์ํ์ ์ํ ๋ฐ์ดํฐ (15๊ฐ)
+- [x] ์ฐจ๋ ์ ํ๋ณ ์ํ ๋ฐ์ดํฐ
+
+### โ
Step 3: ์ ํธ๋ฆฌํฐ ํจ์
+
+- [x] `driverUtils.ts` - ๊ธฐ์ฌ ๊ด๋ฆฌ ์ ํธ๋ฆฌํฐ ํจ์
+- [x] ์ดํ ์ํ๋ณ ์์ ๋ฐํ
+- [x] ์งํ๋ฅ ๊ณ์ฐ
+- [x] ์๊ฐ ํฌ๋งทํ
+- [x] ํํฐ๋ง/์ ๋ ฌ ๋ก์ง
+
+### โ
Step 4: ๋ฆฌ์คํธ ๋ทฐ ์ปดํฌ๋ํธ
+
+- [x] `DriverListView.tsx` - ํ
์ด๋ธ ํ์ ๋ฆฌ์คํธ ๋ทฐ
+- [x] ์ํ๋ณ ์์ ๊ตฌ๋ถ
+- [x] ์ ๋ ฌ ๊ธฐ๋ฅ (์ ํธ๋ฆฌํฐ์์ ์ฒ๋ฆฌ)
+- [x] ๋ฐ์ํ ํ
์ด๋ธ ๋์์ธ (์ปดํฉํธ ๋ชจ๋ ํฌํจ)
+
+### โ
Step 5: ์นด๋ ๋ทฐ ์ปดํฌ๋ํธ
+
+- [x] ์นด๋ ๋ทฐ๋ ํ์ฌ ๊ตฌํํ์ง ์์ (๋ฆฌ์คํธ ๋ทฐ๋ง ์ฌ์ฉ)
+- [ ] `DriverCardView.tsx` - ํฅํ ์ถ๊ฐ ์์
+
+### โ
Step 6: ๋ฉ์ธ ์์ ฏ ์ปดํฌ๋ํธ
+
+- [x] `DriverManagementWidget.tsx` - ๋ฉ์ธ ์์ ฏ
+- [x] ๋ฆฌ์คํธ ๋ทฐ ํ์
+- [x] ํํฐ๋ง UI (์ํ๋ณ)
+- [x] ๊ฒ์ ๊ธฐ๋ฅ
+- [x] ์๋ ์๋ก๊ณ ์นจ (์๋ฎฌ๋ ์ด์
)
+
+### โ
Step 7: ์ค์ UI
+
+- [x] `DriverManagementSettings.tsx` - ์ค์ ์ปดํฌ๋ํธ
+- [x] ์๋ ์๋ก๊ณ ์นจ ๊ฐ๊ฒฉ ์ค์
+- [x] ํ์ ์ปฌ๋ผ ์ ํ
+- [x] ํ
๋ง ์ค์
+- [x] ์ ๋ ฌ ๊ธฐ์ค ์ค์
+
+### โ
Step 8: ํตํฉ
+
+- [x] `DashboardSidebar`์ ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ ์ถ๊ฐ
+- [x] `CanvasElement`์์ ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ ๋ ๋๋ง
+- [x] `DashboardDesigner`์ ๊ธฐ๋ณธ๊ฐ ์ค์
+- [x] `ElementConfigModal`์ ์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ
+
+### โ
Step 9: ์คํ์ผ๋ง ๋ฐ ์ต์ ํ
+
+- [ ] ๋ฐ์ํ ๋์์ธ (๋ค์ํ ์์ ฏ ํฌ๊ธฐ ๋์)
+- [ ] ์ปดํฉํธ ๋ชจ๋ (์์ ํฌ๊ธฐ)
+- [ ] ๋ก๋ฉ ์ํ ์ฒ๋ฆฌ
+- [ ] ๋น ๋ฐ์ดํฐ ์ํ ์ฒ๋ฆฌ
+
+### โ
Step 10: ํฅํ ํ์ฅ ๊ธฐ๋ฅ
+
+- [ ] ์ค์ REST API ์ฐ๋
+- [ ] ์น์์ผ์ ํตํ ์ค์๊ฐ ์
๋ฐ์ดํธ
+- [ ] ๋งต ๋ทฐ (์ง๋์ ๊ธฐ์ฌ ์์น ํ์)
+- [ ] ๊ธฐ์ฌ๋ณ ์์ธ ์ ๋ณด ๋ชจ๋ฌ
+- [ ] ์ดํ ์ด๋ ฅ ์กฐํ
+- [ ] ์๋ฆผ ๊ธฐ๋ฅ (์ง์ฐ, ๊ธด๊ธ ์ํฉ ๋ฑ)
+
+## ์์ ฏ ํฌ๊ธฐ๋ณ ์ต์ ํ
+
+### 2x2 (์ต์ ํฌ๊ธฐ)
+
+- ์์ฝ ์ ๋ณด๋ง ํ์ (์ดํ์ค ๊ธฐ์ฌ ์, ๋๊ธฐ ๊ธฐ์ฌ ์)
+- ๊ฐ๋จํ ์ํ ํ์
+
+### 3x3
+
+- ์นด๋ ๋ทฐ (2-3๊ฐ ๊ธฐ์ฌ ํ์)
+- ๊ธฐ๋ณธ ์ ๋ณด ํ์
+
+### 4x3 ์ด์ (๊ถ์ฅ)
+
+- ๋ฆฌ์คํธ ๋ทฐ ๋๋ ์นด๋ ๋ทฐ ์ ์ฒด ํ์
+- ํํฐ๋ง ๋ฐ ๊ฒ์ ๊ธฐ๋ฅ
+- ๋ชจ๋ ์ ๋ณด ํ์
+
+## ์๋ฃ ๊ธฐ์ค
+
+- [x] ๊ธฐ๋ณธ ํ์
์ ์ ์๋ฃ
+- [x] ๋ชฉ์
๋ฐ์ดํฐ ์์ฑ ์๋ฃ
+- [x] ๋ฆฌ์คํธ ๋ทฐ ๊ตฌํ ์๋ฃ
+- [ ] ์นด๋ ๋ทฐ ๊ตฌํ ์๋ฃ (ํฅํ ์ถ๊ฐ)
+- [x] ํํฐ๋ง/๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ ์๋ฃ
+- [x] ์ค์ UI ๊ตฌํ ์๋ฃ
+- [x] ๋์๋ณด๋ ํตํฉ ์๋ฃ
+- [ ] ๋ค์ํ ํฌ๊ธฐ์์ ํ
์คํธ ์๋ฃ (์ฌ์ฉ์ ํ
์คํธ ํ์)
+
+## ์ฃผ์์ฌํญ
+
+1. **์ฑ๋ฅ ์ต์ ํ**: ๋ง์ ๊ธฐ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ๋ ๊ฐ์ ์คํฌ๋กค๋ง ๊ณ ๋ ค
+2. **์ค์๊ฐ ์
๋ฐ์ดํธ**: ์๋ ์๋ก๊ณ ์นจ ์ ๋ถ๋๋ฌ์ด ์ ํ ์ ๋๋ฉ์ด์
+3. **์ ๊ทผ์ฑ**: ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
์ง์
+4. **์๋ฌ ์ฒ๋ฆฌ**: API ์ฐ๋ ์ ์๋ฌ ์ํ ์ฒ๋ฆฌ
+5. **๋ฐ์ํ**: ์์ ํฌ๊ธฐ์์๋ ์ ๋ณด๊ฐ ์ ๋ณด์ด๋๋ก ๋์์ธ
+
+## ์ถ๊ฐ ๊ฐ์ ์ฌํญ ์ ์
+
+### 1. ํต๊ณ ์ ๋ณด
+
+- ์ค๋ ์ด ์ดํ ๊ฑด์
+- ํ๊ท ์ดํ ์๊ฐ
+- ์ฐจ๋ ์ ํ๋ณ ์ดํ ํต๊ณ
+
+### 2. ๊ธด๊ธ ์ํฉ ์๋ฆผ
+
+- ์ดํ ์ง์ฐ ์๋ฆผ (์์ ์๊ฐ ์ด๊ณผ)
+- ์ฐจ๋ ์ ๊ฒ ํ์ ์๋ฆผ
+- ๊ธฐ์ฌ ์ฐ๋ฝ ๋์ ์๋ฆผ
+
+### 3. ๋ฐฐ์ฐจ ๊ด๋ฆฌ (๊ณ ๊ธ ๊ธฐ๋ฅ)
+
+- ๋๊ธฐ ์ค์ธ ๊ธฐ์ฌ์๊ฒ ๋ฐฐ์ฐจ
+- ์ดํ ์ค์ผ์ค ๊ด๋ฆฌ
+- ๊ฒฝ๋ก ์ต์ ํ ์ ์
+
+### 4. ๋ณด๊ณ ์ ๊ธฐ๋ฅ
+
+- ์ผ์ผ ์ดํ ๋ณด๊ณ ์
+- ๊ธฐ์ฌ๋ณ ์ดํ ์ค์
+- ์ฐจ๋๋ณ ๊ฐ๋๋ฅ
+
+---
+
+## ๐ฏ ๊ตฌํ ์ฐ์ ์์
+
+1. **ํ์ (Phase 1)**
+ - ํ์
์ ์
+ - ๋ชฉ์
๋ฐ์ดํฐ
+ - ๋ฆฌ์คํธ ๋ทฐ
+ - ๊ธฐ๋ณธ ํํฐ๋ง
+
+2. **์ค์ (Phase 2)**
+ - ์นด๋ ๋ทฐ
+ - ๊ฒ์ ๊ธฐ๋ฅ
+ - ์ค์ UI
+ - ์๋ ์๋ก๊ณ ์นจ
+
+3. **์ถ๊ฐ (Phase 3)**
+ - ํต๊ณ ์ ๋ณด
+ - ์์ธ ์ ๋ณด ๋ชจ๋ฌ
+ - ์ดํ ์ด๋ ฅ
+
+4. **ํฅํ (Phase 4)**
+ - ๋งต ๋ทฐ
+ - ์ค์๊ฐ ์์น ์ถ์
+ - ๋ฐฐ์ฐจ ๊ด๋ฆฌ
+ - ๋ณด๊ณ ์ ๊ธฐ๋ฅ
+
+---
+
+**๊ตฌํ ์์์ผ**: 2025-10-14
+**๊ตฌํ ์๋ฃ์ผ**: 2025-10-14
+**ํ์ฌ ์งํ๋ฅ **: 90% (์นด๋ ๋ทฐ ๋ฐ ์ต์ข
ํ
์คํธ ์ ์ธ)
+
+## ๐ ๊ตฌํ ์๋ฃ!
+
+๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ์ ํต์ฌ ๊ธฐ๋ฅ์ด ๋ชจ๋ ๊ตฌํ๋์์ต๋๋ค!
+
+### โ
๊ตฌํ๋ ๊ธฐ๋ฅ
+
+1. **๋ฐ์ดํฐ ๊ตฌ์กฐ**
+ - DriverInfo, DriverManagementConfig ํ์
์ ์
+ - 15๊ฐ์ ๋ค์ํ ๋ชฉ์
๋ฐ์ดํฐ
+ - 6๊ฐ์ง ์ฐจ๋ ์ ํ ์ง์
+
+2. **๋ฆฌ์คํธ ๋ทฐ**
+ - ํ
์ด๋ธ ํ์์ ๊น๋ํ UI
+ - ์ํ๋ณ ์์ ๊ตฌ๋ถ (์ดํ์ค/๋๊ธฐ์ค/ํด์์ค/์ ๊ฒ์ค)
+ - ์ปดํฉํธ ๋ชจ๋ ์ง์ (2x2 ํฌ๊ธฐ)
+
+3. **ํํฐ๋ง ๋ฐ ๊ฒ์**
+ - ์ํ๋ณ ํํฐ (์ ์ฒด/์ดํ์ค/๋๊ธฐ์ค/ํด์์ค/์ ๊ฒ์ค)
+ - ๊ธฐ์ฌ๋ช
, ์ฐจ๋๋ฒํธ ๊ฒ์
+ - ์ค์๊ฐ ํํฐ๋ง
+
+4. **์ ๋ ฌ ๊ธฐ๋ฅ**
+ - ๊ธฐ์ฌ๋ช
, ์ฐจ๋๋ฒํธ, ์ดํ์ํ, ์ถ๋ฐ์๊ฐ ๊ธฐ์ค ์ ๋ ฌ
+ - ์ค๋ฆ์ฐจ์/๋ด๋ฆผ์ฐจ์ ์ง์
+
+5. **์๋ ์๋ก๊ณ ์นจ**
+ - 10์ด/30์ด/1๋ถ/5๋ถ ๊ฐ๊ฒฉ ์ค์ ๊ฐ๋ฅ
+ - ์ค์๊ฐ ๋ฐ์ดํฐ ์๋ฎฌ๋ ์ด์
+
+6. **์ค์ UI**
+ - Popover ๋ฐฉ์์ ์ง๊ด์ ์ธ ์ค์
+ - ํ์ ์ปฌ๋ผ ์ ํ (9๊ฐ ์ปฌ๋ผ)
+ - ํ
๋ง ์ค์ (Light/Dark/Custom)
+ - ์ ๋ ฌ ๊ธฐ์ค ๋ฐ ์์ ์ค์
+
+7. **๋์๋ณด๋ ํตํฉ**
+ - ์ฌ์ด๋๋ฐ์ ๋๋๊ทธ ๊ฐ๋ฅํ ์์ ฏ ์ถ๊ฐ
+ - ์บ๋ฒ์ค์์ ์์ ๋ก์ด ๋ฐฐ์น ๋ฐ ํฌ๊ธฐ ์กฐ์
+ - ์ค์ ์ ์ฅ ๋ฐ ๋ถ๋ฌ์ค๊ธฐ
+
+### ๐ ํฅํ ๊ฐ์ ์ฌํญ
+
+- ์นด๋ ๋ทฐ ๊ตฌํ
+- ๋งต ๋ทฐ (์ง๋ ์ฐ๋)
+- ์ค์ REST API ์ฐ๋
+- ์น์์ผ ์ค์๊ฐ ์
๋ฐ์ดํธ
+- ํต๊ณ ์ ๋ณด ์ถ๊ฐ
+- ๋ฐฐ์ฐจ ๊ด๋ฆฌ ๊ธฐ๋ฅ
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);
diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx
index f74035ba..6033fc7c 100644
--- a/frontend/components/admin/dashboard/DashboardDesigner.tsx
+++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useRef, useCallback, useEffect } from "react";
+import React, { useState, useRef, useCallback } from "react";
import { DashboardCanvas } from "./DashboardCanvas";
import { DashboardSidebar } from "./DashboardSidebar";
import { DashboardToolbar } from "./DashboardToolbar";
@@ -80,8 +80,15 @@ export default function DashboardDesigner() {
// ์๋ก์ด ์์ ์์ฑ (๊ณ ์ ๊ทธ๋ฆฌ๋ ๊ธฐ๋ฐ ๊ธฐ๋ณธ ํฌ๊ธฐ)
const createElement = useCallback(
(type: ElementType, subtype: ElementSubtype, x: number, y: number) => {
- // ๊ธฐ๋ณธ ํฌ๊ธฐ: ์ฐจํธ๋ 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;
@@ -233,13 +240,13 @@ export default function DashboardDesigner() {
{/* ํธ์ง ์ค์ธ ๋์๋ณด๋ ํ์ */}
{dashboardTitle && (
-
+
๐ ํธ์ง ์ค: {dashboardTitle}
)}
-
+
+
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx
index dc9d3f32..8bcacd2c 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,48 +56,19 @@ 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" || element.subtype === "driver-management")
+ ) {
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 a29a5640..b4876f27 100644
--- a/frontend/components/admin/dashboard/types.ts
+++ b/frontend/components/admin/dashboard/types.ts
@@ -15,8 +15,10 @@ export type ElementSubtype =
| "exchange"
| "weather"
| "clock"
+ | "calendar"
| "calculator"
- | "vehicle-map"; // ์์ ฏ ํ์
+ | "vehicle-map"
+ | "driver-management"; // ์์ ฏ ํ์
export interface Position {
x: number;
@@ -39,6 +41,8 @@ export interface DashboardElement {
dataSource?: ChartDataSource; // ๋ฐ์ดํฐ ์์ค ์ค์
chartConfig?: ChartConfig; // ์ฐจํธ ์ค์
clockConfig?: ClockConfig; // ์๊ณ ์ค์
+ calendarConfig?: CalendarConfig; // ๋ฌ๋ ฅ ์ค์
+ driverManagementConfig?: DriverManagementConfig; // ๊ธฐ์ฌ ๊ด๋ฆฌ ์ค์
}
export interface DragData {
@@ -88,3 +92,42 @@ 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; // ์ฃผ์ฐจ ํ์ (์ ํ)
+}
+
+// ๊ธฐ์ฌ ๊ด๋ฆฌ ์์ ฏ ์ค์
+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/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 (
-
- );
-}
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") && (
+ | {COLUMN_LABELS.status} |
+ )}
+ {visibleColumns.includes("name") && (
+ {COLUMN_LABELS.name} |
+ )}
+ {visibleColumns.includes("vehicleNumber") && (
+ {COLUMN_LABELS.vehicleNumber} |
+ )}
+ {visibleColumns.includes("vehicleType") && (
+ {COLUMN_LABELS.vehicleType} |
+ )}
+ {visibleColumns.includes("departure") && (
+ {COLUMN_LABELS.departure} |
+ )}
+ {visibleColumns.includes("destination") && (
+ {COLUMN_LABELS.destination} |
+ )}
+ {visibleColumns.includes("departureTime") && (
+ {COLUMN_LABELS.departureTime} |
+ )}
+ {visibleColumns.includes("estimatedArrival") && (
+
+ {COLUMN_LABELS.estimatedArrival}
+ |
+ )}
+ {visibleColumns.includes("phone") && (
+ {COLUMN_LABELS.phone} |
+ )}
+ {visibleColumns.includes("progress") && (
+ {COLUMN_LABELS.progress} |
+ )}
+
+
+
+ {drivers.map((driver) => {
+ const statusColors = getStatusColor(driver.status);
+ return (
+
+ {visibleColumns.includes("status") && (
+ |
+
+ {getStatusLabel(driver.status)}
+
+ |
+ )}
+ {visibleColumns.includes("name") && (
+ {driver.name} |
+ )}
+ {visibleColumns.includes("vehicleNumber") && (
+ {driver.vehicleNumber} |
+ )}
+ {visibleColumns.includes("vehicleType") && (
+ {driver.vehicleType} |
+ )}
+ {visibleColumns.includes("departure") && (
+
+ {driver.departure || -}
+ |
+ )}
+ {visibleColumns.includes("destination") && (
+
+ {driver.destination || -}
+ |
+ )}
+ {visibleColumns.includes("departureTime") && (
+ {formatTime(driver.departureTime)} |
+ )}
+ {visibleColumns.includes("estimatedArrival") && (
+ {formatTime(driver.estimatedArrival)} |
+ )}
+ {visibleColumns.includes("phone") && (
+ {driver.phone} |
+ )}
+ {visibleColumns.includes("progress") && (
+
+ {driver.progress !== undefined ? (
+
+ ) : (
+ -
+ )}
+ |
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
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: "์งํ๋ฅ ",
+};