Compare commits
No commits in common. "6449eb5ac340de8b1bb56c75db2792dd2e1d80ba" and "3c73c202927f7facc41f728564ab2b72e5872425" have entirely different histories.
6449eb5ac3
...
3c73c20292
|
|
@ -148,19 +148,9 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case "date":
|
case "date":
|
||||||
try {
|
return new Date(value).toLocaleDateString("ko-KR");
|
||||||
const dateVal = new Date(value);
|
|
||||||
return dateVal.toLocaleDateString("ko-KR", { timeZone: "Asia/Seoul" });
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
case "datetime":
|
case "datetime":
|
||||||
try {
|
return new Date(value).toLocaleString("ko-KR");
|
||||||
const dateVal = new Date(value);
|
|
||||||
return dateVal.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" });
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
case "number":
|
case "number":
|
||||||
return Number(value).toLocaleString("ko-KR");
|
return Number(value).toLocaleString("ko-KR");
|
||||||
case "currency":
|
case "currency":
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { ArrowLeft, Save, Loader2, Grid3x3, Move, Box, Package, Truck, Check, ParkingCircle } from "lucide-react";
|
||||||
ArrowLeft,
|
|
||||||
Save,
|
|
||||||
Loader2,
|
|
||||||
Grid3x3,
|
|
||||||
Move,
|
|
||||||
Box,
|
|
||||||
Package,
|
|
||||||
Truck,
|
|
||||||
Check,
|
|
||||||
ParkingCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
@ -557,9 +545,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
|
|
||||||
// 레이아웃 데이터 로드
|
// 레이아웃 데이터 로드
|
||||||
const [layoutData, setLayoutData] = useState<{ layout: any; objects: any[] } | null>(null);
|
const [layoutData, setLayoutData] = useState<{ layout: any; objects: any[] } | null>(null);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
|
|
||||||
// 레이아웃 로드 함수
|
useEffect(() => {
|
||||||
const loadLayout = async () => {
|
const loadLayout = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -664,7 +651,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
// Location 객체들의 자재 개수 로드 (직접 dbConnectionId와 materialTableName 전달)
|
// Location 객체들의 자재 개수 로드 (직접 dbConnectionId와 materialTableName 전달)
|
||||||
const dbConnectionId = layout.external_db_connection_id;
|
const dbConnectionId = layout.external_db_connection_id;
|
||||||
const hierarchyConfigParsed =
|
const hierarchyConfigParsed =
|
||||||
typeof layout.hierarchy_config === "string" ? JSON.parse(layout.hierarchy_config) : layout.hierarchy_config;
|
typeof layout.hierarchy_config === "string"
|
||||||
|
? JSON.parse(layout.hierarchy_config)
|
||||||
|
: layout.hierarchy_config;
|
||||||
const materialTableName = hierarchyConfigParsed?.material?.tableName;
|
const materialTableName = hierarchyConfigParsed?.material?.tableName;
|
||||||
|
|
||||||
const locationObjects = loadedObjects.filter(
|
const locationObjects = loadedObjects.filter(
|
||||||
|
|
@ -697,30 +686,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 위젯 새로고침 핸들러
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
const confirmed = window.confirm(
|
|
||||||
"저장되지 않은 변경사항이 있습니다. 새로고침하면 변경사항이 사라집니다. 계속하시겠습니까?",
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
}
|
|
||||||
setIsRefreshing(true);
|
|
||||||
setSelectedObject(null);
|
|
||||||
setMaterials([]);
|
|
||||||
await loadLayout();
|
|
||||||
setIsRefreshing(false);
|
|
||||||
toast({
|
|
||||||
title: "새로고침 완료",
|
|
||||||
description: "데이터가 갱신되었습니다.",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 초기 로드
|
|
||||||
useEffect(() => {
|
|
||||||
loadLayout();
|
loadLayout();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [layoutId]);
|
}, [layoutId]); // toast 제거
|
||||||
|
|
||||||
// 외부 DB 연결 자동 선택 (externalDbConnections 로드 완료 시)
|
// 외부 DB 연결 자동 선택 (externalDbConnections 로드 완료 시)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1084,11 +1052,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
};
|
};
|
||||||
|
|
||||||
// Location별 자재 개수 로드 (locaKeys를 직접 받음)
|
// Location별 자재 개수 로드 (locaKeys를 직접 받음)
|
||||||
const loadMaterialCountsForLocations = async (
|
const loadMaterialCountsForLocations = async (locaKeys: string[], dbConnectionId?: number, materialTableName?: string) => {
|
||||||
locaKeys: string[],
|
|
||||||
dbConnectionId?: number,
|
|
||||||
materialTableName?: string,
|
|
||||||
) => {
|
|
||||||
const connectionId = dbConnectionId || selectedDbConnection;
|
const connectionId = dbConnectionId || selectedDbConnection;
|
||||||
const tableName = materialTableName || selectedTables.material;
|
const tableName = materialTableName || selectedTables.material;
|
||||||
if (!connectionId || locaKeys.length === 0) return;
|
if (!connectionId || locaKeys.length === 0) return;
|
||||||
|
|
@ -1109,7 +1073,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
}
|
}
|
||||||
// 백엔드 응답 필드명: location_key, count (대소문자 모두 체크)
|
// 백엔드 응답 필드명: location_key, count (대소문자 모두 체크)
|
||||||
const materialCount = response.data?.find(
|
const materialCount = response.data?.find(
|
||||||
(mc: any) => mc.LOCAKEY === obj.locaKey || mc.location_key === obj.locaKey || mc.locakey === obj.locaKey,
|
(mc: any) =>
|
||||||
|
mc.LOCAKEY === obj.locaKey ||
|
||||||
|
mc.location_key === obj.locaKey ||
|
||||||
|
mc.locakey === obj.locaKey
|
||||||
);
|
);
|
||||||
if (materialCount) {
|
if (materialCount) {
|
||||||
// count 또는 material_count 필드 사용
|
// count 또는 material_count 필드 사용
|
||||||
|
|
@ -1560,16 +1527,6 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{hasUnsavedChanges && <span className="text-warning text-sm font-medium">미저장 변경사항 있음</span>}
|
{hasUnsavedChanges && <span className="text-warning text-sm font-medium">미저장 변경사항 있음</span>}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={isRefreshing || isLoading}
|
|
||||||
title="새로고침"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
|
||||||
{isRefreshing ? "갱신 중..." : "새로고침"}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave} disabled={isSaving || !hasUnsavedChanges}>
|
<Button size="sm" onClick={handleSave} disabled={isSaving || !hasUnsavedChanges}>
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1663,20 +1620,27 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select value={selectedTemplateId} onValueChange={(val) => setSelectedTemplateId(val)}>
|
<Select
|
||||||
|
value={selectedTemplateId}
|
||||||
|
onValueChange={(val) => setSelectedTemplateId(val)}
|
||||||
|
>
|
||||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||||
<SelectValue placeholder={loadingTemplates ? "로딩 중..." : "템플릿 선택..."} />
|
<SelectValue placeholder={loadingTemplates ? "로딩 중..." : "템플릿 선택..."} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{mappingTemplates.length === 0 ? (
|
{mappingTemplates.length === 0 ? (
|
||||||
<div className="text-muted-foreground px-2 py-1 text-xs">사용 가능한 템플릿이 없습니다</div>
|
<div className="text-muted-foreground px-2 py-1 text-xs">
|
||||||
|
사용 가능한 템플릿이 없습니다
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
mappingTemplates.map((tpl) => (
|
mappingTemplates.map((tpl) => (
|
||||||
<SelectItem key={tpl.id} value={tpl.id} className="text-xs">
|
<SelectItem key={tpl.id} value={tpl.id} className="text-xs">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{tpl.name}</span>
|
<span>{tpl.name}</span>
|
||||||
{tpl.description && (
|
{tpl.description && (
|
||||||
<span className="text-muted-foreground text-[10px]">{tpl.description}</span>
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
{tpl.description}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -1740,11 +1704,17 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
}}
|
}}
|
||||||
onLoadColumns={async (tableName: string) => {
|
onLoadColumns={async (tableName: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await ExternalDbConnectionAPI.getTableColumns(selectedDbConnection, tableName);
|
const response = await ExternalDbConnectionAPI.getTableColumns(
|
||||||
|
selectedDbConnection,
|
||||||
|
tableName,
|
||||||
|
);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// 컬럼 정보 객체 배열로 반환 (이름 + 설명 + PK 플래그)
|
// 컬럼 정보 객체 배열로 반환 (이름 + 설명 + PK 플래그)
|
||||||
return response.data.map((col: any) => ({
|
return response.data.map((col: any) => ({
|
||||||
column_name: typeof col === "string" ? col : col.column_name || col.COLUMN_NAME || String(col),
|
column_name:
|
||||||
|
typeof col === "string"
|
||||||
|
? col
|
||||||
|
: col.column_name || col.COLUMN_NAME || String(col),
|
||||||
data_type: col.data_type || col.DATA_TYPE,
|
data_type: col.data_type || col.DATA_TYPE,
|
||||||
description: col.description || col.COLUMN_COMMENT || undefined,
|
description: col.description || col.COLUMN_COMMENT || undefined,
|
||||||
is_primary_key: col.is_primary_key ?? col.IS_PRIMARY_KEY,
|
is_primary_key: col.is_primary_key ?? col.IS_PRIMARY_KEY,
|
||||||
|
|
@ -2384,7 +2354,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSaveTemplate} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
<Button
|
||||||
|
onClick={handleSaveTemplate}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
저장
|
저장
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Loader2, Search, X, Grid3x3, Package, ParkingCircle, RefreshCw } from "lucide-react";
|
import { Loader2, Search, X, Grid3x3, Package, ParkingCircle } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -41,9 +41,9 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
// 검색 및 필터
|
// 검색 및 필터
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [filterType, setFilterType] = useState<string>("all");
|
const [filterType, setFilterType] = useState<string>("all");
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
|
|
||||||
// 레이아웃 데이터 로드 함수
|
// 레이아웃 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
const loadLayout = async () => {
|
const loadLayout = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -61,7 +61,9 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
let hierarchyConfigData: any = null;
|
let hierarchyConfigData: any = null;
|
||||||
if (layout.hierarchy_config) {
|
if (layout.hierarchy_config) {
|
||||||
hierarchyConfigData =
|
hierarchyConfigData =
|
||||||
typeof layout.hierarchy_config === "string" ? JSON.parse(layout.hierarchy_config) : layout.hierarchy_config;
|
typeof layout.hierarchy_config === "string"
|
||||||
|
? JSON.parse(layout.hierarchy_config)
|
||||||
|
: layout.hierarchy_config;
|
||||||
setHierarchyConfig(hierarchyConfigData);
|
setHierarchyConfig(hierarchyConfigData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +111,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
const locationObjects = loadedObjects.filter(
|
const locationObjects = loadedObjects.filter(
|
||||||
(obj) =>
|
(obj) =>
|
||||||
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
||||||
obj.locaKey,
|
obj.locaKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
||||||
|
|
@ -141,7 +143,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
return { ...obj, materialCount: countData.count };
|
return { ...obj, materialCount: countData.count };
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -160,25 +162,9 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 위젯 새로고침 핸들러
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setIsRefreshing(true);
|
|
||||||
setSelectedObject(null);
|
|
||||||
setMaterials([]);
|
|
||||||
setShowInfoPanel(false);
|
|
||||||
await loadLayout();
|
|
||||||
setIsRefreshing(false);
|
|
||||||
toast({
|
|
||||||
title: "새로고침 완료",
|
|
||||||
description: "데이터가 갱신되었습니다.",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 초기 로드
|
|
||||||
useEffect(() => {
|
|
||||||
loadLayout();
|
loadLayout();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [layoutId]);
|
}, [layoutId]); // toast 제거 - 무한 루프 방지
|
||||||
|
|
||||||
// Location의 자재 목록 로드
|
// Location의 자재 목록 로드
|
||||||
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
||||||
|
|
@ -336,16 +322,6 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
|
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
|
||||||
<p className="text-muted-foreground text-sm">읽기 전용 뷰</p>
|
<p className="text-muted-foreground text-sm">읽기 전용 뷰</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={isRefreshing || isLoading}
|
|
||||||
title="새로고침"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
|
||||||
{isRefreshing ? "갱신 중..." : "새로고침"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 메인 영역 */}
|
{/* 메인 영역 */}
|
||||||
|
|
@ -549,7 +525,8 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
위치: ({locationObj.position.x.toFixed(1)}, {locationObj.position.z.toFixed(1)})
|
위치: ({locationObj.position.x.toFixed(1)},{" "}
|
||||||
|
{locationObj.position.z.toFixed(1)})
|
||||||
</p>
|
</p>
|
||||||
{locationObj.locaKey && (
|
{locationObj.locaKey && (
|
||||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||||
|
|
|
||||||
|
|
@ -180,19 +180,9 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case "date":
|
case "date":
|
||||||
try {
|
return new Date(value).toLocaleDateString("ko-KR");
|
||||||
const dateVal = new Date(value);
|
|
||||||
return dateVal.toLocaleDateString("ko-KR", { timeZone: "Asia/Seoul" });
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
case "datetime":
|
case "datetime":
|
||||||
try {
|
return new Date(value).toLocaleString("ko-KR");
|
||||||
const dateVal = new Date(value);
|
|
||||||
return dateVal.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" });
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
case "number":
|
case "number":
|
||||||
return Number(value).toLocaleString("ko-KR");
|
return Number(value).toLocaleString("ko-KR");
|
||||||
case "currency":
|
case "currency":
|
||||||
|
|
|
||||||
|
|
@ -203,14 +203,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
setTripInfoLoading(identifier);
|
setTripInfoLoading(identifier);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// user_id 또는 vehicle_number로 조회 (TIMESTAMPTZ는 변환 불필요)
|
// user_id 또는 vehicle_number로 조회 (시간은 KST로 변환)
|
||||||
const query = `SELECT
|
const query = `SELECT
|
||||||
id, vehicle_number, user_id,
|
id, vehicle_number, user_id,
|
||||||
last_trip_start,
|
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||||
last_trip_end,
|
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||||
last_trip_distance, last_trip_time,
|
last_trip_distance, last_trip_time,
|
||||||
last_empty_start,
|
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||||
last_empty_end,
|
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||||
last_empty_distance, last_empty_time,
|
last_empty_distance, last_empty_time,
|
||||||
departure, arrival, status
|
departure, arrival, status
|
||||||
FROM vehicles
|
FROM vehicles
|
||||||
|
|
@ -281,15 +281,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
if (identifiers.length === 0) return;
|
if (identifiers.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 모든 마커의 운행/공차 정보를 한 번에 조회 (TIMESTAMPTZ는 변환 불필요)
|
// 모든 마커의 운행/공차 정보를 한 번에 조회 (시간은 KST로 변환)
|
||||||
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
const query = `SELECT
|
const query = `SELECT
|
||||||
id, vehicle_number, user_id,
|
id, vehicle_number, user_id,
|
||||||
last_trip_start,
|
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||||
last_trip_end,
|
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||||
last_trip_distance, last_trip_time,
|
last_trip_distance, last_trip_time,
|
||||||
last_empty_start,
|
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||||
last_empty_end,
|
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||||
last_empty_distance, last_empty_time,
|
last_empty_distance, last_empty_time,
|
||||||
departure, arrival, status
|
departure, arrival, status
|
||||||
FROM vehicles
|
FROM vehicles
|
||||||
|
|
|
||||||
|
|
@ -1506,7 +1506,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
tableName: tableConfig.selectedTable,
|
tableName: tableConfig.selectedTable,
|
||||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||||
linkedFilters: splitPanelContext?.linkedFilters,
|
linkedFilters: splitPanelContext?.linkedFilters,
|
||||||
splitPanelPosition: splitPanelPosition,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (splitPanelContext) {
|
if (splitPanelContext) {
|
||||||
|
|
@ -1538,39 +1537,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
linkedFilterValues[key] = value;
|
linkedFilterValues[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 자동 컬럼 매칭: linkedFilters가 설정되어 있지 않아도
|
|
||||||
// 우측 화면(splitPanelPosition === "right")이고 좌측 데이터가 선택되어 있으면
|
|
||||||
// 동일한 컬럼명이 있는 경우 자동으로 필터링 적용
|
|
||||||
if (
|
|
||||||
splitPanelPosition === "right" &&
|
|
||||||
hasSelectedLeftData &&
|
|
||||||
Object.keys(linkedFilterValues).length === 0 &&
|
|
||||||
!hasLinkedFiltersConfigured
|
|
||||||
) {
|
|
||||||
const leftData = splitPanelContext.selectedLeftData!;
|
|
||||||
const tableColumns = (tableConfig.columns || []).map((col) => col.columnName);
|
|
||||||
|
|
||||||
// 좌측 데이터의 컬럼 중 현재 테이블에 동일한 컬럼이 있는지 확인
|
|
||||||
for (const [colName, colValue] of Object.entries(leftData)) {
|
|
||||||
// null, undefined, 빈 문자열 제외
|
|
||||||
if (colValue === null || colValue === undefined || colValue === "") continue;
|
|
||||||
// id, objid 등 기본 키는 제외 (너무 일반적인 컬럼명)
|
|
||||||
if (colName === "id" || colName === "objid" || colName === "company_code") continue;
|
|
||||||
|
|
||||||
// 현재 테이블에 동일한 컬럼이 있는지 확인
|
|
||||||
if (tableColumns.includes(colName)) {
|
|
||||||
linkedFilterValues[colName] = colValue;
|
|
||||||
hasLinkedFiltersConfigured = true;
|
|
||||||
console.log(`🔗 [TableList] 자동 컬럼 매칭: ${colName} = ${colValue}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(linkedFilterValues).length > 0) {
|
|
||||||
console.log("🔗 [TableList] 자동 컬럼 매칭 필터 적용:", linkedFilterValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(linkedFilterValues).length > 0) {
|
if (Object.keys(linkedFilterValues).length > 0) {
|
||||||
console.log("🔗 [TableList] 연결 필터 적용:", linkedFilterValues);
|
console.log("🔗 [TableList] 연결 필터 적용:", linkedFilterValues);
|
||||||
}
|
}
|
||||||
|
|
@ -1783,10 +1749,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchValues,
|
searchValues,
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
// 🆕 우측 화면일 때만 selectedLeftData 변경에 반응 (좌측 테이블은 재조회 불필요)
|
splitPanelContext?.selectedLeftData, // 🆕 연결 필터 변경 시 재조회
|
||||||
splitPanelPosition,
|
|
||||||
currentSplitPosition,
|
|
||||||
splitPanelContext?.selectedLeftData,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fetchTableDataDebounced = useCallback(
|
const fetchTableDataDebounced = useCallback(
|
||||||
|
|
@ -2096,18 +2059,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
||||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
||||||
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
|
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
|
||||||
|
|
||||||
console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", {
|
|
||||||
splitPanelPosition,
|
|
||||||
currentSplitPosition,
|
|
||||||
effectiveSplitPosition,
|
|
||||||
hasSplitPanelContext: !!splitPanelContext,
|
|
||||||
disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
|
||||||
if (!isCurrentlySelected) {
|
if (!isCurrentlySelected) {
|
||||||
// 선택된 경우: 데이터 저장
|
// 선택된 경우: 데이터 저장
|
||||||
splitPanelContext.setSelectedLeftData(row);
|
splitPanelContext.setSelectedLeftData(row);
|
||||||
|
|
@ -2125,57 +2077,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 셀 클릭 핸들러 (포커스 설정 + 행 선택)
|
// 🆕 셀 클릭 핸들러 (포커스 설정)
|
||||||
const handleCellClick = (rowIndex: number, colIndex: number, e: React.MouseEvent) => {
|
const handleCellClick = (rowIndex: number, colIndex: number, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setFocusedCell({ rowIndex, colIndex });
|
setFocusedCell({ rowIndex, colIndex });
|
||||||
// 테이블 컨테이너에 포커스 설정 (키보드 이벤트 수신용)
|
// 테이블 컨테이너에 포커스 설정 (키보드 이벤트 수신용)
|
||||||
tableContainerRef.current?.focus();
|
tableContainerRef.current?.focus();
|
||||||
|
|
||||||
// 🆕 분할 패널 내에서 셀 클릭 시에도 해당 행 선택 처리
|
|
||||||
// filteredData에서 해당 행의 데이터 가져오기
|
|
||||||
const row = filteredData[rowIndex];
|
|
||||||
if (!row) return;
|
|
||||||
|
|
||||||
const rowKey = getRowKey(row, rowIndex);
|
|
||||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
|
||||||
|
|
||||||
// 분할 패널 컨텍스트가 있고, 좌측 화면인 경우에만 행 선택 및 데이터 전달
|
|
||||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
|
||||||
|
|
||||||
console.log("🔗 [TableList] 셀 클릭 - 분할 패널 위치 확인:", {
|
|
||||||
rowIndex,
|
|
||||||
colIndex,
|
|
||||||
splitPanelPosition,
|
|
||||||
currentSplitPosition,
|
|
||||||
effectiveSplitPosition,
|
|
||||||
hasSplitPanelContext: !!splitPanelContext,
|
|
||||||
isCurrentlySelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
|
||||||
// 이미 선택된 행과 다른 행을 클릭한 경우에만 처리
|
|
||||||
if (!isCurrentlySelected) {
|
|
||||||
// 기존 선택 해제하고 새 행 선택
|
|
||||||
setSelectedRows(new Set([rowKey]));
|
|
||||||
setIsAllSelected(false);
|
|
||||||
|
|
||||||
// 분할 패널 컨텍스트에 데이터 저장
|
|
||||||
splitPanelContext.setSelectedLeftData(row);
|
|
||||||
console.log("🔗 [TableList] 셀 클릭으로 분할 패널 좌측 데이터 저장:", {
|
|
||||||
row,
|
|
||||||
parentDataMapping: splitPanelContext.parentDataMapping,
|
|
||||||
});
|
|
||||||
|
|
||||||
// onSelectedRowsChange 콜백 호출
|
|
||||||
if (onSelectedRowsChange) {
|
|
||||||
onSelectedRowsChange([rowKey], [row], sortColumn || undefined, sortDirection);
|
|
||||||
}
|
|
||||||
if (onFormDataChange) {
|
|
||||||
onFormDataChange({ selectedRows: [rowKey], selectedRowsData: [row] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 셀 더블클릭 핸들러 (편집 모드 진입) - visibleColumns 정의 후 사용
|
// 🆕 셀 더블클릭 핸들러 (편집 모드 진입) - visibleColumns 정의 후 사용
|
||||||
|
|
@ -4193,12 +4100,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const fileNames = files.map((f: any) => f.realFileName || f.real_file_name || f.name || "파일").join(", ");
|
const fileNames = files.map((f: any) => f.realFileName || f.real_file_name || f.name || "파일").join(", ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-w-full items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm max-w-full">
|
||||||
<Paperclip className="h-4 w-4 flex-shrink-0 text-gray-500" />
|
<Paperclip className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
||||||
<span className="truncate text-blue-600" title={fileNames}>
|
<span
|
||||||
|
className="text-blue-600 truncate"
|
||||||
|
title={fileNames}
|
||||||
|
>
|
||||||
{fileNames}
|
{fileNames}
|
||||||
</span>
|
</span>
|
||||||
{files.length > 1 && <span className="text-muted-foreground flex-shrink-0 text-xs">({files.length})</span>}
|
{files.length > 1 && (
|
||||||
|
<span className="text-muted-foreground text-xs flex-shrink-0">
|
||||||
|
({files.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -4763,10 +4677,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
fetchTableLabel();
|
fetchTableLabel();
|
||||||
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
||||||
|
|
||||||
// 🆕 우측 화면일 때만 selectedLeftData 변경에 반응하도록 변수 생성
|
|
||||||
const isRightPanel = splitPanelPosition === "right" || currentSplitPosition === "right";
|
|
||||||
const selectedLeftDataForRightPanel = isRightPanel ? splitPanelContext?.selectedLeftData : null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
|
// console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
|
||||||
// isDesignMode,
|
// isDesignMode,
|
||||||
|
|
@ -4790,7 +4700,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
refreshKey,
|
refreshKey,
|
||||||
refreshTrigger, // 강제 새로고침 트리거
|
refreshTrigger, // 강제 새로고침 트리거
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
selectedLeftDataForRightPanel, // 🆕 우측 화면일 때만 좌측 데이터 선택 변경 시 데이터 새로고침
|
splitPanelContext?.selectedLeftData, // 🆕 좌측 데이터 선택 변경 시 데이터 새로고침
|
||||||
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue