diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index b89ef902..43ccce32 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { AuthenticatedRequest } from "../types/auth"; import { syncScreenGroupsToMenu, syncMenuToScreenGroups, @@ -16,9 +17,9 @@ const pool = getPool(); // ============================================================ // 화면 그룹 목록 조회 -export const getScreenGroups = async (req: Request, res: Response) => { +export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { page = 1, size = 20, searchTerm } = req.query; const offset = (parseInt(page as string) - 1) * parseInt(size as string); @@ -90,10 +91,10 @@ export const getScreenGroups = async (req: Request, res: Response) => { }; // 화면 그룹 상세 조회 -export const getScreenGroup = async (req: Request, res: Response) => { +export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = ` SELECT sg.*, @@ -136,10 +137,10 @@ export const getScreenGroup = async (req: Request, res: Response) => { }; // 화면 그룹 생성 -export const createScreenGroup = async (req: Request, res: Response) => { +export const createScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; if (!group_name || !group_code) { @@ -210,10 +211,10 @@ export const createScreenGroup = async (req: Request, res: Response) => { }; // 화면 그룹 수정 -export const updateScreenGroup = async (req: Request, res: Response) => { +export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const userCompanyCode = (req.user as any).companyCode; + const userCompanyCode = req.user?.companyCode || "*"; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; // 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 현재 그룹의 회사 유지 @@ -299,11 +300,11 @@ export const updateScreenGroup = async (req: Request, res: Response) => { }; // 화면 그룹 삭제 -export const deleteScreenGroup = async (req: Request, res: Response) => { +export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response) => { const client = await pool.connect(); try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; await client.query('BEGIN'); @@ -366,10 +367,10 @@ export const deleteScreenGroup = async (req: Request, res: Response) => { // ============================================================ // 그룹에 화면 추가 -export const addScreenToGroup = async (req: Request, res: Response) => { +export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, screen_id, screen_role, display_order, is_default } = req.body; if (!group_id || !screen_id) { @@ -406,10 +407,10 @@ export const addScreenToGroup = async (req: Request, res: Response) => { }; // 그룹에서 화면 제거 -export const removeScreenFromGroup = async (req: Request, res: Response) => { +export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_group_screens WHERE id = $1`; const params: any[] = [id]; @@ -437,10 +438,10 @@ export const removeScreenFromGroup = async (req: Request, res: Response) => { }; // 그룹 내 화면 순서/역할 수정 -export const updateScreenInGroup = async (req: Request, res: Response) => { +export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_role, display_order, is_default } = req.body; let query = ` @@ -476,9 +477,9 @@ export const updateScreenInGroup = async (req: Request, res: Response) => { // ============================================================ // 화면 필드 조인 목록 조회 -export const getFieldJoins = async (req: Request, res: Response) => { +export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id } = req.query; let query = ` @@ -517,10 +518,10 @@ export const getFieldJoins = async (req: Request, res: Response) => { }; // 화면 필드 조인 생성 -export const createFieldJoin = async (req: Request, res: Response) => { +export const createFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { screen_id, layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -558,10 +559,10 @@ export const createFieldJoin = async (req: Request, res: Response) => { }; // 화면 필드 조인 수정 -export const updateFieldJoin = async (req: Request, res: Response) => { +export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -603,10 +604,10 @@ export const updateFieldJoin = async (req: Request, res: Response) => { }; // 화면 필드 조인 삭제 -export const deleteFieldJoin = async (req: Request, res: Response) => { +export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_field_joins WHERE id = $1`; const params: any[] = [id]; @@ -637,9 +638,9 @@ export const deleteFieldJoin = async (req: Request, res: Response) => { // ============================================================ // 데이터 흐름 목록 조회 -export const getDataFlows = async (req: Request, res: Response) => { +export const getDataFlows = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id } = req.query; let query = ` @@ -687,10 +688,10 @@ export const getDataFlows = async (req: Request, res: Response) => { }; // 데이터 흐름 생성 -export const createDataFlow = async (req: Request, res: Response) => { +export const createDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -726,10 +727,10 @@ export const createDataFlow = async (req: Request, res: Response) => { }; // 데이터 흐름 수정 -export const updateDataFlow = async (req: Request, res: Response) => { +export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -769,10 +770,10 @@ export const updateDataFlow = async (req: Request, res: Response) => { }; // 데이터 흐름 삭제 -export const deleteDataFlow = async (req: Request, res: Response) => { +export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_data_flows WHERE id = $1`; const params: any[] = [id]; @@ -803,9 +804,9 @@ export const deleteDataFlow = async (req: Request, res: Response) => { // ============================================================ // 화면-테이블 관계 목록 조회 -export const getTableRelations = async (req: Request, res: Response) => { +export const getTableRelations = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id, group_id } = req.query; let query = ` @@ -852,10 +853,10 @@ export const getTableRelations = async (req: Request, res: Response) => { }; // 화면-테이블 관계 생성 -export const createTableRelation = async (req: Request, res: Response) => { +export const createTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body; if (!screen_id || !table_name) { @@ -885,10 +886,10 @@ export const createTableRelation = async (req: Request, res: Response) => { }; // 화면-테이블 관계 수정 -export const updateTableRelation = async (req: Request, res: Response) => { +export const updateTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body; let query = ` @@ -920,10 +921,10 @@ export const updateTableRelation = async (req: Request, res: Response) => { }; // 화면-테이블 관계 삭제 -export const deleteTableRelation = async (req: Request, res: Response) => { +export const deleteTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_table_relations WHERE id = $1`; const params: any[] = [id]; @@ -953,7 +954,7 @@ export const deleteTableRelation = async (req: Request, res: Response) => { // ============================================================ // 화면 레이아웃 요약 조회 (위젯 타입별 개수, 라벨 목록) -export const getScreenLayoutSummary = async (req: Request, res: Response) => { +export const getScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenId } = req.params; @@ -1021,7 +1022,7 @@ export const getScreenLayoutSummary = async (req: Request, res: Response) => { }; // 여러 화면의 레이아웃 요약 일괄 조회 (미니어처 렌더링용 좌표 포함) -export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => { +export const getMultipleScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -1221,7 +1222,7 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response // ============================================================ // 여러 화면의 서브 테이블 정보 조회 (메인 테이블 → 서브 테이블 관계) -export const getScreenSubTables = async (req: Request, res: Response) => { +export const getScreenSubTables = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -2060,10 +2061,10 @@ export const getScreenSubTables = async (req: Request, res: Response) => { * 화면관리 → 메뉴 동기화 * screen_groups를 menu_info로 동기화 */ -export const syncScreenGroupsToMenuController = async (req: Request, res: Response) => { +export const syncScreenGroupsToMenuController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { targetCompanyCode } = req.body; // 최고 관리자가 특정 회사를 지정한 경우 해당 회사로 @@ -2111,10 +2112,10 @@ export const syncScreenGroupsToMenuController = async (req: Request, res: Respon * 메뉴 → 화면관리 동기화 * menu_info를 screen_groups로 동기화 */ -export const syncMenuToScreenGroupsController = async (req: Request, res: Response) => { +export const syncMenuToScreenGroupsController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { targetCompanyCode } = req.body; // 최고 관리자가 특정 회사를 지정한 경우 해당 회사로 @@ -2161,9 +2162,9 @@ export const syncMenuToScreenGroupsController = async (req: Request, res: Respon /** * 동기화 상태 조회 */ -export const getSyncStatusController = async (req: Request, res: Response) => { +export const getSyncStatusController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; + const userCompanyCode = req.user?.companyCode || "*"; const { targetCompanyCode } = req.query; // 최고 관리자가 특정 회사를 지정한 경우 해당 회사로 @@ -2200,10 +2201,10 @@ export const getSyncStatusController = async (req: Request, res: Response) => { * 전체 회사 동기화 * 모든 회사에 대해 양방향 동기화 수행 (최고 관리자만) */ -export const syncAllCompaniesController = async (req: Request, res: Response) => { +export const syncAllCompaniesController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; // 최고 관리자만 전체 동기화 가능 if (userCompanyCode !== "*") { diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index bdc00019..9135231c 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -384,20 +384,36 @@ export const PivotGridComponent: React.FC = ({ localStorage.setItem(stateStorageKey, JSON.stringify(stateToSave)); }, [fields, pivotState, sortConfig, columnWidths, stateStorageKey]); - // 상태 복원 (localStorage) + // 상태 복원 (localStorage) - 프로덕션 안전성 강화 useEffect(() => { if (typeof window === "undefined") return; - const savedState = localStorage.getItem(stateStorageKey); - if (savedState) { - try { - const parsed = JSON.parse(savedState); - if (parsed.fields) setFields(parsed.fields); - if (parsed.pivotState) setPivotState(parsed.pivotState); - if (parsed.sortConfig) setSortConfig(parsed.sortConfig); - if (parsed.columnWidths) setColumnWidths(parsed.columnWidths); - } catch (e) { - console.warn("피벗 상태 복원 실패:", e); + + try { + const savedState = localStorage.getItem(stateStorageKey); + if (!savedState) return; + + const parsed = JSON.parse(savedState); + + // 필드 복원 시 유효성 검사 (중요!) + if (parsed.fields && Array.isArray(parsed.fields) && parsed.fields.length > 0) { + // 저장된 필드가 현재 데이터와 호환되는지 확인 + const validFields = parsed.fields.filter((f: PivotFieldConfig) => + f && typeof f.field === "string" && typeof f.area === "string" + ); + + if (validFields.length > 0) { + setFields(validFields); + } } + + // 나머지 상태 복원 + if (parsed.pivotState) setPivotState(parsed.pivotState); + if (parsed.sortConfig) setSortConfig(parsed.sortConfig); + if (parsed.columnWidths) setColumnWidths(parsed.columnWidths); + } catch (e) { + console.warn("피벗 상태 복원 실패, localStorage 초기화:", e); + // 손상된 상태는 제거 + localStorage.removeItem(stateStorageKey); } }, [stateStorageKey]); @@ -432,10 +448,20 @@ export const PivotGridComponent: React.FC = ({ // 필터 영역 필드 const filterFields = useMemo( - () => - fields + () => { + const result = fields .filter((f) => f.area === "filter" && f.visible !== false) - .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)), + .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); + + console.log("🔷 [filterFields] 필터 필드 계산:", { + totalFields: fields.length, + filterFieldsCount: result.length, + filterFieldNames: result.map(f => f.field), + allFieldAreas: fields.map(f => ({ field: f.field, area: f.area, visible: f.visible })), + }); + + return result; + }, [fields] ); @@ -502,15 +528,15 @@ export const PivotGridComponent: React.FC = ({ return null; } - const visibleFields = fields.filter((f) => f.visible !== false); + // FieldChooser에서 이미 필드를 완전히 제거하므로 visible 필터링 불필요 // 행, 열, 데이터 영역에 필드가 하나도 없으면 null 반환 (필터는 제외) - if (visibleFields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { + if (fields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { return null; } const result = processPivotData( filteredData, - visibleFields, + fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths ); @@ -528,32 +554,23 @@ export const PivotGridComponent: React.FC = ({ return result; }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]); - // 🆕 초기 로드 시 첫 레벨 자동 확장 + // 초기 로드 시 첫 레벨 자동 확장 useEffect(() => { - if (pivotResult && pivotResult.flatRows.length > 0) { - console.log("🔶 피벗 결과 생성됨:", { - flatRowsCount: pivotResult.flatRows.length, - expandedRowPaths: pivotState.expandedRowPaths.length, - isInitialExpanded, - }); - + if (pivotResult && pivotResult.flatRows.length > 0 && !isInitialExpanded) { // 첫 레벨 행들의 경로 수집 (level 0인 행들) - const firstLevelRows = pivotResult.flatRows.filter(row => row.level === 0 && row.hasChildren); - - console.log("🔶 첫 레벨 행 (level 0, hasChildren):", firstLevelRows.map(r => ({ path: r.path, caption: r.caption }))); + const firstLevelRows = pivotResult.flatRows.filter((row) => row.level === 0 && row.hasChildren); - // 초기 확장이 안 되어 있고, 첫 레벨 행이 있으면 자동 확장 - if (!isInitialExpanded && firstLevelRows.length > 0) { - const firstLevelPaths = firstLevelRows.map(row => row.path); - console.log("🔶 초기 자동 확장 실행:", firstLevelPaths); - setPivotState(prev => ({ + // 첫 레벨 행이 있으면 자동 확장 + if (firstLevelRows.length > 0) { + const firstLevelPaths = firstLevelRows.map((row) => row.path); + setPivotState((prev) => ({ ...prev, expandedRowPaths: firstLevelPaths, })); setIsInitialExpanded(true); } } - }, [pivotResult, isInitialExpanded, pivotState.expandedRowPaths.length]); + }, [pivotResult, isInitialExpanded]); // 조건부 서식용 전체 값 수집 const allCellValues = useMemo(() => { @@ -710,6 +727,15 @@ export const PivotGridComponent: React.FC = ({ // 필드 변경 const handleFieldsChange = useCallback( (newFields: PivotFieldConfig[]) => { + // FieldChooser에서 이미 필드를 완전히 제거하므로 추가 필터링 불필요 + console.log("🔷 [handleFieldsChange] 필드 변경:", { + totalFields: newFields.length, + filterFields: newFields.filter(f => f.area === "filter").length, + filterFieldNames: newFields.filter(f => f.area === "filter").map(f => f.field), + rowFields: newFields.filter(f => f.area === "row").length, + columnFields: newFields.filter(f => f.area === "column").length, + dataFields: newFields.filter(f => f.area === "data").length, + }); setFields(newFields); }, [] @@ -749,31 +775,61 @@ export const PivotGridComponent: React.FC = ({ [onExpandChange] ); - // 전체 확장 + // 전체 확장 (재귀적으로 모든 레벨 확장) const handleExpandAll = useCallback(() => { - if (!pivotResult) return; + if (!pivotResult) { + console.log("❌ [handleExpandAll] pivotResult가 없음"); + return; + } + // 🆕 재귀적으로 모든 가능한 경로 생성 const allRowPaths: string[][] = []; - pivotResult.flatRows.forEach((row) => { - if (row.hasChildren) { - allRowPaths.push(row.path); + const rowFields = fields.filter((f) => f.area === "row" && f.visible !== false); + + // 데이터에서 모든 고유한 경로 추출 + const pathSet = new Set(); + filteredData.forEach((item) => { + for (let depth = 1; depth <= rowFields.length; depth++) { + const path = rowFields.slice(0, depth).map((f) => String(item[f.field] ?? "")); + const pathKey = JSON.stringify(path); + pathSet.add(pathKey); } }); + // Set을 배열로 변환 + pathSet.forEach((pathKey) => { + allRowPaths.push(JSON.parse(pathKey)); + }); + + console.log("🔷 [handleExpandAll] 확장할 행:", { + totalRows: pivotResult.flatRows.length, + rowsWithChildren: allRowPaths.length, + paths: allRowPaths.slice(0, 5), // 처음 5개만 로그 + }); + setPivotState((prev) => ({ ...prev, expandedRowPaths: allRowPaths, expandedColumnPaths: [], })); - }, [pivotResult]); + }, [pivotResult, fields, filteredData]); // 전체 축소 const handleCollapseAll = useCallback(() => { - setPivotState((prev) => ({ - ...prev, - expandedRowPaths: [], - expandedColumnPaths: [], - })); + console.log("🔷 [handleCollapseAll] 전체 축소 실행"); + + setPivotState((prev) => { + console.log("🔷 [handleCollapseAll] 이전 상태:", { + expandedRowPaths: prev.expandedRowPaths.length, + expandedColumnPaths: prev.expandedColumnPaths.length, + }); + + return { + ...prev, + expandedRowPaths: [], + expandedColumnPaths: [], + }; + }); }, []); // 셀 클릭 @@ -888,6 +944,8 @@ export const PivotGridComponent: React.FC = ({ // 인쇄 기능 (PDF 내보내기보다 먼저 정의해야 함) const handlePrint = useCallback(() => { + if (typeof window === "undefined") return; + const printContent = tableRef.current; if (!printContent) return; @@ -988,10 +1046,14 @@ export const PivotGridComponent: React.FC = ({ console.log("피벗 상태가 저장되었습니다."); }, [saveStateToStorage]); - // 상태 초기화 + // 상태 초기화 (확장/축소, 정렬, 필터만 초기화, 필드 설정은 유지) const handleResetState = useCallback(() => { - localStorage.removeItem(stateStorageKey); - setFields(initialFields); + // 로컬 스토리지에서 상태 제거 (SSR 보호) + if (typeof window !== "undefined") { + localStorage.removeItem(stateStorageKey); + } + + // 확장/축소, 정렬, 필터 상태만 초기화 setPivotState({ expandedRowPaths: [], expandedColumnPaths: [], @@ -1002,7 +1064,7 @@ export const PivotGridComponent: React.FC = ({ setColumnWidths({}); setSelectedCell(null); setSelectionRange(null); - }, [stateStorageKey, initialFields]); + }, [stateStorageKey]); // 필드 숨기기/표시 상태 const [hiddenFields, setHiddenFields] = useState>(new Set()); @@ -1019,11 +1081,6 @@ export const PivotGridComponent: React.FC = ({ }); }, []); - // 숨겨진 필드 제외한 활성 필드들 - const visibleFields = useMemo(() => { - return fields.filter((f) => !hiddenFields.has(f.field)); - }, [fields, hiddenFields]); - // 숨겨진 필드 목록 const hiddenFieldsList = useMemo(() => { return fields.filter((f) => hiddenFields.has(f.field)); @@ -1391,8 +1448,8 @@ export const PivotGridComponent: React.FC = ({ variant="ghost" size="sm" className="h-7 px-2" - onClick={handleExpandAll} - title="전체 확장" + onClick={handleCollapseAll} + title="전체 축소" > @@ -1401,8 +1458,8 @@ export const PivotGridComponent: React.FC = ({ variant="ghost" size="sm" className="h-7 px-2" - onClick={handleCollapseAll} - title="전체 축소" + onClick={handleExpandAll} + title="전체 확장" > @@ -1582,19 +1639,25 @@ export const PivotGridComponent: React.FC = ({ } /> diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx index 89fe5128..a948aba0 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx @@ -267,11 +267,13 @@ export const FieldChooser: React.FC = ({ const existingConfig = selectedFields.find((f) => f.field === field.field); if (area === "none") { - // 필드 제거 또는 숨기기 + // 🆕 필드 완전 제거 (visible: false 대신 배열에서 제거) if (existingConfig) { - const newFields = selectedFields.map((f) => - f.field === field.field ? { ...f, visible: false } : f - ); + const newFields = selectedFields.filter((f) => f.field !== field.field); + console.log("🔷 [FieldChooser] 필드 제거:", { + removedField: field.field, + remainingFields: newFields.length, + }); onFieldsChange(newFields); } } else { @@ -282,6 +284,10 @@ export const FieldChooser: React.FC = ({ ? { ...f, area, visible: true } : f ); + console.log("🔷 [FieldChooser] 필드 영역 변경:", { + field: field.field, + newArea: area, + }); onFieldsChange(newFields); } else { // 새 필드 추가 @@ -294,6 +300,10 @@ export const FieldChooser: React.FC = ({ summaryType: area === "data" ? "sum" : undefined, areaIndex: selectedFields.filter((f) => f.area === area).length, }; + console.log("🔷 [FieldChooser] 필드 추가:", { + field: field.field, + area, + }); onFieldsChange([...selectedFields, newField]); } } diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx index fed43afb..08dca70e 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx @@ -25,6 +25,7 @@ import { horizontalListSortingStrategy, useSortable, } from "@dnd-kit/sortable"; +import { useDroppable } from "@dnd-kit/core"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; import { PivotFieldConfig, PivotAreaType } from "../types"; @@ -244,22 +245,31 @@ const DroppableArea: React.FC = ({ const areaFields = fields.filter((f) => f.area === area && f.visible !== false); const fieldIds = areaFields.map((f) => `${area}-${f.field}`); + // 🆕 드롭 가능 영역 설정 + const { setNodeRef, isOver: isOverDroppable } = useDroppable({ + id: area, // "filter", "column", "row", "data" + }); + + const finalIsOver = isOver || isOverDroppable; + return (
{/* 영역 헤더 */} -
+
{icon} {title} {areaFields.length > 0 && ( - + {areaFields.length} )} @@ -267,11 +277,16 @@ const DroppableArea: React.FC = ({ {/* 필드 목록 */} -
+
{areaFields.length === 0 ? ( - - 필드를 여기로 드래그 - +
+ + ← 필드를 여기로 드래그하세요 + +
) : ( areaFields.map((field) => ( = ({ return; } - // 드롭 영역 감지 + // 드롭 영역 감지 (영역 자체의 ID를 우선 확인) const overId = over.id as string; + + // 1. overId가 영역 자체인 경우 (filter, column, row, data) + if (["filter", "column", "row", "data"].includes(overId)) { + setOverArea(overId as PivotAreaType); + console.log("🔷 [handleDragOver] 영역 감지:", overId); + return; + } + + // 2. overId가 필드인 경우 (예: row-part_name) const targetArea = overId.split("-")[0] as PivotAreaType; if (["filter", "column", "row", "data"].includes(targetArea)) { setOverArea(targetArea); + console.log("🔷 [handleDragOver] 필드 영역 감지:", targetArea); } }; // 드래그 종료 const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; + const currentOverArea = overArea; // handleDragOver에서 감지한 영역 저장 setActiveId(null); setOverArea(null); - if (!over) return; + if (!over) { + console.log("🔷 [FieldPanel] 드롭 대상 없음"); + return; + } const activeId = active.id as string; const overId = over.id as string; + console.log("🔷 [FieldPanel] 드래그 종료:", { + activeId, + overId, + detectedOverArea: currentOverArea, + }); + // 필드 정보 파싱 const [sourceArea, sourceField] = activeId.split("-") as [ PivotAreaType, string ]; - const [targetArea] = overId.split("-") as [PivotAreaType, string]; + + // targetArea 결정: handleDragOver에서 감지한 영역 우선 사용 + let targetArea: PivotAreaType; + if (currentOverArea) { + targetArea = currentOverArea; + } else if (["filter", "column", "row", "data"].includes(overId)) { + targetArea = overId as PivotAreaType; + } else { + targetArea = overId.split("-")[0] as PivotAreaType; + } + + console.log("🔷 [FieldPanel] 파싱 결과:", { + sourceArea, + sourceField, + targetArea, + usedOverArea: !!currentOverArea, + }); // 같은 영역 내 정렬 if (sourceArea === targetArea) { @@ -396,6 +447,12 @@ export const FieldPanel: React.FC = ({ // 다른 영역으로 이동 if (["filter", "column", "row", "data"].includes(targetArea)) { + console.log("🔷 [FieldPanel] 영역 이동:", { + field: sourceField, + from: sourceArea, + to: targetArea, + }); + const newFields = fields.map((f) => { if (f.field === sourceField && f.area === sourceArea) { return { @@ -406,6 +463,13 @@ export const FieldPanel: React.FC = ({ } return f; }); + + console.log("🔷 [FieldPanel] 변경된 필드:", { + totalFields: newFields.length, + filterFields: newFields.filter(f => f.area === "filter").length, + changedField: newFields.find(f => f.field === sourceField), + }); + onFieldsChange(newFields); } };