From e4b1f7e4d8a9a3112c22af7c31bfc9285022b633 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 10:19:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/contexts/SplitPanelContext.tsx | 49 +++++++++++++++++ .../RepeaterFieldGroupRenderer.tsx | 55 +++++++++++++++++-- .../table-list/TableListComponent.tsx | 46 +++++++++++----- 3 files changed, 131 insertions(+), 19 deletions(-) diff --git a/frontend/contexts/SplitPanelContext.tsx b/frontend/contexts/SplitPanelContext.tsx index e5052295..bfb9610b 100644 --- a/frontend/contexts/SplitPanelContext.tsx +++ b/frontend/contexts/SplitPanelContext.tsx @@ -48,6 +48,12 @@ interface SplitPanelContextValue { // screenId로 위치 찾기 getPositionByScreenId: (screenId: number) => SplitPanelPosition | null; + + // 🆕 우측에 추가된 항목 ID 관리 (좌측 테이블에서 필터링용) + addedItemIds: Set; + addItemIds: (ids: string[]) => void; + removeItemIds: (ids: string[]) => void; + clearItemIds: () => void; } const SplitPanelContext = createContext(null); @@ -74,6 +80,9 @@ export function SplitPanelProvider({ // 강제 리렌더링용 상태 const [, forceUpdate] = useState(0); + + // 🆕 우측에 추가된 항목 ID 상태 + const [addedItemIds, setAddedItemIds] = useState>(new Set()); /** * 데이터 수신자 등록 @@ -191,6 +200,38 @@ export function SplitPanelProvider({ [leftScreenId, rightScreenId] ); + /** + * 🆕 추가된 항목 ID 등록 + */ + const addItemIds = useCallback((ids: string[]) => { + setAddedItemIds((prev) => { + const newSet = new Set(prev); + ids.forEach((id) => newSet.add(id)); + logger.debug(`[SplitPanelContext] 항목 ID 추가: ${ids.length}개`, { ids }); + return newSet; + }); + }, []); + + /** + * 🆕 추가된 항목 ID 제거 + */ + const removeItemIds = useCallback((ids: string[]) => { + setAddedItemIds((prev) => { + const newSet = new Set(prev); + ids.forEach((id) => newSet.delete(id)); + logger.debug(`[SplitPanelContext] 항목 ID 제거: ${ids.length}개`, { ids }); + return newSet; + }); + }, []); + + /** + * 🆕 모든 항목 ID 초기화 + */ + const clearItemIds = useCallback(() => { + setAddedItemIds(new Set()); + logger.debug(`[SplitPanelContext] 항목 ID 초기화`); + }, []); + // 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지) const value = React.useMemo(() => ({ splitPanelId, @@ -202,6 +243,10 @@ export function SplitPanelProvider({ getOtherSideReceivers, isInSplitPanel: true, getPositionByScreenId, + addedItemIds, + addItemIds, + removeItemIds, + clearItemIds, }), [ splitPanelId, leftScreenId, @@ -211,6 +256,10 @@ export function SplitPanelProvider({ transferToOtherSide, getOtherSideReceivers, getPositionByScreenId, + addedItemIds, + addItemIds, + removeItemIds, + clearItemIds, ]); return ( diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 3b7fc339..c47ff3c9 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -120,10 +120,15 @@ const RepeaterFieldGroupComponent: React.FC = (props) => setGroupedData(items); // 🆕 원본 데이터 ID 목록 저장 (삭제 추적용) - const itemIds = items.map((item: any) => item.id).filter(Boolean); + const itemIds = items.map((item: any) => String(item.id || item.po_item_id || item.item_id)).filter(Boolean); setOriginalItemIds(itemIds); console.log("📋 [RepeaterFieldGroup] 원본 데이터 ID 목록 저장:", itemIds); + // 🆕 SplitPanelContext에 기존 항목 ID 등록 (좌측 테이블 필터링용) + if (splitPanelContext?.addItemIds && itemIds.length > 0) { + splitPanelContext.addItemIds(itemIds); + } + // onChange 호출하여 부모에게 알림 if (onChange && items.length > 0) { const dataWithMeta = items.map((item: any) => ({ @@ -285,6 +290,14 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // 🆕 groupedData 상태도 직접 업데이트 (UI 즉시 반영) setGroupedData(newItems); + // 🆕 SplitPanelContext에 추가된 항목 ID 등록 (좌측 테이블 필터링용) + if (splitPanelContext?.addItemIds && addedCount > 0) { + const newItemIds = newItems + .map((item: any) => String(item.id || item.po_item_id || item.item_id)) + .filter(Boolean); + splitPanelContext.addItemIds(newItemIds); + } + // JSON 문자열로 변환하여 저장 const jsonValue = JSON.stringify(newItems); console.log("📥 [RepeaterFieldGroup] onChange/onFormDataChange 호출:", { @@ -380,14 +393,44 @@ const RepeaterFieldGroupComponent: React.FC = (props) => }; }, [screenContext?.splitPanelPosition, handleReceiveData, component.id]); + // 🆕 RepeaterInput에서 항목 변경 시 SplitPanelContext의 addedItemIds 동기화 + const handleRepeaterChange = useCallback((newValue: any[]) => { + // 배열을 JSON 문자열로 변환하여 저장 + const jsonValue = JSON.stringify(newValue); + onChange?.(jsonValue); + + // 🆕 groupedData 상태도 업데이트 + setGroupedData(newValue); + + // 🆕 SplitPanelContext의 addedItemIds 동기화 + if (splitPanelContext?.isInSplitPanel && screenContext?.splitPanelPosition === "right") { + // 현재 항목들의 ID 목록 + const currentIds = newValue + .map((item: any) => String(item.id || item.po_item_id || item.item_id)) + .filter(Boolean); + + // 기존 addedItemIds와 비교하여 삭제된 ID 찾기 + const addedIds = splitPanelContext.addedItemIds; + const removedIds = Array.from(addedIds).filter(id => !currentIds.includes(id)); + + if (removedIds.length > 0) { + console.log("🗑️ [RepeaterFieldGroup] 삭제된 항목 ID 제거:", removedIds); + splitPanelContext.removeItemIds(removedIds); + } + + // 새로 추가된 ID가 있으면 등록 + const newIds = currentIds.filter((id: string) => !addedIds.has(id)); + if (newIds.length > 0) { + console.log("➕ [RepeaterFieldGroup] 새 항목 ID 추가:", newIds); + splitPanelContext.addItemIds(newIds); + } + } + }, [onChange, splitPanelContext, screenContext?.splitPanelPosition]); + return ( { - // 배열을 JSON 문자열로 변환하여 저장 - const jsonValue = JSON.stringify(newValue); - onChange?.(jsonValue); - }} + onChange={handleRepeaterChange} config={config} disabled={disabled} readonly={readonly} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a0f01727..f0690943 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -330,6 +330,25 @@ export const TableListComponent: React.FC = ({ const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + + // 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + const filteredData = useMemo(() => { + // 분할 패널 좌측에 있고, 우측에 추가된 항목이 있는 경우에만 필터링 + if (splitPanelPosition === "left" && splitPanelContext?.addedItemIds && splitPanelContext.addedItemIds.size > 0) { + const addedIds = splitPanelContext.addedItemIds; + const filtered = data.filter((row) => { + const rowId = String(row.id || row.po_item_id || row.item_id || ""); + return !addedIds.has(rowId); + }); + console.log("🔍 [TableList] 우측 추가 항목 필터링:", { + originalCount: data.length, + filteredCount: filtered.length, + addedIdsCount: addedIds.size, + }); + return filtered; + } + return data; + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); @@ -438,8 +457,8 @@ export const TableListComponent: React.FC = ({ componentType: "table-list", getSelectedData: () => { - // 선택된 행의 실제 데이터 반환 - const selectedData = data.filter((row) => { + // 🆕 필터링된 데이터에서 선택된 행만 반환 (우측에 추가된 항목 제외) + const selectedData = filteredData.filter((row) => { const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || ""); return selectedRows.has(rowId); }); @@ -447,7 +466,8 @@ export const TableListComponent: React.FC = ({ }, getAllData: () => { - return data; + // 🆕 필터링된 데이터 반환 + return filteredData; }, clearSelection: () => { @@ -1375,31 +1395,31 @@ export const TableListComponent: React.FC = ({ }); } - const allRowsSelected = data.every((row, index) => newSelectedRows.has(getRowKey(row, index))); - setIsAllSelected(allRowsSelected && data.length > 0); + const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index))); + setIsAllSelected(allRowsSelected && filteredData.length > 0); }; const handleSelectAll = (checked: boolean) => { if (checked) { - const allKeys = data.map((row, index) => getRowKey(row, index)); + const allKeys = filteredData.map((row, index) => getRowKey(row, index)); const newSelectedRows = new Set(allKeys); setSelectedRows(newSelectedRows); setIsAllSelected(true); if (onSelectedRowsChange) { - onSelectedRowsChange(Array.from(newSelectedRows), data, sortColumn || undefined, sortDirection); + onSelectedRowsChange(Array.from(newSelectedRows), filteredData, sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: Array.from(newSelectedRows), - selectedRowsData: data, + selectedRowsData: filteredData, }); } // 🆕 modalDataStore에 전체 데이터 저장 - if (tableConfig.selectedTable && data.length > 0) { + if (tableConfig.selectedTable && filteredData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { - const modalItems = data.map((row, idx) => ({ + const modalItems = filteredData.map((row, idx) => ({ id: getRowKey(row, idx), originalData: row, additionalData: {}, @@ -2003,11 +2023,11 @@ export const TableListComponent: React.FC = ({ // 데이터 그룹화 const groupedData = useMemo((): GroupedData[] => { - if (groupByColumns.length === 0 || data.length === 0) return []; + if (groupByColumns.length === 0 || filteredData.length === 0) return []; const grouped = new Map(); - data.forEach((item) => { + filteredData.forEach((item) => { // 그룹 키 생성: "통화:KRW > 단위:EA" const keyParts = groupByColumns.map((col) => { // 카테고리/엔티티 타입인 경우 _name 필드 사용 @@ -2706,7 +2726,7 @@ export const TableListComponent: React.FC = ({ }) ) : ( // 일반 렌더링 (그룹 없음) - data.map((row, index) => ( + filteredData.map((row, index) => (