Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
6449eb5ac3
|
|
@ -148,9 +148,19 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
|
||||
switch (format) {
|
||||
case "date":
|
||||
return new Date(value).toLocaleDateString("ko-KR");
|
||||
try {
|
||||
const dateVal = new Date(value);
|
||||
return dateVal.toLocaleDateString("ko-KR", { timeZone: "Asia/Seoul" });
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
case "datetime":
|
||||
return new Date(value).toLocaleString("ko-KR");
|
||||
try {
|
||||
const dateVal = new Date(value);
|
||||
return dateVal.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" });
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
case "number":
|
||||
return Number(value).toLocaleString("ko-KR");
|
||||
case "currency":
|
||||
|
|
|
|||
|
|
@ -2,7 +2,19 @@
|
|||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Save, Loader2, Grid3x3, Move, Box, Package, Truck, Check, ParkingCircle } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Loader2,
|
||||
Grid3x3,
|
||||
Move,
|
||||
Box,
|
||||
Package,
|
||||
Truck,
|
||||
Check,
|
||||
ParkingCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -545,150 +557,170 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
|
||||
// 레이아웃 데이터 로드
|
||||
const [layoutData, setLayoutData] = useState<{ layout: any; objects: any[] } | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getLayoutById(layoutId);
|
||||
// 레이아웃 로드 함수
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getLayoutById(layoutId);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { layout, objects } = response.data;
|
||||
setLayoutData({ layout, objects }); // 레이아웃 데이터 저장
|
||||
if (response.success && response.data) {
|
||||
const { layout, objects } = response.data;
|
||||
setLayoutData({ layout, objects }); // 레이아웃 데이터 저장
|
||||
|
||||
// 외부 DB 연결 ID 복원
|
||||
if (layout.external_db_connection_id) {
|
||||
setSelectedDbConnection(layout.external_db_connection_id);
|
||||
}
|
||||
|
||||
// 계층 구조 설정 로드
|
||||
if (layout.hierarchy_config) {
|
||||
try {
|
||||
// hierarchy_config가 문자열이면 파싱, 객체면 그대로 사용
|
||||
const config =
|
||||
typeof layout.hierarchy_config === "string"
|
||||
? JSON.parse(layout.hierarchy_config)
|
||||
: layout.hierarchy_config;
|
||||
setHierarchyConfig(config);
|
||||
|
||||
// 선택된 테이블 정보도 복원
|
||||
const newSelectedTables: any = {
|
||||
warehouse: config.warehouse?.tableName || "",
|
||||
area: "",
|
||||
location: "",
|
||||
material: "",
|
||||
};
|
||||
|
||||
if (config.levels && config.levels.length > 0) {
|
||||
// 레벨 1 = Area
|
||||
if (config.levels[0]?.tableName) {
|
||||
newSelectedTables.area = config.levels[0].tableName;
|
||||
}
|
||||
// 레벨 2 = Location
|
||||
if (config.levels[1]?.tableName) {
|
||||
newSelectedTables.location = config.levels[1].tableName;
|
||||
}
|
||||
}
|
||||
|
||||
// 자재 테이블 정보
|
||||
if (config.material?.tableName) {
|
||||
newSelectedTables.material = config.material.tableName;
|
||||
}
|
||||
|
||||
setSelectedTables(newSelectedTables);
|
||||
} catch (e) {
|
||||
console.error("계층 구조 설정 파싱 실패:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 객체 데이터 변환 (DB -> PlacedObject)
|
||||
const loadedObjects: PlacedObject[] = (objects as unknown as DbObject[]).map((obj) => ({
|
||||
id: obj.id,
|
||||
type: obj.object_type,
|
||||
name: obj.object_name,
|
||||
position: {
|
||||
x: parseFloat(obj.position_x),
|
||||
y: parseFloat(obj.position_y),
|
||||
z: parseFloat(obj.position_z),
|
||||
},
|
||||
size: {
|
||||
x: parseFloat(obj.size_x),
|
||||
y: parseFloat(obj.size_y),
|
||||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: obj.color,
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
|
||||
materialPreview:
|
||||
obj.loc_type === "STP" || !obj.material_preview_height
|
||||
? undefined
|
||||
: { height: parseFloat(obj.material_preview_height) },
|
||||
parentId: obj.parent_id,
|
||||
displayOrder: obj.display_order,
|
||||
locked: obj.locked,
|
||||
visible: obj.visible !== false,
|
||||
hierarchyLevel: obj.hierarchy_level || 1,
|
||||
parentKey: obj.parent_key,
|
||||
externalKey: obj.external_key,
|
||||
}));
|
||||
|
||||
setPlacedObjects(loadedObjects);
|
||||
|
||||
// 다음 임시 ID 설정 (기존 ID 중 최소값 - 1)
|
||||
const minId = Math.min(...loadedObjects.map((o) => o.id), 0);
|
||||
setNextObjectId(minId - 1);
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
toast({
|
||||
title: "레이아웃 불러오기 완료",
|
||||
description: `${loadedObjects.length}개의 객체를 불러왔습니다.`,
|
||||
});
|
||||
|
||||
// Location 객체들의 자재 개수 로드 (직접 dbConnectionId와 materialTableName 전달)
|
||||
const dbConnectionId = layout.external_db_connection_id;
|
||||
const hierarchyConfigParsed =
|
||||
typeof layout.hierarchy_config === "string"
|
||||
? JSON.parse(layout.hierarchy_config)
|
||||
: layout.hierarchy_config;
|
||||
const materialTableName = hierarchyConfigParsed?.material?.tableName;
|
||||
|
||||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" ||
|
||||
obj.type === "location-stp" ||
|
||||
obj.type === "location-temp" ||
|
||||
obj.type === "location-dest") &&
|
||||
obj.locaKey,
|
||||
);
|
||||
if (locationObjects.length > 0 && dbConnectionId && materialTableName) {
|
||||
const locaKeys = locationObjects.map((obj) => obj.locaKey!);
|
||||
setTimeout(() => {
|
||||
loadMaterialCountsForLocations(locaKeys, dbConnectionId, materialTableName);
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.error || "레이아웃 조회 실패");
|
||||
// 외부 DB 연결 ID 복원
|
||||
if (layout.external_db_connection_id) {
|
||||
setSelectedDbConnection(layout.external_db_connection_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("레이아웃 로드 실패:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "레이아웃을 불러오는데 실패했습니다.";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 계층 구조 설정 로드
|
||||
if (layout.hierarchy_config) {
|
||||
try {
|
||||
// hierarchy_config가 문자열이면 파싱, 객체면 그대로 사용
|
||||
const config =
|
||||
typeof layout.hierarchy_config === "string"
|
||||
? JSON.parse(layout.hierarchy_config)
|
||||
: layout.hierarchy_config;
|
||||
setHierarchyConfig(config);
|
||||
|
||||
// 선택된 테이블 정보도 복원
|
||||
const newSelectedTables: any = {
|
||||
warehouse: config.warehouse?.tableName || "",
|
||||
area: "",
|
||||
location: "",
|
||||
material: "",
|
||||
};
|
||||
|
||||
if (config.levels && config.levels.length > 0) {
|
||||
// 레벨 1 = Area
|
||||
if (config.levels[0]?.tableName) {
|
||||
newSelectedTables.area = config.levels[0].tableName;
|
||||
}
|
||||
// 레벨 2 = Location
|
||||
if (config.levels[1]?.tableName) {
|
||||
newSelectedTables.location = config.levels[1].tableName;
|
||||
}
|
||||
}
|
||||
|
||||
// 자재 테이블 정보
|
||||
if (config.material?.tableName) {
|
||||
newSelectedTables.material = config.material.tableName;
|
||||
}
|
||||
|
||||
setSelectedTables(newSelectedTables);
|
||||
} catch (e) {
|
||||
console.error("계층 구조 설정 파싱 실패:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 객체 데이터 변환 (DB -> PlacedObject)
|
||||
const loadedObjects: PlacedObject[] = (objects as unknown as DbObject[]).map((obj) => ({
|
||||
id: obj.id,
|
||||
type: obj.object_type,
|
||||
name: obj.object_name,
|
||||
position: {
|
||||
x: parseFloat(obj.position_x),
|
||||
y: parseFloat(obj.position_y),
|
||||
z: parseFloat(obj.position_z),
|
||||
},
|
||||
size: {
|
||||
x: parseFloat(obj.size_x),
|
||||
y: parseFloat(obj.size_y),
|
||||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: obj.color,
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
|
||||
materialPreview:
|
||||
obj.loc_type === "STP" || !obj.material_preview_height
|
||||
? undefined
|
||||
: { height: parseFloat(obj.material_preview_height) },
|
||||
parentId: obj.parent_id,
|
||||
displayOrder: obj.display_order,
|
||||
locked: obj.locked,
|
||||
visible: obj.visible !== false,
|
||||
hierarchyLevel: obj.hierarchy_level || 1,
|
||||
parentKey: obj.parent_key,
|
||||
externalKey: obj.external_key,
|
||||
}));
|
||||
|
||||
setPlacedObjects(loadedObjects);
|
||||
|
||||
// 다음 임시 ID 설정 (기존 ID 중 최소값 - 1)
|
||||
const minId = Math.min(...loadedObjects.map((o) => o.id), 0);
|
||||
setNextObjectId(minId - 1);
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
toast({
|
||||
title: "레이아웃 불러오기 완료",
|
||||
description: `${loadedObjects.length}개의 객체를 불러왔습니다.`,
|
||||
});
|
||||
|
||||
// Location 객체들의 자재 개수 로드 (직접 dbConnectionId와 materialTableName 전달)
|
||||
const dbConnectionId = layout.external_db_connection_id;
|
||||
const hierarchyConfigParsed =
|
||||
typeof layout.hierarchy_config === "string" ? JSON.parse(layout.hierarchy_config) : layout.hierarchy_config;
|
||||
const materialTableName = hierarchyConfigParsed?.material?.tableName;
|
||||
|
||||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" ||
|
||||
obj.type === "location-stp" ||
|
||||
obj.type === "location-temp" ||
|
||||
obj.type === "location-dest") &&
|
||||
obj.locaKey,
|
||||
);
|
||||
if (locationObjects.length > 0 && dbConnectionId && materialTableName) {
|
||||
const locaKeys = locationObjects.map((obj) => obj.locaKey!);
|
||||
setTimeout(() => {
|
||||
loadMaterialCountsForLocations(locaKeys, dbConnectionId, materialTableName);
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.error || "레이아웃 조회 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("레이아웃 로드 실패:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "레이아웃을 불러오는데 실패했습니다.";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 위젯 새로고침 핸들러
|
||||
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();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layoutId]); // toast 제거
|
||||
}, [layoutId]);
|
||||
|
||||
// 외부 DB 연결 자동 선택 (externalDbConnections 로드 완료 시)
|
||||
useEffect(() => {
|
||||
|
|
@ -1052,7 +1084,11 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
};
|
||||
|
||||
// Location별 자재 개수 로드 (locaKeys를 직접 받음)
|
||||
const loadMaterialCountsForLocations = async (locaKeys: string[], dbConnectionId?: number, materialTableName?: string) => {
|
||||
const loadMaterialCountsForLocations = async (
|
||||
locaKeys: string[],
|
||||
dbConnectionId?: number,
|
||||
materialTableName?: string,
|
||||
) => {
|
||||
const connectionId = dbConnectionId || selectedDbConnection;
|
||||
const tableName = materialTableName || selectedTables.material;
|
||||
if (!connectionId || locaKeys.length === 0) return;
|
||||
|
|
@ -1073,10 +1109,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
}
|
||||
// 백엔드 응답 필드명: location_key, count (대소문자 모두 체크)
|
||||
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) {
|
||||
// count 또는 material_count 필드 사용
|
||||
|
|
@ -1527,6 +1560,16 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
|
||||
<div className="flex items-center gap-2">
|
||||
{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}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
|
|
@ -1620,27 +1663,20 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
<SelectValue placeholder={loadingTemplates ? "로딩 중..." : "템플릿 선택..."} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{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) => (
|
||||
<SelectItem key={tpl.id} value={tpl.id} className="text-xs">
|
||||
<div className="flex flex-col">
|
||||
<span>{tpl.name}</span>
|
||||
{tpl.description && (
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{tpl.description}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[10px]">{tpl.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
|
@ -1704,17 +1740,11 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
}}
|
||||
onLoadColumns={async (tableName: string) => {
|
||||
try {
|
||||
const response = await ExternalDbConnectionAPI.getTableColumns(
|
||||
selectedDbConnection,
|
||||
tableName,
|
||||
);
|
||||
const response = await ExternalDbConnectionAPI.getTableColumns(selectedDbConnection, tableName);
|
||||
if (response.success && response.data) {
|
||||
// 컬럼 정보 객체 배열로 반환 (이름 + 설명 + PK 플래그)
|
||||
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,
|
||||
description: col.description || col.COLUMN_COMMENT || undefined,
|
||||
is_primary_key: col.is_primary_key ?? col.IS_PRIMARY_KEY,
|
||||
|
|
@ -2354,10 +2384,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
>
|
||||
취소
|
||||
</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>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Loader2, Search, X, Grid3x3, Package, ParkingCircle } from "lucide-react";
|
||||
import { Loader2, Search, X, Grid3x3, Package, ParkingCircle, RefreshCw } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -41,130 +41,144 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
// 검색 및 필터
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filterType, setFilterType] = useState<string>("all");
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// 레이아웃 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getLayoutById(layoutId);
|
||||
// 레이아웃 데이터 로드 함수
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getLayoutById(layoutId);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { layout, objects } = response.data;
|
||||
if (response.success && response.data) {
|
||||
const { layout, objects } = response.data;
|
||||
|
||||
// 레이아웃 정보 저장 (snake_case와 camelCase 모두 지원)
|
||||
setLayoutName(layout.layout_name || layout.layoutName);
|
||||
const dbConnectionId = layout.external_db_connection_id || layout.externalDbConnectionId;
|
||||
setExternalDbConnectionId(dbConnectionId);
|
||||
// 레이아웃 정보 저장 (snake_case와 camelCase 모두 지원)
|
||||
setLayoutName(layout.layout_name || layout.layoutName);
|
||||
const dbConnectionId = layout.external_db_connection_id || layout.externalDbConnectionId;
|
||||
setExternalDbConnectionId(dbConnectionId);
|
||||
|
||||
// hierarchy_config 저장
|
||||
let hierarchyConfigData: any = null;
|
||||
if (layout.hierarchy_config) {
|
||||
hierarchyConfigData =
|
||||
typeof layout.hierarchy_config === "string"
|
||||
? JSON.parse(layout.hierarchy_config)
|
||||
: layout.hierarchy_config;
|
||||
setHierarchyConfig(hierarchyConfigData);
|
||||
}
|
||||
// hierarchy_config 저장
|
||||
let hierarchyConfigData: any = null;
|
||||
if (layout.hierarchy_config) {
|
||||
hierarchyConfigData =
|
||||
typeof layout.hierarchy_config === "string" ? JSON.parse(layout.hierarchy_config) : layout.hierarchy_config;
|
||||
setHierarchyConfig(hierarchyConfigData);
|
||||
}
|
||||
|
||||
// 객체 데이터 변환
|
||||
const loadedObjects: PlacedObject[] = objects.map((obj: any) => {
|
||||
const objectType = obj.object_type;
|
||||
return {
|
||||
id: obj.id,
|
||||
type: objectType,
|
||||
name: obj.object_name,
|
||||
position: {
|
||||
x: parseFloat(obj.position_x),
|
||||
y: parseFloat(obj.position_y),
|
||||
z: parseFloat(obj.position_z),
|
||||
},
|
||||
size: {
|
||||
x: parseFloat(obj.size_x),
|
||||
y: parseFloat(obj.size_y),
|
||||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: getObjectColor(objectType, obj.color), // 저장된 색상 우선, 없으면 타입별 기본 색상
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
|
||||
materialPreview:
|
||||
obj.loc_type === "STP" || !obj.material_preview_height
|
||||
? undefined
|
||||
: { height: parseFloat(obj.material_preview_height) },
|
||||
parentId: obj.parent_id,
|
||||
displayOrder: obj.display_order,
|
||||
locked: obj.locked,
|
||||
visible: obj.visible !== false,
|
||||
hierarchyLevel: obj.hierarchy_level,
|
||||
parentKey: obj.parent_key,
|
||||
externalKey: obj.external_key,
|
||||
};
|
||||
// 객체 데이터 변환
|
||||
const loadedObjects: PlacedObject[] = objects.map((obj: any) => {
|
||||
const objectType = obj.object_type;
|
||||
return {
|
||||
id: obj.id,
|
||||
type: objectType,
|
||||
name: obj.object_name,
|
||||
position: {
|
||||
x: parseFloat(obj.position_x),
|
||||
y: parseFloat(obj.position_y),
|
||||
z: parseFloat(obj.position_z),
|
||||
},
|
||||
size: {
|
||||
x: parseFloat(obj.size_x),
|
||||
y: parseFloat(obj.size_y),
|
||||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: getObjectColor(objectType, obj.color), // 저장된 색상 우선, 없으면 타입별 기본 색상
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
|
||||
materialPreview:
|
||||
obj.loc_type === "STP" || !obj.material_preview_height
|
||||
? undefined
|
||||
: { height: parseFloat(obj.material_preview_height) },
|
||||
parentId: obj.parent_id,
|
||||
displayOrder: obj.display_order,
|
||||
locked: obj.locked,
|
||||
visible: obj.visible !== false,
|
||||
hierarchyLevel: obj.hierarchy_level,
|
||||
parentKey: obj.parent_key,
|
||||
externalKey: obj.external_key,
|
||||
};
|
||||
});
|
||||
|
||||
setPlacedObjects(loadedObjects);
|
||||
|
||||
// 외부 DB 연결이 있고 자재 설정이 있으면, 각 Location의 실제 자재 개수 조회
|
||||
if (dbConnectionId && hierarchyConfigData?.material) {
|
||||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
||||
obj.locaKey,
|
||||
);
|
||||
|
||||
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
||||
const materialCountPromises = locationObjects.map(async (obj) => {
|
||||
try {
|
||||
const matResponse = await getMaterials(dbConnectionId, {
|
||||
tableName: hierarchyConfigData.material.tableName,
|
||||
keyColumn: hierarchyConfigData.material.keyColumn,
|
||||
locationKeyColumn: hierarchyConfigData.material.locationKeyColumn,
|
||||
layerColumn: hierarchyConfigData.material.layerColumn,
|
||||
locaKey: obj.locaKey!,
|
||||
});
|
||||
if (matResponse.success && matResponse.data) {
|
||||
return { id: obj.id, count: matResponse.data.length };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`자재 개수 조회 실패 (${obj.locaKey}):`, e);
|
||||
}
|
||||
return { id: obj.id, count: 0 };
|
||||
});
|
||||
|
||||
setPlacedObjects(loadedObjects);
|
||||
const materialCounts = await Promise.all(materialCountPromises);
|
||||
|
||||
// 외부 DB 연결이 있고 자재 설정이 있으면, 각 Location의 실제 자재 개수 조회
|
||||
if (dbConnectionId && hierarchyConfigData?.material) {
|
||||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
||||
obj.locaKey
|
||||
);
|
||||
|
||||
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
||||
const materialCountPromises = locationObjects.map(async (obj) => {
|
||||
try {
|
||||
const matResponse = await getMaterials(dbConnectionId, {
|
||||
tableName: hierarchyConfigData.material.tableName,
|
||||
keyColumn: hierarchyConfigData.material.keyColumn,
|
||||
locationKeyColumn: hierarchyConfigData.material.locationKeyColumn,
|
||||
layerColumn: hierarchyConfigData.material.layerColumn,
|
||||
locaKey: obj.locaKey!,
|
||||
});
|
||||
if (matResponse.success && matResponse.data) {
|
||||
return { id: obj.id, count: matResponse.data.length };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`자재 개수 조회 실패 (${obj.locaKey}):`, e);
|
||||
// materialCount 업데이트
|
||||
setPlacedObjects((prev) =>
|
||||
prev.map((obj) => {
|
||||
const countData = materialCounts.find((m) => m.id === obj.id);
|
||||
if (countData && countData.count > 0) {
|
||||
return { ...obj, materialCount: countData.count };
|
||||
}
|
||||
return { id: obj.id, count: 0 };
|
||||
});
|
||||
|
||||
const materialCounts = await Promise.all(materialCountPromises);
|
||||
|
||||
// materialCount 업데이트
|
||||
setPlacedObjects((prev) =>
|
||||
prev.map((obj) => {
|
||||
const countData = materialCounts.find((m) => m.id === obj.id);
|
||||
if (countData && countData.count > 0) {
|
||||
return { ...obj, materialCount: countData.count };
|
||||
}
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.error || "레이아웃 조회 실패");
|
||||
return obj;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("레이아웃 로드 실패:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "레이아웃을 불러오는데 실패했습니다.";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
throw new Error(response.error || "레이아웃 조회 실패");
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("레이아웃 로드 실패:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "레이아웃을 불러오는데 실패했습니다.";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "오류",
|
||||
description: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 위젯 새로고침 핸들러
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
setSelectedObject(null);
|
||||
setMaterials([]);
|
||||
setShowInfoPanel(false);
|
||||
await loadLayout();
|
||||
setIsRefreshing(false);
|
||||
toast({
|
||||
title: "새로고침 완료",
|
||||
description: "데이터가 갱신되었습니다.",
|
||||
});
|
||||
};
|
||||
|
||||
// 초기 로드
|
||||
useEffect(() => {
|
||||
loadLayout();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layoutId]); // toast 제거 - 무한 루프 방지
|
||||
}, [layoutId]);
|
||||
|
||||
// Location의 자재 목록 로드
|
||||
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
|
||||
|
|
@ -322,6 +336,16 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
<h2 className="text-lg font-semibold">{layoutName || "디지털 트윈 야드"}</h2>
|
||||
<p className="text-muted-foreground text-sm">읽기 전용 뷰</p>
|
||||
</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>
|
||||
|
||||
{/* 메인 영역 */}
|
||||
|
|
@ -404,59 +428,59 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
// Area가 없으면 기존 평면 리스트 유지
|
||||
if (areaObjects.length === 0) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{filteredObjects.map((obj) => {
|
||||
let typeLabel = obj.type;
|
||||
if (obj.type === "location-bed") typeLabel = "베드(BED)";
|
||||
else if (obj.type === "location-stp") typeLabel = "정차포인트(STP)";
|
||||
else if (obj.type === "location-temp") typeLabel = "임시베드(TMP)";
|
||||
else if (obj.type === "location-dest") typeLabel = "지정착지(DES)";
|
||||
else if (obj.type === "crane-mobile") typeLabel = "크레인";
|
||||
else if (obj.type === "area") typeLabel = "Area";
|
||||
else if (obj.type === "rack") typeLabel = "랙";
|
||||
<div className="space-y-2">
|
||||
{filteredObjects.map((obj) => {
|
||||
let typeLabel = obj.type;
|
||||
if (obj.type === "location-bed") typeLabel = "베드(BED)";
|
||||
else if (obj.type === "location-stp") typeLabel = "정차포인트(STP)";
|
||||
else if (obj.type === "location-temp") typeLabel = "임시베드(TMP)";
|
||||
else if (obj.type === "location-dest") typeLabel = "지정착지(DES)";
|
||||
else if (obj.type === "crane-mobile") typeLabel = "크레인";
|
||||
else if (obj.type === "area") typeLabel = "Area";
|
||||
else if (obj.type === "rack") typeLabel = "랙";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id}
|
||||
onClick={() => handleObjectClick(obj.id)}
|
||||
className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${
|
||||
selectedObject?.id === obj.id ? "ring-primary bg-primary/5 ring-2" : "hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{obj.name}</p>
|
||||
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: obj.color }}
|
||||
/>
|
||||
<span>{typeLabel}</span>
|
||||
return (
|
||||
<div
|
||||
key={obj.id}
|
||||
onClick={() => handleObjectClick(obj.id)}
|
||||
className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${
|
||||
selectedObject?.id === obj.id ? "ring-primary bg-primary/5 ring-2" : "hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{obj.name}</p>
|
||||
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: obj.color }}
|
||||
/>
|
||||
<span>{typeLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{obj.areaKey && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Area: <span className="font-medium">{obj.areaKey}</span>
|
||||
</p>
|
||||
)}
|
||||
{obj.locaKey && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Location: <span className="font-medium">{obj.locaKey}</span>
|
||||
</p>
|
||||
)}
|
||||
{obj.materialCount !== undefined && obj.materialCount > 0 && (
|
||||
<p className="text-xs text-yellow-600">
|
||||
자재: <span className="font-semibold">{obj.materialCount}개</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{obj.areaKey && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Area: <span className="font-medium">{obj.areaKey}</span>
|
||||
</p>
|
||||
)}
|
||||
{obj.locaKey && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Location: <span className="font-medium">{obj.locaKey}</span>
|
||||
</p>
|
||||
)}
|
||||
{obj.materialCount !== undefined && obj.materialCount > 0 && (
|
||||
<p className="text-xs text-yellow-600">
|
||||
자재: <span className="font-semibold">{obj.materialCount}개</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Area가 있는 경우: Area → Location 계층 아코디언
|
||||
|
|
@ -525,8 +549,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
/>
|
||||
</div>
|
||||
<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>
|
||||
{locationObj.locaKey && (
|
||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
|
|
|
|||
|
|
@ -180,9 +180,19 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
|||
|
||||
switch (format) {
|
||||
case "date":
|
||||
return new Date(value).toLocaleDateString("ko-KR");
|
||||
try {
|
||||
const dateVal = new Date(value);
|
||||
return dateVal.toLocaleDateString("ko-KR", { timeZone: "Asia/Seoul" });
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
case "datetime":
|
||||
return new Date(value).toLocaleString("ko-KR");
|
||||
try {
|
||||
const dateVal = new Date(value);
|
||||
return dateVal.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" });
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
case "number":
|
||||
return Number(value).toLocaleString("ko-KR");
|
||||
case "currency":
|
||||
|
|
|
|||
|
|
@ -203,14 +203,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
setTripInfoLoading(identifier);
|
||||
|
||||
try {
|
||||
// user_id 또는 vehicle_number로 조회 (시간은 KST로 변환)
|
||||
// user_id 또는 vehicle_number로 조회 (TIMESTAMPTZ는 변환 불필요)
|
||||
const query = `SELECT
|
||||
id, vehicle_number, user_id,
|
||||
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||
last_trip_start,
|
||||
last_trip_end,
|
||||
last_trip_distance, last_trip_time,
|
||||
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||
last_empty_start,
|
||||
last_empty_end,
|
||||
last_empty_distance, last_empty_time,
|
||||
departure, arrival, status
|
||||
FROM vehicles
|
||||
|
|
@ -281,15 +281,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
if (identifiers.length === 0) return;
|
||||
|
||||
try {
|
||||
// 모든 마커의 운행/공차 정보를 한 번에 조회 (시간은 KST로 변환)
|
||||
// 모든 마커의 운행/공차 정보를 한 번에 조회 (TIMESTAMPTZ는 변환 불필요)
|
||||
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const query = `SELECT
|
||||
id, vehicle_number, user_id,
|
||||
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||
last_trip_start,
|
||||
last_trip_end,
|
||||
last_trip_distance, last_trip_time,
|
||||
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||
last_empty_start,
|
||||
last_empty_end,
|
||||
last_empty_distance, last_empty_time,
|
||||
departure, arrival, status
|
||||
FROM vehicles
|
||||
|
|
|
|||
|
|
@ -1506,6 +1506,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
tableName: tableConfig.selectedTable,
|
||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||
linkedFilters: splitPanelContext?.linkedFilters,
|
||||
splitPanelPosition: splitPanelPosition,
|
||||
});
|
||||
|
||||
if (splitPanelContext) {
|
||||
|
|
@ -1537,6 +1538,39 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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) {
|
||||
console.log("🔗 [TableList] 연결 필터 적용:", linkedFilterValues);
|
||||
}
|
||||
|
|
@ -1749,7 +1783,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
searchTerm,
|
||||
searchValues,
|
||||
isDesignMode,
|
||||
splitPanelContext?.selectedLeftData, // 🆕 연결 필터 변경 시 재조회
|
||||
// 🆕 우측 화면일 때만 selectedLeftData 변경에 반응 (좌측 테이블은 재조회 불필요)
|
||||
splitPanelPosition,
|
||||
currentSplitPosition,
|
||||
splitPanelContext?.selectedLeftData,
|
||||
]);
|
||||
|
||||
const fetchTableDataDebounced = useCallback(
|
||||
|
|
@ -2059,7 +2096,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
||||
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
|
||||
console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", {
|
||||
splitPanelPosition,
|
||||
currentSplitPosition,
|
||||
effectiveSplitPosition,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer,
|
||||
});
|
||||
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (!isCurrentlySelected) {
|
||||
// 선택된 경우: 데이터 저장
|
||||
splitPanelContext.setSelectedLeftData(row);
|
||||
|
|
@ -2077,12 +2125,57 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||
};
|
||||
|
||||
// 🆕 셀 클릭 핸들러 (포커스 설정)
|
||||
// 🆕 셀 클릭 핸들러 (포커스 설정 + 행 선택)
|
||||
const handleCellClick = (rowIndex: number, colIndex: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setFocusedCell({ rowIndex, colIndex });
|
||||
// 테이블 컨테이너에 포커스 설정 (키보드 이벤트 수신용)
|
||||
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 정의 후 사용
|
||||
|
|
@ -4100,19 +4193,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const fileNames = files.map((f: any) => f.realFileName || f.real_file_name || f.name || "파일").join(", ");
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-sm max-w-full">
|
||||
<Paperclip className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
||||
<span
|
||||
className="text-blue-600 truncate"
|
||||
title={fileNames}
|
||||
>
|
||||
<div className="flex max-w-full items-center gap-1.5 text-sm">
|
||||
<Paperclip className="h-4 w-4 flex-shrink-0 text-gray-500" />
|
||||
<span className="truncate text-blue-600" title={fileNames}>
|
||||
{fileNames}
|
||||
</span>
|
||||
{files.length > 1 && (
|
||||
<span className="text-muted-foreground text-xs flex-shrink-0">
|
||||
({files.length})
|
||||
</span>
|
||||
)}
|
||||
{files.length > 1 && <span className="text-muted-foreground flex-shrink-0 text-xs">({files.length})</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4677,6 +4763,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
fetchTableLabel();
|
||||
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
||||
|
||||
// 🆕 우측 화면일 때만 selectedLeftData 변경에 반응하도록 변수 생성
|
||||
const isRightPanel = splitPanelPosition === "right" || currentSplitPosition === "right";
|
||||
const selectedLeftDataForRightPanel = isRightPanel ? splitPanelContext?.selectedLeftData : null;
|
||||
|
||||
useEffect(() => {
|
||||
// console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
|
||||
// isDesignMode,
|
||||
|
|
@ -4700,7 +4790,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
refreshKey,
|
||||
refreshTrigger, // 강제 새로고침 트리거
|
||||
isDesignMode,
|
||||
splitPanelContext?.selectedLeftData, // 🆕 좌측 데이터 선택 변경 시 데이터 새로고침
|
||||
selectedLeftDataForRightPanel, // 🆕 우측 화면일 때만 좌측 데이터 선택 변경 시 데이터 새로고침
|
||||
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
||||
]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue