Merge pull request 'feature/screen-management' (#202) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/202
This commit is contained in:
commit
95d3742507
|
|
@ -104,7 +104,7 @@ export class DDLExecutionService {
|
|||
await this.saveTableMetadata(client, tableName, description);
|
||||
|
||||
// 5-3. 컬럼 메타데이터 저장
|
||||
await this.saveColumnMetadata(client, tableName, columns);
|
||||
await this.saveColumnMetadata(client, tableName, columns, userCompanyCode);
|
||||
});
|
||||
|
||||
// 6. 성공 로그 기록
|
||||
|
|
@ -272,7 +272,7 @@ export class DDLExecutionService {
|
|||
await client.query(ddlQuery);
|
||||
|
||||
// 6-2. 컬럼 메타데이터 저장
|
||||
await this.saveColumnMetadata(client, tableName, [column]);
|
||||
await this.saveColumnMetadata(client, tableName, [column], userCompanyCode);
|
||||
});
|
||||
|
||||
// 7. 성공 로그 기록
|
||||
|
|
@ -446,7 +446,8 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
private async saveColumnMetadata(
|
||||
client: any,
|
||||
tableName: string,
|
||||
columns: CreateColumnDefinition[]
|
||||
columns: CreateColumnDefinition[],
|
||||
companyCode: string
|
||||
): Promise<void> {
|
||||
// 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성
|
||||
await client.query(
|
||||
|
|
@ -508,19 +509,19 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
await client.query(
|
||||
`
|
||||
INSERT INTO table_type_columns (
|
||||
table_name, column_name, input_type, detail_settings,
|
||||
table_name, column_name, company_code, input_type, detail_settings,
|
||||
is_nullable, display_order, created_date, updated_date
|
||||
) VALUES (
|
||||
$1, $2, $3, '{}',
|
||||
'Y', $4, now(), now()
|
||||
$1, $2, $3, $4, '{}',
|
||||
'Y', $5, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
ON CONFLICT (table_name, column_name, company_code)
|
||||
DO UPDATE SET
|
||||
input_type = $3,
|
||||
display_order = $4,
|
||||
input_type = $4,
|
||||
display_order = $5,
|
||||
updated_date = now()
|
||||
`,
|
||||
[tableName, defaultCol.name, defaultCol.inputType, defaultCol.order]
|
||||
[tableName, defaultCol.name, companyCode, defaultCol.inputType, defaultCol.order]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -535,20 +536,20 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
await client.query(
|
||||
`
|
||||
INSERT INTO table_type_columns (
|
||||
table_name, column_name, input_type, detail_settings,
|
||||
table_name, column_name, company_code, input_type, detail_settings,
|
||||
is_nullable, display_order, created_date, updated_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
'Y', $5, now(), now()
|
||||
$1, $2, $3, $4, $5,
|
||||
'Y', $6, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
ON CONFLICT (table_name, column_name, company_code)
|
||||
DO UPDATE SET
|
||||
input_type = $3,
|
||||
detail_settings = $4,
|
||||
display_order = $5,
|
||||
input_type = $4,
|
||||
detail_settings = $5,
|
||||
display_order = $6,
|
||||
updated_date = now()
|
||||
`,
|
||||
[tableName, column.name, inputType, detailSettings, i]
|
||||
[tableName, column.name, companyCode, inputType, detailSettings, i]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,29 +36,61 @@ export async function getSiblingMenuObjids(menuObjid: number): Promise<number[]>
|
|||
try {
|
||||
logger.debug("메뉴 스코프 조회 시작", { menuObjid });
|
||||
|
||||
// 1. 현재 메뉴 자신을 포함
|
||||
const menuObjids = [menuObjid];
|
||||
// 1. 현재 메뉴 정보 조회 (부모 ID 확인)
|
||||
const currentMenuQuery = `
|
||||
SELECT parent_obj_id FROM menu_info
|
||||
WHERE objid = $1
|
||||
`;
|
||||
const currentMenuResult = await pool.query(currentMenuQuery, [menuObjid]);
|
||||
|
||||
// 2. 현재 메뉴의 자식 메뉴들 조회
|
||||
const childrenQuery = `
|
||||
if (currentMenuResult.rows.length === 0) {
|
||||
logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid });
|
||||
return [menuObjid];
|
||||
}
|
||||
|
||||
const parentObjId = Number(currentMenuResult.rows[0].parent_obj_id);
|
||||
|
||||
// 2. 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환
|
||||
if (parentObjId === 0) {
|
||||
logger.debug("최상위 메뉴, 자기 자신만 반환", { menuObjid });
|
||||
return [menuObjid];
|
||||
}
|
||||
|
||||
// 3. 형제 메뉴들 조회 (같은 부모를 가진 메뉴들)
|
||||
const siblingsQuery = `
|
||||
SELECT objid FROM menu_info
|
||||
WHERE parent_obj_id = $1
|
||||
ORDER BY objid
|
||||
`;
|
||||
const childrenResult = await pool.query(childrenQuery, [menuObjid]);
|
||||
const siblingsResult = await pool.query(siblingsQuery, [parentObjId]);
|
||||
|
||||
const childObjids = childrenResult.rows.map((row) => Number(row.objid));
|
||||
const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid));
|
||||
|
||||
// 3. 자신 + 자식을 합쳐서 정렬
|
||||
const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b);
|
||||
// 4. 각 형제 메뉴(자기 자신 포함)의 자식 메뉴들도 조회
|
||||
const allObjids = [...siblingObjids];
|
||||
|
||||
for (const siblingObjid of siblingObjids) {
|
||||
const childrenQuery = `
|
||||
SELECT objid FROM menu_info
|
||||
WHERE parent_obj_id = $1
|
||||
ORDER BY objid
|
||||
`;
|
||||
const childrenResult = await pool.query(childrenQuery, [siblingObjid]);
|
||||
const childObjids = childrenResult.rows.map((row) => Number(row.objid));
|
||||
allObjids.push(...childObjids);
|
||||
}
|
||||
|
||||
// 5. 중복 제거 및 정렬
|
||||
const uniqueObjids = Array.from(new Set(allObjids)).sort((a, b) => a - b);
|
||||
|
||||
logger.debug("메뉴 스코프 조회 완료", {
|
||||
menuObjid,
|
||||
childCount: childObjids.length,
|
||||
totalCount: allObjids.length
|
||||
menuObjid,
|
||||
parentObjId,
|
||||
siblingCount: siblingObjids.length,
|
||||
totalCount: uniqueObjids.length
|
||||
});
|
||||
|
||||
return allObjids;
|
||||
return uniqueObjids;
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴 스코프 조회 실패", {
|
||||
menuObjid,
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ class TableCategoryValueService {
|
|||
} else {
|
||||
// 일반 회사: 자신의 카테고리 값만 조회
|
||||
if (menuObjid && siblingObjids.length > 0) {
|
||||
// 메뉴 스코프 적용
|
||||
// 메뉴 스코프 적용 + created_menu_objid 필터링
|
||||
// 현재 메뉴 스코프(형제 메뉴)에서 생성된 값만 표시
|
||||
query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
|
|
@ -197,6 +198,7 @@ class TableCategoryValueService {
|
|||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_menu_objid AS "createdMenuObjid",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
|
|
@ -206,6 +208,10 @@ class TableCategoryValueService {
|
|||
AND column_name = $2
|
||||
AND menu_objid = ANY($3)
|
||||
AND company_code = $4
|
||||
AND (
|
||||
created_menu_objid = ANY($3) -- 형제 메뉴에서 생성된 값만
|
||||
OR created_menu_objid IS NULL -- 레거시 데이터 (모든 메뉴에서 보임)
|
||||
)
|
||||
`;
|
||||
params = [tableName, columnName, siblingObjids, companyCode];
|
||||
} else {
|
||||
|
|
@ -331,8 +337,8 @@ class TableCategoryValueService {
|
|||
INSERT INTO table_column_category_values (
|
||||
table_name, column_name, value_code, value_label, value_order,
|
||||
parent_value_id, depth, description, color, icon,
|
||||
is_active, is_default, company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
is_active, is_default, company_code, menu_objid, created_menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
RETURNING
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
|
|
@ -349,6 +355,7 @@ class TableCategoryValueService {
|
|||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
menu_objid AS "menuObjid",
|
||||
created_menu_objid AS "createdMenuObjid",
|
||||
created_at AS "createdAt",
|
||||
created_by AS "createdBy"
|
||||
`;
|
||||
|
|
@ -368,6 +375,7 @@ class TableCategoryValueService {
|
|||
value.isDefault || false,
|
||||
companyCode,
|
||||
menuObjid, // ← 메뉴 OBJID 저장
|
||||
menuObjid, // ← 🆕 생성 메뉴 OBJID 저장 (같은 값)
|
||||
userId,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1069,12 +1069,28 @@ export class TableManagementService {
|
|||
paramCount: number;
|
||||
} | null> {
|
||||
try {
|
||||
// 🔧 {value, operator} 형태의 필터 객체 처리
|
||||
let actualValue = value;
|
||||
let operator = "contains"; // 기본값
|
||||
|
||||
if (typeof value === "object" && value !== null && "value" in value) {
|
||||
actualValue = value.value;
|
||||
operator = value.operator || "contains";
|
||||
|
||||
logger.info("🔍 필터 객체 처리:", {
|
||||
columnName,
|
||||
originalValue: value,
|
||||
actualValue,
|
||||
operator,
|
||||
});
|
||||
}
|
||||
|
||||
// "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
|
||||
if (
|
||||
value === "__ALL__" ||
|
||||
value === "" ||
|
||||
value === null ||
|
||||
value === undefined
|
||||
actualValue === "__ALL__" ||
|
||||
actualValue === "" ||
|
||||
actualValue === null ||
|
||||
actualValue === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1083,12 +1099,22 @@ export class TableManagementService {
|
|||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
||||
|
||||
if (!columnInfo) {
|
||||
// 컬럼 정보가 없으면 기본 문자열 검색
|
||||
return {
|
||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||
values: [`%${value}%`],
|
||||
paramCount: 1,
|
||||
};
|
||||
// 컬럼 정보가 없으면 operator에 따른 기본 검색
|
||||
switch (operator) {
|
||||
case "equals":
|
||||
return {
|
||||
whereClause: `${columnName}::text = $${paramIndex}`,
|
||||
values: [actualValue],
|
||||
paramCount: 1,
|
||||
};
|
||||
case "contains":
|
||||
default:
|
||||
return {
|
||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||
values: [`%${actualValue}%`],
|
||||
paramCount: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const webType = columnInfo.webType;
|
||||
|
|
@ -1097,17 +1123,17 @@ export class TableManagementService {
|
|||
switch (webType) {
|
||||
case "date":
|
||||
case "datetime":
|
||||
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
||||
return this.buildDateRangeCondition(columnName, actualValue, paramIndex);
|
||||
|
||||
case "number":
|
||||
case "decimal":
|
||||
return this.buildNumberRangeCondition(columnName, value, paramIndex);
|
||||
return this.buildNumberRangeCondition(columnName, actualValue, paramIndex);
|
||||
|
||||
case "code":
|
||||
return await this.buildCodeSearchCondition(
|
||||
tableName,
|
||||
columnName,
|
||||
value,
|
||||
actualValue,
|
||||
paramIndex
|
||||
);
|
||||
|
||||
|
|
@ -1115,15 +1141,15 @@ export class TableManagementService {
|
|||
return await this.buildEntitySearchCondition(
|
||||
tableName,
|
||||
columnName,
|
||||
value,
|
||||
actualValue,
|
||||
paramIndex
|
||||
);
|
||||
|
||||
default:
|
||||
// 기본 문자열 검색
|
||||
// 기본 문자열 검색 (actualValue 사용)
|
||||
return {
|
||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||
values: [`%${value}%`],
|
||||
values: [`%${actualValue}%`],
|
||||
paramCount: 1,
|
||||
};
|
||||
}
|
||||
|
|
@ -1133,9 +1159,14 @@ export class TableManagementService {
|
|||
error
|
||||
);
|
||||
// 오류 시 기본 검색으로 폴백
|
||||
let fallbackValue = value;
|
||||
if (typeof value === "object" && value !== null && "value" in value) {
|
||||
fallbackValue = value.value;
|
||||
}
|
||||
|
||||
return {
|
||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||
values: [`%${value}%`],
|
||||
values: [`%${fallbackValue}%`],
|
||||
paramCount: 1,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,15 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
|
|||
|
||||
const handleApply = () => {
|
||||
table?.onColumnVisibilityChange(localColumns);
|
||||
|
||||
// 컬럼 순서 변경 콜백 호출
|
||||
if (table?.onColumnOrderChange) {
|
||||
const newOrder = localColumns
|
||||
.map((col) => col.columnName)
|
||||
.filter((name) => name !== "__checkbox__");
|
||||
table.onColumnOrderChange(newOrder);
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
|||
const effectiveMenuObjid = menuObjid || props.menuObjid;
|
||||
|
||||
const [selectedColumn, setSelectedColumn] = useState<{
|
||||
uniqueKey: string; // 테이블명.컬럼명 형식
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
tableName: string;
|
||||
|
|
@ -98,10 +99,12 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
|||
<div style={{ width: `${leftWidth}%` }} className="pr-3">
|
||||
<CategoryColumnList
|
||||
tableName={tableName}
|
||||
selectedColumn={selectedColumn?.columnName || null}
|
||||
onColumnSelect={(columnName, columnLabel, tableName) =>
|
||||
setSelectedColumn({ columnName, columnLabel, tableName })
|
||||
}
|
||||
selectedColumn={selectedColumn?.uniqueKey || null}
|
||||
onColumnSelect={(uniqueKey, columnLabel, tableName) => {
|
||||
// uniqueKey는 "테이블명.컬럼명" 형식
|
||||
const columnName = uniqueKey.split('.')[1];
|
||||
setSelectedColumn({ uniqueKey, columnName, columnLabel, tableName });
|
||||
}}
|
||||
menuObjid={effectiveMenuObjid}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -118,6 +121,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
|||
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
||||
{selectedColumn ? (
|
||||
<CategoryValueManager
|
||||
key={selectedColumn.uniqueKey} // 테이블명.컬럼명으로 컴포넌트 재생성
|
||||
tableName={selectedColumn.tableName}
|
||||
columnName={selectedColumn.columnName}
|
||||
columnLabel={selectedColumn.columnLabel}
|
||||
|
|
|
|||
|
|
@ -147,17 +147,18 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
|||
<div className="space-y-2">
|
||||
{columns.map((column) => {
|
||||
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
||||
const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
|
||||
return (
|
||||
<div
|
||||
key={uniqueKey}
|
||||
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName, column.tableName)}
|
||||
onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)}
|
||||
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
|
||||
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||
isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderTree
|
||||
className={`h-4 w-4 ${selectedColumn === column.columnName ? "text-primary" : "text-muted-foreground"}`}
|
||||
className={`h-4 w-4 ${isSelected ? "text-primary" : "text-muted-foreground"}`}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { ComponentRendererProps } from "../../types";
|
||||
import { SplitPanelLayoutConfig } from "./types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
|
@ -8,12 +8,14 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import { TableFilter, ColumnVisibility } from "@/types/table-options";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||
// 추가 props
|
||||
|
|
@ -44,6 +46,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const [leftFilters, setLeftFilters] = useState<TableFilter[]>([]);
|
||||
const [leftGrouping, setLeftGrouping] = useState<string[]>([]);
|
||||
const [leftColumnVisibility, setLeftColumnVisibility] = useState<ColumnVisibility[]>([]);
|
||||
const [leftColumnOrder, setLeftColumnOrder] = useState<string[]>([]); // 🔧 컬럼 순서
|
||||
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
|
||||
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
|
||||
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
|
||||
|
|
@ -160,6 +163,84 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
return rootItems;
|
||||
}, [componentConfig.leftPanel?.itemAddConfig]);
|
||||
|
||||
// 🔧 사용자 ID 가져오기
|
||||
const { userId: currentUserId } = useAuth();
|
||||
|
||||
// 🔄 필터를 searchValues 형식으로 변환
|
||||
const searchValues = useMemo(() => {
|
||||
if (!leftFilters || leftFilters.length === 0) return {};
|
||||
|
||||
const values: Record<string, any> = {};
|
||||
leftFilters.forEach(filter => {
|
||||
if (filter.value !== undefined && filter.value !== null && filter.value !== '') {
|
||||
values[filter.columnName] = {
|
||||
value: filter.value,
|
||||
operator: filter.operator || 'contains',
|
||||
};
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}, [leftFilters]);
|
||||
|
||||
// 🔄 컬럼 가시성 및 순서 처리
|
||||
const visibleLeftColumns = useMemo(() => {
|
||||
const displayColumns = componentConfig.leftPanel?.columns || [];
|
||||
|
||||
if (displayColumns.length === 0) return [];
|
||||
|
||||
let columns = displayColumns;
|
||||
|
||||
// columnVisibility가 있으면 가시성 적용
|
||||
if (leftColumnVisibility.length > 0) {
|
||||
const visibilityMap = new Map(leftColumnVisibility.map(cv => [cv.columnName, cv.visible]));
|
||||
columns = columns.filter((col: any) => {
|
||||
const colName = typeof col === 'string' ? col : (col.name || col.columnName);
|
||||
return visibilityMap.get(colName) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
// 🔧 컬럼 순서 적용
|
||||
if (leftColumnOrder.length > 0) {
|
||||
const orderMap = new Map(leftColumnOrder.map((name, index) => [name, index]));
|
||||
columns = [...columns].sort((a, b) => {
|
||||
const aName = typeof a === 'string' ? a : (a.name || a.columnName);
|
||||
const bName = typeof b === 'string' ? b : (b.name || b.columnName);
|
||||
const aIndex = orderMap.get(aName) ?? 999;
|
||||
const bIndex = orderMap.get(bName) ?? 999;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, [componentConfig.leftPanel?.columns, leftColumnVisibility, leftColumnOrder]);
|
||||
|
||||
// 🔄 데이터 그룹화
|
||||
const groupedLeftData = useMemo(() => {
|
||||
if (!leftGrouping || leftGrouping.length === 0 || leftData.length === 0) return [];
|
||||
|
||||
const grouped = new Map<string, any[]>();
|
||||
|
||||
leftData.forEach((item) => {
|
||||
// 각 그룹 컬럼의 값을 조합하여 그룹 키 생성
|
||||
const groupKey = leftGrouping.map(col => {
|
||||
const value = item[col];
|
||||
// null/undefined 처리
|
||||
return value === null || value === undefined ? "(비어있음)" : String(value);
|
||||
}).join(" > ");
|
||||
|
||||
if (!grouped.has(groupKey)) {
|
||||
grouped.set(groupKey, []);
|
||||
}
|
||||
grouped.get(groupKey)!.push(item);
|
||||
});
|
||||
|
||||
return Array.from(grouped.entries()).map(([key, items]) => ({
|
||||
groupKey: key,
|
||||
items,
|
||||
count: items.length,
|
||||
}));
|
||||
}, [leftData, leftGrouping]);
|
||||
|
||||
// 좌측 데이터 로드
|
||||
const loadLeftData = useCallback(async () => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
|
|
@ -167,12 +248,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
|
||||
setIsLoadingLeft(true);
|
||||
try {
|
||||
const result = await dataApi.getTableData(leftTableName, {
|
||||
// 🎯 필터 조건을 API에 전달 (entityJoinApi 사용)
|
||||
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
|
||||
|
||||
|
||||
const result = await entityJoinApi.getTableDataWithJoins(leftTableName, {
|
||||
page: 1,
|
||||
size: 100,
|
||||
// searchTerm 제거 - 클라이언트 사이드에서 필터링
|
||||
search: filters, // 필터 조건 전달
|
||||
enableEntityJoin: true, // 엔티티 조인 활성화
|
||||
});
|
||||
|
||||
|
||||
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||
if (leftColumn && result.data.length > 0) {
|
||||
|
|
@ -196,7 +283,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
} finally {
|
||||
setIsLoadingLeft(false);
|
||||
}
|
||||
}, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy]);
|
||||
}, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy, searchValues]);
|
||||
|
||||
// 우측 데이터 로드
|
||||
const loadRightData = useCallback(
|
||||
|
|
@ -283,67 +370,101 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
[rightTableColumns],
|
||||
);
|
||||
|
||||
// 🔧 컬럼의 고유값 가져오기 함수
|
||||
const getLeftColumnUniqueValues = useCallback(async (columnName: string) => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (!leftTableName || leftData.length === 0) return [];
|
||||
|
||||
// 현재 로드된 데이터에서 고유값 추출
|
||||
const uniqueValues = new Set<string>();
|
||||
|
||||
leftData.forEach((item) => {
|
||||
const value = item[columnName];
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
// _name 필드 우선 사용 (category/entity type)
|
||||
const displayValue = item[`${columnName}_name`] || value;
|
||||
uniqueValues.add(String(displayValue));
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueValues).map(value => ({
|
||||
value: value,
|
||||
label: value,
|
||||
}));
|
||||
}, [componentConfig.leftPanel?.tableName, leftData]);
|
||||
|
||||
// 좌측 테이블 등록 (Context에 등록)
|
||||
useEffect(() => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (!leftTableName || isDesignMode) return;
|
||||
|
||||
const leftTableId = `split-panel-left-${component.id}`;
|
||||
const leftColumns = componentConfig.leftPanel?.displayColumns || [];
|
||||
// 🔧 화면에 표시되는 컬럼 사용 (columns 속성)
|
||||
const configuredColumns = componentConfig.leftPanel?.columns || [];
|
||||
const displayColumns = configuredColumns.map((col: any) => {
|
||||
if (typeof col === 'string') return col;
|
||||
return col.columnName || col.name || col;
|
||||
}).filter(Boolean);
|
||||
|
||||
// 화면에 설정된 컬럼이 없으면 등록하지 않음
|
||||
if (displayColumns.length === 0) return;
|
||||
|
||||
if (leftColumns.length > 0) {
|
||||
registerTable({
|
||||
tableId: leftTableId,
|
||||
label: `${component.title || "분할 패널"} (좌측)`,
|
||||
tableName: leftTableName,
|
||||
columns: leftColumns.map((col: string) => ({
|
||||
columnName: col,
|
||||
columnLabel: leftColumnLabels[col] || col,
|
||||
inputType: "text",
|
||||
visible: true,
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
})),
|
||||
onFilterChange: setLeftFilters,
|
||||
onGroupChange: setLeftGrouping,
|
||||
onColumnVisibilityChange: setLeftColumnVisibility,
|
||||
});
|
||||
// 테이블명이 있으면 등록
|
||||
registerTable({
|
||||
tableId: leftTableId,
|
||||
label: `${component.title || "분할 패널"} (좌측)`,
|
||||
tableName: leftTableName,
|
||||
columns: displayColumns.map((col: string) => ({
|
||||
columnName: col,
|
||||
columnLabel: leftColumnLabels[col] || col,
|
||||
inputType: "text",
|
||||
visible: true,
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
})),
|
||||
onFilterChange: setLeftFilters,
|
||||
onGroupChange: setLeftGrouping,
|
||||
onColumnVisibilityChange: setLeftColumnVisibility,
|
||||
onColumnOrderChange: setLeftColumnOrder, // 🔧 컬럼 순서 변경 콜백 추가
|
||||
getColumnUniqueValues: getLeftColumnUniqueValues, // 🔧 고유값 가져오기 함수 추가
|
||||
});
|
||||
|
||||
return () => unregisterTable(leftTableId);
|
||||
}
|
||||
}, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.displayColumns, leftColumnLabels, component.title, isDesignMode]);
|
||||
return () => unregisterTable(leftTableId);
|
||||
}, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode, getLeftColumnUniqueValues]);
|
||||
|
||||
// 우측 테이블 등록 (Context에 등록)
|
||||
useEffect(() => {
|
||||
const rightTableName = componentConfig.rightPanel?.tableName;
|
||||
if (!rightTableName || isDesignMode) return;
|
||||
|
||||
const rightTableId = `split-panel-right-${component.id}`;
|
||||
const rightColumns = rightTableColumns.map((col: any) => col.columnName || col.column_name).filter(Boolean);
|
||||
|
||||
if (rightColumns.length > 0) {
|
||||
registerTable({
|
||||
tableId: rightTableId,
|
||||
label: `${component.title || "분할 패널"} (우측)`,
|
||||
tableName: rightTableName,
|
||||
columns: rightColumns.map((col: string) => ({
|
||||
columnName: col,
|
||||
columnLabel: rightColumnLabels[col] || col,
|
||||
inputType: "text",
|
||||
visible: true,
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
})),
|
||||
onFilterChange: setRightFilters,
|
||||
onGroupChange: setRightGrouping,
|
||||
onColumnVisibilityChange: setRightColumnVisibility,
|
||||
});
|
||||
|
||||
return () => unregisterTable(rightTableId);
|
||||
}
|
||||
}, [component.id, componentConfig.rightPanel?.tableName, rightTableColumns, rightColumnLabels, component.title, isDesignMode]);
|
||||
// 우측 테이블은 검색 컴포넌트 등록 제외 (좌측 마스터 테이블만 검색 가능)
|
||||
// useEffect(() => {
|
||||
// const rightTableName = componentConfig.rightPanel?.tableName;
|
||||
// if (!rightTableName || isDesignMode) return;
|
||||
//
|
||||
// const rightTableId = `split-panel-right-${component.id}`;
|
||||
// // 🔧 화면에 표시되는 컬럼만 등록 (displayColumns 또는 columns)
|
||||
// const displayColumns = componentConfig.rightPanel?.columns || [];
|
||||
// const rightColumns = displayColumns.map((col: any) => col.columnName || col.name || col).filter(Boolean);
|
||||
//
|
||||
// if (rightColumns.length > 0) {
|
||||
// registerTable({
|
||||
// tableId: rightTableId,
|
||||
// label: `${component.title || "분할 패널"} (우측)`,
|
||||
// tableName: rightTableName,
|
||||
// columns: rightColumns.map((col: string) => ({
|
||||
// columnName: col,
|
||||
// columnLabel: rightColumnLabels[col] || col,
|
||||
// inputType: "text",
|
||||
// visible: true,
|
||||
// width: 150,
|
||||
// sortable: true,
|
||||
// filterable: true,
|
||||
// })),
|
||||
// onFilterChange: setRightFilters,
|
||||
// onGroupChange: setRightGrouping,
|
||||
// onColumnVisibilityChange: setRightColumnVisibility,
|
||||
// });
|
||||
//
|
||||
// return () => unregisterTable(rightTableId);
|
||||
// }
|
||||
// }, [component.id, componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, rightColumnLabels, component.title, isDesignMode]);
|
||||
|
||||
// 좌측 테이블 컬럼 라벨 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -786,6 +907,43 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}
|
||||
}, [addModalPanel, componentConfig, addModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]);
|
||||
|
||||
// 🔧 좌측 컬럼 가시성 설정 저장 및 불러오기
|
||||
useEffect(() => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (leftTableName && currentUserId) {
|
||||
// localStorage에서 저장된 설정 불러오기
|
||||
const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`;
|
||||
const savedSettings = localStorage.getItem(storageKey);
|
||||
|
||||
if (savedSettings) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedSettings) as ColumnVisibility[];
|
||||
setLeftColumnVisibility(parsed);
|
||||
} catch (error) {
|
||||
console.error("저장된 컬럼 설정 불러오기 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [componentConfig.leftPanel?.tableName, currentUserId]);
|
||||
|
||||
// 🔧 컬럼 가시성 변경 시 localStorage에 저장 및 순서 업데이트
|
||||
useEffect(() => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
|
||||
if (leftColumnVisibility.length > 0 && leftTableName && currentUserId) {
|
||||
// 순서 업데이트
|
||||
const newOrder = leftColumnVisibility
|
||||
.map((cv) => cv.columnName)
|
||||
.filter((name) => name !== "__checkbox__"); // 체크박스 제외
|
||||
|
||||
setLeftColumnOrder(newOrder);
|
||||
|
||||
// localStorage에 저장
|
||||
const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`;
|
||||
localStorage.setItem(storageKey, JSON.stringify(leftColumnVisibility));
|
||||
}
|
||||
}, [leftColumnVisibility, componentConfig.leftPanel?.tableName, currentUserId]);
|
||||
|
||||
// 초기 데이터 로드
|
||||
useEffect(() => {
|
||||
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
||||
|
|
@ -794,6 +952,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDesignMode, componentConfig.autoLoad]);
|
||||
|
||||
// 🔄 필터 변경 시 데이터 다시 로드
|
||||
useEffect(() => {
|
||||
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
||||
loadLeftData();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [leftFilters]);
|
||||
|
||||
// 리사이저 드래그 핸들러
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!resizable) return;
|
||||
|
|
@ -933,6 +1099,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
// 🔧 로컬 검색 필터 적용
|
||||
const filteredData = leftSearchQuery
|
||||
? leftData.filter((item) => {
|
||||
const searchLower = leftSearchQuery.toLowerCase();
|
||||
|
|
@ -943,12 +1110,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
})
|
||||
: leftData;
|
||||
|
||||
const displayColumns = componentConfig.leftPanel?.columns || [];
|
||||
const columnsToShow = displayColumns.length > 0
|
||||
? displayColumns.map(col => ({
|
||||
...col,
|
||||
label: leftColumnLabels[col.name] || col.label || col.name
|
||||
}))
|
||||
// 🔧 가시성 처리된 컬럼 사용
|
||||
const columnsToShow = visibleLeftColumns.length > 0
|
||||
? visibleLeftColumns.map((col: any) => {
|
||||
const colName = typeof col === 'string' ? col : (col.name || col.columnName);
|
||||
return {
|
||||
name: colName,
|
||||
label: leftColumnLabels[colName] || (typeof col === 'object' ? col.label : null) || colName,
|
||||
width: typeof col === 'object' ? col.width : 150,
|
||||
align: (typeof col === 'object' ? col.align : "left") as "left" | "center" | "right"
|
||||
};
|
||||
})
|
||||
: Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({
|
||||
name: key,
|
||||
label: leftColumnLabels[key] || key,
|
||||
|
|
@ -956,6 +1128,66 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
align: "left" as const
|
||||
}));
|
||||
|
||||
// 🔧 그룹화된 데이터 렌더링
|
||||
if (groupedLeftData.length > 0) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
{groupedLeftData.map((group, groupIdx) => (
|
||||
<div key={groupIdx} className="mb-4">
|
||||
<div className="bg-gray-100 px-3 py-2 font-semibold text-sm">
|
||||
{group.groupKey} ({group.count}개)
|
||||
</div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
style={{ width: col.width ? `${col.width}px` : 'auto', textAlign: col.align || "left" }}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{group.items.map((item, idx) => {
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const itemId = item[sourceColumn] || item.id || item.ID || idx;
|
||||
const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={itemId}
|
||||
onClick={() => handleLeftItemSelect(item)}
|
||||
className={`hover:bg-accent cursor-pointer transition-colors ${
|
||||
isSelected ? "bg-primary/10" : ""
|
||||
}`}
|
||||
>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
key={colIdx}
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-gray-900"
|
||||
style={{ textAlign: col.align || "left" }}
|
||||
>
|
||||
{item[col.name] !== null && item[col.name] !== undefined
|
||||
? String(item[col.name])
|
||||
: "-"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 🔧 일반 테이블 렌더링 (그룹화 없음)
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
|
|
|
|||
|
|
@ -274,6 +274,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
setCurrentPage(1); // 필터 변경 시 첫 페이지로
|
||||
}, [filters]);
|
||||
|
||||
// grouping이 변경되면 groupByColumns 업데이트
|
||||
useEffect(() => {
|
||||
setGroupByColumns(grouping);
|
||||
}, [grouping]);
|
||||
|
||||
// 초기 로드 시 localStorage에서 저장된 설정 불러오기
|
||||
useEffect(() => {
|
||||
if (tableConfig.selectedTable && currentUserId) {
|
||||
|
|
@ -1652,9 +1657,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
data.forEach((item) => {
|
||||
// 그룹 키 생성: "통화:KRW > 단위:EA"
|
||||
const keyParts = groupByColumns.map((col) => {
|
||||
const value = item[col];
|
||||
// 카테고리/엔티티 타입인 경우 _name 필드 사용
|
||||
const inputType = columnMeta?.[col]?.inputType;
|
||||
let displayValue = item[col];
|
||||
|
||||
if (inputType === 'category' || inputType === 'entity' || inputType === 'code') {
|
||||
// _name 필드가 있으면 사용 (예: division_name, writer_name)
|
||||
const nameField = `${col}_name`;
|
||||
if (item[nameField] !== undefined && item[nameField] !== null) {
|
||||
displayValue = item[nameField];
|
||||
}
|
||||
}
|
||||
|
||||
const label = columnLabels[col] || col;
|
||||
return `${label}:${value !== null && value !== undefined ? value : "-"}`;
|
||||
return `${label}:${displayValue !== null && displayValue !== undefined ? displayValue : "-"}`;
|
||||
});
|
||||
const groupKey = keyParts.join(" > ");
|
||||
|
||||
|
|
@ -1677,7 +1693,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
count: items.length,
|
||||
};
|
||||
});
|
||||
}, [data, groupByColumns, columnLabels]);
|
||||
}, [data, groupByColumns, columnLabels, columnMeta]);
|
||||
|
||||
// 저장된 그룹 설정 불러오기
|
||||
useEffect(() => {
|
||||
|
|
@ -1860,124 +1876,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (tableConfig.stickyHeader && !isDesignMode) {
|
||||
return (
|
||||
<div {...domProps}>
|
||||
{tableConfig.filter?.enabled && (
|
||||
<div className="border-border border-b px-4 py-2 sm:px-6 sm:py-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
||||
<div className="flex-1">
|
||||
<AdvancedSearchFilters
|
||||
filters={activeFilters}
|
||||
searchValues={searchValues}
|
||||
onSearchValueChange={handleSearchValueChange}
|
||||
onSearch={handleAdvancedSearch}
|
||||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 전체 개수 */}
|
||||
<div className="hidden sm:block text-sm text-muted-foreground whitespace-nowrap">
|
||||
전체 <span className="font-semibold text-foreground">{totalItems.toLocaleString()}</span>개
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsTableOptionsOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<TableIcon className="mr-2 h-4 w-4" />
|
||||
테이블 옵션
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsFilterSettingOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
필터 설정
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||
>
|
||||
<Layers className="mr-2 h-4 w-4" />
|
||||
그룹 설정
|
||||
{groupByColumns.length > 0 && (
|
||||
<span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground">
|
||||
{groupByColumns.length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="end">
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">그룹 설정</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
데이터를 그룹화할 컬럼을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 목록 */}
|
||||
<div className="max-h-[300px] space-y-2 overflow-y-auto">
|
||||
{visibleColumns
|
||||
.filter((col) => col.columnName !== "__checkbox__")
|
||||
.map((col) => (
|
||||
<div
|
||||
key={col.columnName}
|
||||
className="flex items-center gap-3 rounded p-2 hover:bg-muted/50"
|
||||
>
|
||||
<Checkbox
|
||||
id={`group-dropdown-${col.columnName}`}
|
||||
checked={groupByColumns.includes(col.columnName)}
|
||||
onCheckedChange={() => toggleGroupColumn(col.columnName)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`group-dropdown-${col.columnName}`}
|
||||
className="flex-1 cursor-pointer text-xs font-normal"
|
||||
>
|
||||
{columnLabels[col.columnName] || col.displayName || col.columnName}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 선택된 그룹 안내 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
<div className="rounded bg-muted/30 p-2 text-xs text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">
|
||||
{groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 초기화 버튼 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setGroupByColumns([]);
|
||||
if (groupSettingKey) {
|
||||
localStorage.removeItem(groupSettingKey);
|
||||
}
|
||||
toast.success("그룹 설정이 초기화되었습니다");
|
||||
}}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 필터 헤더는 TableSearchWidget으로 이동 */}
|
||||
|
||||
{/* 그룹 표시 배지 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
|
|
@ -2040,125 +1939,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div {...domProps}>
|
||||
{/* 필터 */}
|
||||
{tableConfig.filter?.enabled && (
|
||||
<div className="border-border flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||
<div className="flex-1">
|
||||
<AdvancedSearchFilters
|
||||
filters={activeFilters}
|
||||
searchValues={searchValues}
|
||||
onSearchValueChange={handleSearchValueChange}
|
||||
onSearch={handleAdvancedSearch}
|
||||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 전체 개수 */}
|
||||
<div className="hidden sm:block text-sm text-muted-foreground whitespace-nowrap">
|
||||
전체 <span className="font-semibold text-foreground">{totalItems.toLocaleString()}</span>개
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsTableOptionsOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<TableIcon className="mr-2 h-4 w-4" />
|
||||
테이블 옵션
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsFilterSettingOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
필터 설정
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||
>
|
||||
<Layers className="mr-2 h-4 w-4" />
|
||||
그룹 설정
|
||||
{groupByColumns.length > 0 && (
|
||||
<span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground">
|
||||
{groupByColumns.length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="end">
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">그룹 설정</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
데이터를 그룹화할 컬럼을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 목록 */}
|
||||
<div className="max-h-[300px] space-y-2 overflow-y-auto">
|
||||
{visibleColumns
|
||||
.filter((col) => col.columnName !== "__checkbox__")
|
||||
.map((col) => (
|
||||
<div
|
||||
key={col.columnName}
|
||||
className="flex items-center gap-3 rounded p-2 hover:bg-muted/50"
|
||||
>
|
||||
<Checkbox
|
||||
id={`group-dropdown-2-${col.columnName}`}
|
||||
checked={groupByColumns.includes(col.columnName)}
|
||||
onCheckedChange={() => toggleGroupColumn(col.columnName)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`group-dropdown-2-${col.columnName}`}
|
||||
className="flex-1 cursor-pointer text-xs font-normal"
|
||||
>
|
||||
{columnLabels[col.columnName] || col.displayName || col.columnName}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 선택된 그룹 안내 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
<div className="rounded bg-muted/30 p-2 text-xs text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">
|
||||
{groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 초기화 버튼 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setGroupByColumns([]);
|
||||
if (groupSettingKey) {
|
||||
localStorage.removeItem(groupSettingKey);
|
||||
}
|
||||
toast.success("그룹 설정이 초기화되었습니다");
|
||||
}}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 필터 헤더는 TableSearchWidget으로 이동 */}
|
||||
|
||||
{/* 그룹 표시 배지 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -76,7 +76,15 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
useEffect(() => {
|
||||
const tables = Array.from(registeredTables.values());
|
||||
|
||||
console.log("🔍 [TableSearchWidget] 테이블 감지:", {
|
||||
tablesCount: tables.length,
|
||||
tableIds: tables.map(t => t.tableId),
|
||||
selectedTableId,
|
||||
autoSelectFirstTable,
|
||||
});
|
||||
|
||||
if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) {
|
||||
console.log("✅ [TableSearchWidget] 첫 번째 테이블 자동 선택:", tables[0].tableId);
|
||||
setSelectedTableId(tables[0].tableId);
|
||||
}
|
||||
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue