From 104c5671f6e531448d71e9f971f94791f6d7e8a1 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Tue, 14 Oct 2025 16:46:42 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9A=B4=EC=A0=84=EC=9E=90=20=EA=B4=80=EB=A6=AC=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20(main?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B3=B5=EC=82=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/widgets/CalendarSettings.tsx | 207 ++++++++++++++ .../dashboard/widgets/CalendarWidget.tsx | 121 +++++++++ .../dashboard/widgets/DriverListView.tsx | 160 +++++++++++ .../widgets/DriverManagementSettings.tsx | 139 ++++++++++ .../widgets/DriverManagementWidget.tsx | 159 +++++++++++ .../admin/dashboard/widgets/MonthView.tsx | 117 ++++++++ .../admin/dashboard/widgets/calendarUtils.ts | 162 +++++++++++ .../admin/dashboard/widgets/driverMockData.ts | 181 +++++++++++++ .../admin/dashboard/widgets/driverUtils.ts | 256 ++++++++++++++++++ 9 files changed, 1502 insertions(+) create mode 100644 frontend/components/admin/dashboard/widgets/CalendarSettings.tsx create mode 100644 frontend/components/admin/dashboard/widgets/CalendarWidget.tsx create mode 100644 frontend/components/admin/dashboard/widgets/DriverListView.tsx create mode 100644 frontend/components/admin/dashboard/widgets/DriverManagementSettings.tsx create mode 100644 frontend/components/admin/dashboard/widgets/DriverManagementWidget.tsx create mode 100644 frontend/components/admin/dashboard/widgets/MonthView.tsx create mode 100644 frontend/components/admin/dashboard/widgets/calendarUtils.ts create mode 100644 frontend/components/admin/dashboard/widgets/driverMockData.ts create mode 100644 frontend/components/admin/dashboard/widgets/driverUtils.ts diff --git a/frontend/components/admin/dashboard/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/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: "진행률", +};