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,8 +557,9 @@ 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);
|
||||
|
|
@ -651,9 +664,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
// 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;
|
||||
typeof layout.hierarchy_config === "string" ? JSON.parse(layout.hierarchy_config) : layout.hierarchy_config;
|
||||
const materialTableName = hierarchyConfigParsed?.material?.tableName;
|
||||
|
||||
const locationObjects = loadedObjects.filter(
|
||||
|
|
@ -686,9 +697,30 @@ 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();
|
||||
// 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,9 +41,9 @@ 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);
|
||||
|
|
@ -61,9 +61,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
let hierarchyConfigData: any = null;
|
||||
if (layout.hierarchy_config) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
const locationObjects = loadedObjects.filter(
|
||||
(obj) =>
|
||||
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
|
||||
obj.locaKey
|
||||
obj.locaKey,
|
||||
);
|
||||
|
||||
// 각 Location에 대해 자재 개수 조회 (병렬 처리)
|
||||
|
|
@ -143,7 +141,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
return { ...obj, materialCount: countData.count };
|
||||
}
|
||||
return obj;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -162,9 +160,25 @@ 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();
|
||||
// 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>
|
||||
|
||||
{/* 메인 영역 */}
|
||||
|
|
@ -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