feature/screen-management #147
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface CargoListWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -47,7 +48,7 @@ export default function CargoListWidget({ element }: CargoListWidgetProps) {
|
|||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface CustomStatsWidgetProps {
|
||||
element?: DashboardElement;
|
||||
|
|
@ -77,7 +78,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
// 쿼리 실행하여 통계 계산
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface CustomerIssuesWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -49,7 +50,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
|||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface DeliveryStatusSummaryWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -39,7 +40,7 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw, Package, TruckIcon, AlertTriangle, CheckCircle, Clock, XCircle } from "lucide-react";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface DeliveryItem {
|
||||
id: string;
|
||||
|
|
@ -50,7 +51,7 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -266,14 +267,13 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
};
|
||||
|
||||
// 필터링된 배송 목록
|
||||
const filteredDeliveries = selectedStatus === "all"
|
||||
? deliveries
|
||||
: deliveries.filter((d) => d.status === selectedStatus);
|
||||
const filteredDeliveries =
|
||||
selectedStatus === "all" ? deliveries : deliveries.filter((d) => d.status === selectedStatus);
|
||||
|
||||
// 오늘 통계 계산
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
const todayStats = {
|
||||
// 오늘 발송 건수 (created_at이 오늘인 것)
|
||||
shipped: deliveries.filter((d: any) => {
|
||||
|
|
@ -293,22 +293,14 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gradient-to-br from-slate-50 to-blue-50 p-4 overflow-auto">
|
||||
<div className="h-full w-full overflow-auto bg-gradient-to-br from-slate-50 to-blue-50 p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">📦 배송 / 화물 처리 현황</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadData}
|
||||
disabled={isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={loadData} disabled={isLoading} className="h-8 w-8 p-0">
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -316,60 +308,60 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
{/* 배송 상태 요약 */}
|
||||
<div className="mb-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700">배송 상태 요약 (클릭하여 필터링)</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-5">
|
||||
<button
|
||||
onClick={() => setSelectedStatus("all")}
|
||||
className={`rounded-lg p-1.5 shadow-sm border-l-4 transition-all ${
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "all"
|
||||
? "border-gray-900 bg-gray-100 ring-2 ring-gray-900"
|
||||
: "border-gray-500 bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-600 mb-0.5">전체</div>
|
||||
<div className="mb-0.5 text-xs text-gray-600">전체</div>
|
||||
<div className="text-lg font-bold text-gray-900">{deliveries.length}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("in_transit")}
|
||||
className={`rounded-lg p-1.5 shadow-sm border-l-4 transition-all ${
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "in_transit"
|
||||
? "border-blue-900 bg-blue-100 ring-2 ring-blue-900"
|
||||
: "border-blue-500 bg-white hover:bg-blue-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-600 mb-0.5">배송중</div>
|
||||
<div className="mb-0.5 text-xs text-gray-600">배송중</div>
|
||||
<div className="text-lg font-bold text-blue-600">{statusStats.in_transit}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("delivered")}
|
||||
className={`rounded-lg p-1.5 shadow-sm border-l-4 transition-all ${
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "delivered"
|
||||
? "border-green-900 bg-green-100 ring-2 ring-green-900"
|
||||
: "border-green-500 bg-white hover:bg-green-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-600 mb-0.5">완료</div>
|
||||
<div className="mb-0.5 text-xs text-gray-600">완료</div>
|
||||
<div className="text-lg font-bold text-green-600">{statusStats.delivered}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("delayed")}
|
||||
className={`rounded-lg p-1.5 shadow-sm border-l-4 transition-all ${
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "delayed"
|
||||
? "border-red-900 bg-red-100 ring-2 ring-red-900"
|
||||
: "border-red-500 bg-white hover:bg-red-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-600 mb-0.5">지연</div>
|
||||
<div className="mb-0.5 text-xs text-gray-600">지연</div>
|
||||
<div className="text-lg font-bold text-red-600">{statusStats.delayed}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("pickup_waiting")}
|
||||
className={`rounded-lg p-1.5 shadow-sm border-l-4 transition-all ${
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "pickup_waiting"
|
||||
? "border-yellow-900 bg-yellow-100 ring-2 ring-yellow-900"
|
||||
: "border-yellow-500 bg-white hover:bg-yellow-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-gray-600 mb-0.5">픽업 대기</div>
|
||||
<div className="mb-0.5 text-xs text-gray-600">픽업 대기</div>
|
||||
<div className="text-lg font-bold text-yellow-600">{statusStats.pickup_waiting}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -379,13 +371,13 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
<div className="mb-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700">오늘 처리 현황</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-gray-500">
|
||||
<div className="text-xs text-gray-600 mb-0.5">발송 건수</div>
|
||||
<div className="rounded-lg border-l-4 border-gray-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-gray-600">발송 건수</div>
|
||||
<div className="text-lg font-bold text-gray-900">{todayStats.shipped}</div>
|
||||
<div className="text-xs text-gray-500">건</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-gray-500">
|
||||
<div className="text-xs text-gray-600 mb-0.5">도착 건수</div>
|
||||
<div className="rounded-lg border-l-4 border-gray-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-gray-600">도착 건수</div>
|
||||
<div className="text-lg font-bold text-gray-900">{todayStats.delivered}</div>
|
||||
<div className="text-xs text-gray-500">건</div>
|
||||
</div>
|
||||
|
|
@ -394,7 +386,7 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
|
||||
{/* 필터링된 화물 리스트 */}
|
||||
<div className="mb-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||
<Package className="h-4 w-4 text-gray-600" />
|
||||
{selectedStatus === "all" && `전체 화물 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "in_transit" && `배송 중인 화물 (${filteredDeliveries.length})`}
|
||||
|
|
@ -402,31 +394,35 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
{selectedStatus === "delayed" && `지연 중인 화물 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "pickup_waiting" && `픽업 대기 (${filteredDeliveries.length})`}
|
||||
</h4>
|
||||
<div className="rounded-lg bg-white shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
{filteredDeliveries.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-gray-500">
|
||||
{selectedStatus === "all" ? "화물이 없습니다" : "해당 상태의 화물이 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
|
||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-[200px] overflow-y-auto">
|
||||
{filteredDeliveries.map((delivery) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="p-3 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
|
||||
className="border-b border-gray-200 p-3 transition-colors last:border-b-0 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-sm text-gray-900">{delivery.customer}</div>
|
||||
<div className="text-sm font-semibold text-gray-900">{delivery.customer}</div>
|
||||
<div className="text-xs text-gray-600">{delivery.trackingNumber}</div>
|
||||
</div>
|
||||
<span className={`rounded-md px-2 py-1 text-xs font-semibold border ${getStatusColor(delivery.status)}`}>
|
||||
<span
|
||||
className={`rounded-md border px-2 py-1 text-xs font-semibold ${getStatusColor(delivery.status)}`}
|
||||
>
|
||||
{getStatusText(delivery.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">경로:</span>
|
||||
<span>{delivery.origin} → {delivery.destination}</span>
|
||||
<span>
|
||||
{delivery.origin} → {delivery.destination}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">예정:</span>
|
||||
|
|
@ -449,37 +445,37 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
|
||||
{/* 고객 클레임/이슈 리포트 */}
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||
<XCircle className="h-4 w-4 text-orange-600" />
|
||||
고객 클레임/이슈 ({issues.filter((i) => i.status !== "resolved").length})
|
||||
</h4>
|
||||
<div className="rounded-lg bg-white shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
{issues.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-gray-500">
|
||||
이슈가 없습니다
|
||||
</div>
|
||||
<div className="p-6 text-center text-sm text-gray-500">이슈가 없습니다</div>
|
||||
) : (
|
||||
<div className="max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
|
||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-[200px] overflow-y-auto">
|
||||
{issues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="p-3 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
|
||||
className="border-b border-gray-200 p-3 transition-colors last:border-b-0 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-sm text-gray-900">{issue.customer}</div>
|
||||
<div className="text-sm font-semibold text-gray-900">{issue.customer}</div>
|
||||
<div className="text-xs text-gray-600">{issue.trackingNumber}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<span className="rounded-md px-2 py-1 text-xs font-semibold bg-gray-100 text-gray-700 border border-gray-300">
|
||||
<span className="rounded-md border border-gray-300 bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-700">
|
||||
{getIssueTypeText(issue.issueType)}
|
||||
</span>
|
||||
<span className={`rounded-md px-2 py-1 text-xs font-semibold border ${getIssueStatusColor(issue.status)}`}>
|
||||
<span
|
||||
className={`rounded-md border px-2 py-1 text-xs font-semibold ${getIssueStatusColor(issue.status)}`}
|
||||
>
|
||||
{getIssueStatusText(issue.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div>{issue.description}</div>
|
||||
<div className="text-gray-500">접수: {issue.reportedAt}</div>
|
||||
</div>
|
||||
|
|
@ -492,4 +488,3 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface DeliveryTodayStatsWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -40,7 +41,7 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface ListSummaryWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -128,7 +129,7 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
setTableName(extractedTableName);
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { getWeather, WeatherData, getWeatherAlerts, WeatherAlert } from "@/lib/a
|
|||
import { Cloud, CloudRain, CloudSnow, Sun, Wind, AlertTriangle } from "lucide-react";
|
||||
import turfUnion from "@turf/union";
|
||||
import { polygon } from "@turf/helpers";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Leaflet 아이콘 경로 설정 (엑박 방지)
|
||||
|
|
@ -72,80 +73,140 @@ const CITY_COORDINATES = [
|
|||
{ name: "광주", lat: 35.1595, lng: 126.8526 },
|
||||
{ name: "대전", lat: 36.3504, lng: 127.3845 },
|
||||
{ name: "울산", lat: 35.5384, lng: 129.3114 },
|
||||
{ name: "세종", lat: 36.4800, lng: 127.2890 },
|
||||
{ name: "세종", lat: 36.48, lng: 127.289 },
|
||||
{ name: "제주", lat: 33.4996, lng: 126.5312 },
|
||||
];
|
||||
|
||||
// 해상 구역 폴리곤 좌표 (기상청 특보 구역 기준 - 깔끔한 사각형)
|
||||
const MARITIME_ZONES: Record<string, Array<[number, number]>> = {
|
||||
// 제주도 해역
|
||||
"제주도남부앞바다": [
|
||||
[33.25, 126.0], [33.25, 126.85], [33.0, 126.85], [33.0, 126.0]
|
||||
제주도남부앞바다: [
|
||||
[33.25, 126.0],
|
||||
[33.25, 126.85],
|
||||
[33.0, 126.85],
|
||||
[33.0, 126.0],
|
||||
],
|
||||
"제주도남쪽바깥먼바다": [
|
||||
[33.15, 125.7], [33.15, 127.3], [32.5, 127.3], [32.5, 125.7]
|
||||
제주도남쪽바깥먼바다: [
|
||||
[33.15, 125.7],
|
||||
[33.15, 127.3],
|
||||
[32.5, 127.3],
|
||||
[32.5, 125.7],
|
||||
],
|
||||
"제주도동부앞바다": [
|
||||
[33.4, 126.7], [33.4, 127.25], [33.05, 127.25], [33.05, 126.7]
|
||||
제주도동부앞바다: [
|
||||
[33.4, 126.7],
|
||||
[33.4, 127.25],
|
||||
[33.05, 127.25],
|
||||
[33.05, 126.7],
|
||||
],
|
||||
"제주도남동쪽안쪽먼바다": [
|
||||
[33.3, 126.85], [33.3, 127.95], [32.65, 127.95], [32.65, 126.85]
|
||||
제주도남동쪽안쪽먼바다: [
|
||||
[33.3, 126.85],
|
||||
[33.3, 127.95],
|
||||
[32.65, 127.95],
|
||||
[32.65, 126.85],
|
||||
],
|
||||
"제주도남서쪽안쪽먼바다": [
|
||||
[33.3, 125.35], [33.3, 126.45], [32.7, 126.45], [32.7, 125.35]
|
||||
제주도남서쪽안쪽먼바다: [
|
||||
[33.3, 125.35],
|
||||
[33.3, 126.45],
|
||||
[32.7, 126.45],
|
||||
[32.7, 125.35],
|
||||
],
|
||||
|
||||
|
||||
// 남해 해역
|
||||
"남해동부앞바다": [
|
||||
[34.65, 128.3], [34.65, 129.65], [33.95, 129.65], [33.95, 128.3]
|
||||
남해동부앞바다: [
|
||||
[34.65, 128.3],
|
||||
[34.65, 129.65],
|
||||
[33.95, 129.65],
|
||||
[33.95, 128.3],
|
||||
],
|
||||
"남해동부안쪽먼바다": [
|
||||
[34.25, 127.95], [34.25, 129.75], [33.45, 129.75], [33.45, 127.95]
|
||||
남해동부안쪽먼바다: [
|
||||
[34.25, 127.95],
|
||||
[34.25, 129.75],
|
||||
[33.45, 129.75],
|
||||
[33.45, 127.95],
|
||||
],
|
||||
"남해동부바깥먼바다": [
|
||||
[33.65, 127.95], [33.65, 130.35], [32.45, 130.35], [32.45, 127.95]
|
||||
남해동부바깥먼바다: [
|
||||
[33.65, 127.95],
|
||||
[33.65, 130.35],
|
||||
[32.45, 130.35],
|
||||
[32.45, 127.95],
|
||||
],
|
||||
|
||||
|
||||
// 동해 해역
|
||||
"경북북부앞바다": [
|
||||
[36.65, 129.2], [36.65, 130.1], [35.95, 130.1], [35.95, 129.2]
|
||||
경북북부앞바다: [
|
||||
[36.65, 129.2],
|
||||
[36.65, 130.1],
|
||||
[35.95, 130.1],
|
||||
[35.95, 129.2],
|
||||
],
|
||||
"경북남부앞바다": [
|
||||
[36.15, 129.1], [36.15, 129.95], [35.45, 129.95], [35.45, 129.1]
|
||||
경북남부앞바다: [
|
||||
[36.15, 129.1],
|
||||
[36.15, 129.95],
|
||||
[35.45, 129.95],
|
||||
[35.45, 129.1],
|
||||
],
|
||||
"동해남부남쪽안쪽먼바다": [
|
||||
[35.65, 129.35], [35.65, 130.65], [34.95, 130.65], [34.95, 129.35]
|
||||
동해남부남쪽안쪽먼바다: [
|
||||
[35.65, 129.35],
|
||||
[35.65, 130.65],
|
||||
[34.95, 130.65],
|
||||
[34.95, 129.35],
|
||||
],
|
||||
"동해남부남쪽바깥먼바다": [
|
||||
[35.25, 129.45], [35.25, 131.15], [34.15, 131.15], [34.15, 129.45]
|
||||
동해남부남쪽바깥먼바다: [
|
||||
[35.25, 129.45],
|
||||
[35.25, 131.15],
|
||||
[34.15, 131.15],
|
||||
[34.15, 129.45],
|
||||
],
|
||||
"동해남부북쪽안쪽먼바다": [
|
||||
[36.6, 129.65], [36.6, 130.95], [35.85, 130.95], [35.85, 129.65]
|
||||
동해남부북쪽안쪽먼바다: [
|
||||
[36.6, 129.65],
|
||||
[36.6, 130.95],
|
||||
[35.85, 130.95],
|
||||
[35.85, 129.65],
|
||||
],
|
||||
"동해남부북쪽바깥먼바다": [
|
||||
[36.65, 130.35], [36.65, 132.15], [35.85, 132.15], [35.85, 130.35]
|
||||
동해남부북쪽바깥먼바다: [
|
||||
[36.65, 130.35],
|
||||
[36.65, 132.15],
|
||||
[35.85, 132.15],
|
||||
[35.85, 130.35],
|
||||
],
|
||||
|
||||
|
||||
// 강원 해역
|
||||
"강원북부앞바다": [
|
||||
[38.15, 128.4], [38.15, 129.55], [37.45, 129.55], [37.45, 128.4]
|
||||
강원북부앞바다: [
|
||||
[38.15, 128.4],
|
||||
[38.15, 129.55],
|
||||
[37.45, 129.55],
|
||||
[37.45, 128.4],
|
||||
],
|
||||
"강원중부앞바다": [
|
||||
[37.65, 128.7], [37.65, 129.6], [36.95, 129.6], [36.95, 128.7]
|
||||
강원중부앞바다: [
|
||||
[37.65, 128.7],
|
||||
[37.65, 129.6],
|
||||
[36.95, 129.6],
|
||||
[36.95, 128.7],
|
||||
],
|
||||
"강원남부앞바다": [
|
||||
[37.15, 128.9], [37.15, 129.85], [36.45, 129.85], [36.45, 128.9]
|
||||
강원남부앞바다: [
|
||||
[37.15, 128.9],
|
||||
[37.15, 129.85],
|
||||
[36.45, 129.85],
|
||||
[36.45, 128.9],
|
||||
],
|
||||
"동해중부안쪽먼바다": [
|
||||
[38.55, 129.35], [38.55, 131.15], [37.25, 131.15], [37.25, 129.35]
|
||||
동해중부안쪽먼바다: [
|
||||
[38.55, 129.35],
|
||||
[38.55, 131.15],
|
||||
[37.25, 131.15],
|
||||
[37.25, 129.35],
|
||||
],
|
||||
"동해중부바깥먼바다": [
|
||||
[38.6, 130.35], [38.6, 132.55], [37.65, 132.55], [37.65, 130.35]
|
||||
동해중부바깥먼바다: [
|
||||
[38.6, 130.35],
|
||||
[38.6, 132.55],
|
||||
[37.65, 132.55],
|
||||
[37.65, 130.35],
|
||||
],
|
||||
|
||||
|
||||
// 울릉도·독도
|
||||
"울릉도.독도": [
|
||||
[37.7, 130.7], [37.7, 132.0], [37.4, 132.0], [37.4, 130.7]
|
||||
[37.7, 130.7],
|
||||
[37.7, 132.0],
|
||||
[37.4, 132.0],
|
||||
[37.4, 130.7],
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -156,10 +217,7 @@ const getDistance = (lat1: number, lng1: number, lat2: number, lng2: number): nu
|
|||
const dLng = ((lng2 - lng1) * Math.PI) / 180;
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos((lat1 * Math.PI) / 180) *
|
||||
Math.cos((lat2 * Math.PI) / 180) *
|
||||
Math.sin(dLng / 2) *
|
||||
Math.sin(dLng / 2);
|
||||
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
};
|
||||
|
|
@ -235,7 +293,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
useEffect(() => {
|
||||
console.log("🗺️ MapSummaryWidget 초기화");
|
||||
console.log("🗺️ showWeatherAlerts:", element.chartConfig?.showWeatherAlerts);
|
||||
|
||||
|
||||
// GeoJSON 데이터 로드
|
||||
loadGeoJsonData();
|
||||
|
||||
|
|
@ -390,7 +448,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
setTableName(extractedTableName);
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -557,13 +615,15 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
)}
|
||||
|
||||
{/* 기상특보 영역 표시 (해상 - Polygon 레이어) - 개별 표시 */}
|
||||
{element.chartConfig?.showWeatherAlerts && weatherAlerts && weatherAlerts.length > 0 &&
|
||||
{element.chartConfig?.showWeatherAlerts &&
|
||||
weatherAlerts &&
|
||||
weatherAlerts.length > 0 &&
|
||||
weatherAlerts
|
||||
.filter((alert) => MARITIME_ZONES[alert.location])
|
||||
.map((alert, idx) => {
|
||||
const coordinates = MARITIME_ZONES[alert.location];
|
||||
const alertColor = getAlertColor(alert.severity);
|
||||
|
||||
|
||||
return (
|
||||
<Polygon
|
||||
key={`maritime-${idx}`}
|
||||
|
|
@ -597,14 +657,28 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
>
|
||||
<Popup>
|
||||
<div style={{ minWidth: "180px" }}>
|
||||
<div style={{ fontWeight: "bold", fontSize: "13px", marginBottom: "6px", display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "13px",
|
||||
marginBottom: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: alertColor }}>⚠️</span>
|
||||
{alert.location}
|
||||
</div>
|
||||
<div style={{ padding: "6px", background: "#f9fafb", borderRadius: "4px", borderLeft: `3px solid ${alertColor}` }}>
|
||||
<div style={{ fontWeight: "600", fontSize: "11px", color: alertColor }}>
|
||||
{alert.title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "6px",
|
||||
background: "#f9fafb",
|
||||
borderRadius: "4px",
|
||||
borderLeft: `3px solid ${alertColor}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: "600", fontSize: "11px", color: alertColor }}>{alert.title}</div>
|
||||
<div style={{ fontSize: "10px", color: "#6b7280", marginTop: "3px" }}>
|
||||
{alert.description}
|
||||
</div>
|
||||
|
|
@ -616,8 +690,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
</Popup>
|
||||
</Polygon>
|
||||
);
|
||||
})
|
||||
}
|
||||
})}
|
||||
|
||||
{/* 마커 표시 */}
|
||||
{markers.map((marker, idx) => (
|
||||
|
|
@ -672,7 +745,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
|
||||
{/* 범례 (특보가 있을 때만 표시) */}
|
||||
{element.chartConfig?.showWeatherAlerts && weatherAlerts && weatherAlerts.length > 0 && (
|
||||
<div className="absolute bottom-4 right-4 z-10 rounded-lg border bg-white p-3 shadow-lg">
|
||||
<div className="absolute right-4 bottom-4 z-10 rounded-lg border bg-white p-3 shadow-lg">
|
||||
<div className="mb-2 flex items-center gap-1 text-xs font-semibold">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
기상특보
|
||||
|
|
@ -691,9 +764,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
<span>약한 주의보</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-t pt-2 text-[10px] text-gray-500">
|
||||
총 {weatherAlerts.length}건 발효 중
|
||||
</div>
|
||||
<div className="mt-2 border-t pt-2 text-[10px] text-gray-500">총 {weatherAlerts.length}건 발효 중</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface StatusSummaryWidgetProps {
|
||||
element: DashboardElement;
|
||||
|
|
@ -180,7 +181,7 @@ export default function StatusSummaryWidget({
|
|||
setTableName(extractedTableName);
|
||||
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface TransportStatsWidgetProps {
|
||||
element?: DashboardElement;
|
||||
|
|
@ -53,7 +54,7 @@ export default function TransportStatsWidget({ element, refreshInterval = 60000
|
|||
|
||||
// 쿼리 실행하여 통계 계산
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { RefreshCw, Truck, Navigation, Gauge } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface Vehicle {
|
||||
id: string;
|
||||
|
|
@ -35,11 +36,11 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const query = element.dataSource.query;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -109,7 +110,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<div className="mb-3 flex gap-2 overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setSelectedStatus("all")}
|
||||
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "all" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -117,7 +118,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("active")}
|
||||
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "active" ? "bg-green-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -125,7 +126,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("inactive")}
|
||||
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "inactive" ? "bg-yellow-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -133,7 +134,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("maintenance")}
|
||||
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "maintenance" ? "bg-orange-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -141,7 +142,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("warning")}
|
||||
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "warning" ? "bg-red-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -170,7 +171,9 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<Truck className="h-4 w-4 text-gray-600" />
|
||||
<span className="font-semibold text-gray-900">{vehicle.vehicle_name}</span>
|
||||
</div>
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold text-white ${getStatusColor(vehicle.status)}`}>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold text-white ${getStatusColor(vehicle.status)}`}
|
||||
>
|
||||
{getStatusText(vehicle.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -201,4 +204,3 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
|
|||
import dynamic from "next/dynamic";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Leaflet 아이콘 경로 설정 (엑박 방지)
|
||||
|
|
@ -66,7 +67,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { RefreshCw, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
|
||||
interface VehicleStatusWidgetProps {
|
||||
element?: any; // 대시보드 요소 (dataSource 포함)
|
||||
|
|
@ -36,11 +37,11 @@ export default function VehicleStatusWidget({ element, refreshInterval = 30000 }
|
|||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const query = element.dataSource.query;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -150,53 +151,50 @@ export default function VehicleStatusWidget({ element, refreshInterval = 30000 }
|
|||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-gray-600">가동률</div>
|
||||
<div className="flex items-center gap-0.5 text-sm font-bold text-green-600">
|
||||
{activeRate}%
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 text-sm font-bold text-green-600">{activeRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상태별 카드 */}
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* 운행 중 */}
|
||||
<div className="rounded border-l-2 border-green-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">운행</div>
|
||||
{/* 운행 중 */}
|
||||
<div className="rounded border-l-2 border-green-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">운행</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-green-600">{statusData.active}</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-green-600">{statusData.active}</div>
|
||||
</div>
|
||||
|
||||
{/* 대기 */}
|
||||
<div className="rounded border-l-2 border-yellow-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-yellow-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">대기</div>
|
||||
{/* 대기 */}
|
||||
<div className="rounded border-l-2 border-yellow-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-yellow-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">대기</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-yellow-600">{statusData.inactive}</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-yellow-600">{statusData.inactive}</div>
|
||||
</div>
|
||||
|
||||
{/* 정비 */}
|
||||
<div className="rounded border-l-2 border-orange-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-orange-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">정비</div>
|
||||
{/* 정비 */}
|
||||
<div className="rounded border-l-2 border-orange-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-orange-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">정비</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-orange-600">{statusData.maintenance}</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-orange-600">{statusData.maintenance}</div>
|
||||
</div>
|
||||
|
||||
{/* 고장 */}
|
||||
<div className="rounded border-l-2 border-red-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-red-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">고장</div>
|
||||
{/* 고장 */}
|
||||
<div className="rounded border-l-2 border-red-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-red-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">고장</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-red-600">{statusData.warning}</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-red-600">{statusData.warning}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||||
import { WORK_TYPE_LABELS, WORK_STATUS_LABELS, WORK_STATUS_COLORS, WorkType, WorkStatus } from "@/types/workHistory";
|
||||
|
||||
interface WorkHistoryWidgetProps {
|
||||
|
|
@ -33,7 +34,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
// 쿼리가 설정되어 있으면 쿼리 실행
|
||||
if (element.dataSource?.query) {
|
||||
const token = localStorage.getItem("authToken");
|
||||
const response = await fetch("/api/dashboards/execute-query", {
|
||||
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
Loading…
Reference in New Issue