Compare commits
7 Commits
e06f21f63f
...
214bd829e9
| Author | SHA1 | Date |
|---|---|---|
|
|
214bd829e9 | |
|
|
77faba7e77 | |
|
|
7b84a81a96 | |
|
|
579c4b7387 | |
|
|
2dcf2c4c8e | |
|
|
9cf9b87068 | |
|
|
c40d8ea1ba |
|
|
@ -104,7 +104,7 @@ export class DDLExecutionService {
|
||||||
await this.saveTableMetadata(client, tableName, description);
|
await this.saveTableMetadata(client, tableName, description);
|
||||||
|
|
||||||
// 5-3. 컬럼 메타데이터 저장
|
// 5-3. 컬럼 메타데이터 저장
|
||||||
await this.saveColumnMetadata(client, tableName, columns);
|
await this.saveColumnMetadata(client, tableName, columns, userCompanyCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. 성공 로그 기록
|
// 6. 성공 로그 기록
|
||||||
|
|
@ -272,7 +272,7 @@ export class DDLExecutionService {
|
||||||
await client.query(ddlQuery);
|
await client.query(ddlQuery);
|
||||||
|
|
||||||
// 6-2. 컬럼 메타데이터 저장
|
// 6-2. 컬럼 메타데이터 저장
|
||||||
await this.saveColumnMetadata(client, tableName, [column]);
|
await this.saveColumnMetadata(client, tableName, [column], userCompanyCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. 성공 로그 기록
|
// 7. 성공 로그 기록
|
||||||
|
|
@ -446,7 +446,8 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
private async saveColumnMetadata(
|
private async saveColumnMetadata(
|
||||||
client: any,
|
client: any,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columns: CreateColumnDefinition[]
|
columns: CreateColumnDefinition[],
|
||||||
|
companyCode: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성
|
// 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|
@ -508,19 +509,19 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO table_type_columns (
|
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
|
is_nullable, display_order, created_date, updated_date
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, '{}',
|
$1, $2, $3, $4, '{}',
|
||||||
'Y', $4, now(), now()
|
'Y', $5, now(), now()
|
||||||
)
|
)
|
||||||
ON CONFLICT (table_name, column_name)
|
ON CONFLICT (table_name, column_name, company_code)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
input_type = $3,
|
input_type = $4,
|
||||||
display_order = $4,
|
display_order = $5,
|
||||||
updated_date = now()
|
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(
|
await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO table_type_columns (
|
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
|
is_nullable, display_order, created_date, updated_date
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4,
|
$1, $2, $3, $4, $5,
|
||||||
'Y', $5, now(), now()
|
'Y', $6, now(), now()
|
||||||
)
|
)
|
||||||
ON CONFLICT (table_name, column_name)
|
ON CONFLICT (table_name, column_name, company_code)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
input_type = $3,
|
input_type = $4,
|
||||||
detail_settings = $4,
|
detail_settings = $5,
|
||||||
display_order = $5,
|
display_order = $6,
|
||||||
updated_date = now()
|
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 {
|
try {
|
||||||
logger.debug("메뉴 스코프 조회 시작", { menuObjid });
|
logger.debug("메뉴 스코프 조회 시작", { menuObjid });
|
||||||
|
|
||||||
// 1. 현재 메뉴 자신을 포함
|
// 1. 현재 메뉴 정보 조회 (부모 ID 확인)
|
||||||
const menuObjids = [menuObjid];
|
const currentMenuQuery = `
|
||||||
|
SELECT parent_obj_id FROM menu_info
|
||||||
|
WHERE objid = $1
|
||||||
|
`;
|
||||||
|
const currentMenuResult = await pool.query(currentMenuQuery, [menuObjid]);
|
||||||
|
|
||||||
// 2. 현재 메뉴의 자식 메뉴들 조회
|
if (currentMenuResult.rows.length === 0) {
|
||||||
const childrenQuery = `
|
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
|
SELECT objid FROM menu_info
|
||||||
WHERE parent_obj_id = $1
|
WHERE parent_obj_id = $1
|
||||||
ORDER BY objid
|
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. 자신 + 자식을 합쳐서 정렬
|
// 4. 각 형제 메뉴(자기 자신 포함)의 자식 메뉴들도 조회
|
||||||
const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b);
|
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("메뉴 스코프 조회 완료", {
|
logger.debug("메뉴 스코프 조회 완료", {
|
||||||
menuObjid,
|
menuObjid,
|
||||||
childCount: childObjids.length,
|
parentObjId,
|
||||||
totalCount: allObjids.length
|
siblingCount: siblingObjids.length,
|
||||||
|
totalCount: uniqueObjids.length
|
||||||
});
|
});
|
||||||
|
|
||||||
return allObjids;
|
return uniqueObjids;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("메뉴 스코프 조회 실패", {
|
logger.error("메뉴 스코프 조회 실패", {
|
||||||
menuObjid,
|
menuObjid,
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,8 @@ class TableCategoryValueService {
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 조회
|
// 일반 회사: 자신의 카테고리 값만 조회
|
||||||
if (menuObjid && siblingObjids.length > 0) {
|
if (menuObjid && siblingObjids.length > 0) {
|
||||||
// 메뉴 스코프 적용
|
// 메뉴 스코프 적용 + created_menu_objid 필터링
|
||||||
|
// 현재 메뉴 스코프(형제 메뉴)에서 생성된 값만 표시
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
value_id AS "valueId",
|
value_id AS "valueId",
|
||||||
|
|
@ -197,6 +198,7 @@ class TableCategoryValueService {
|
||||||
is_default AS "isDefault",
|
is_default AS "isDefault",
|
||||||
company_code AS "companyCode",
|
company_code AS "companyCode",
|
||||||
menu_objid AS "menuObjid",
|
menu_objid AS "menuObjid",
|
||||||
|
created_menu_objid AS "createdMenuObjid",
|
||||||
created_at AS "createdAt",
|
created_at AS "createdAt",
|
||||||
updated_at AS "updatedAt",
|
updated_at AS "updatedAt",
|
||||||
created_by AS "createdBy",
|
created_by AS "createdBy",
|
||||||
|
|
@ -206,6 +208,10 @@ class TableCategoryValueService {
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND menu_objid = ANY($3)
|
AND menu_objid = ANY($3)
|
||||||
AND company_code = $4
|
AND company_code = $4
|
||||||
|
AND (
|
||||||
|
created_menu_objid = ANY($3) -- 형제 메뉴에서 생성된 값만
|
||||||
|
OR created_menu_objid IS NULL -- 레거시 데이터 (모든 메뉴에서 보임)
|
||||||
|
)
|
||||||
`;
|
`;
|
||||||
params = [tableName, columnName, siblingObjids, companyCode];
|
params = [tableName, columnName, siblingObjids, companyCode];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -331,8 +337,8 @@ class TableCategoryValueService {
|
||||||
INSERT INTO table_column_category_values (
|
INSERT INTO table_column_category_values (
|
||||||
table_name, column_name, value_code, value_label, value_order,
|
table_name, column_name, value_code, value_label, value_order,
|
||||||
parent_value_id, depth, description, color, icon,
|
parent_value_id, depth, description, color, icon,
|
||||||
is_active, is_default, company_code, menu_objid, created_by
|
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)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||||
RETURNING
|
RETURNING
|
||||||
value_id AS "valueId",
|
value_id AS "valueId",
|
||||||
table_name AS "tableName",
|
table_name AS "tableName",
|
||||||
|
|
@ -349,6 +355,7 @@ class TableCategoryValueService {
|
||||||
is_default AS "isDefault",
|
is_default AS "isDefault",
|
||||||
company_code AS "companyCode",
|
company_code AS "companyCode",
|
||||||
menu_objid AS "menuObjid",
|
menu_objid AS "menuObjid",
|
||||||
|
created_menu_objid AS "createdMenuObjid",
|
||||||
created_at AS "createdAt",
|
created_at AS "createdAt",
|
||||||
created_by AS "createdBy"
|
created_by AS "createdBy"
|
||||||
`;
|
`;
|
||||||
|
|
@ -368,6 +375,7 @@ class TableCategoryValueService {
|
||||||
value.isDefault || false,
|
value.isDefault || false,
|
||||||
companyCode,
|
companyCode,
|
||||||
menuObjid, // ← 메뉴 OBJID 저장
|
menuObjid, // ← 메뉴 OBJID 저장
|
||||||
|
menuObjid, // ← 🆕 생성 메뉴 OBJID 저장 (같은 값)
|
||||||
userId,
|
userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1069,12 +1069,28 @@ export class TableManagementService {
|
||||||
paramCount: number;
|
paramCount: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
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__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
|
// "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
|
||||||
if (
|
if (
|
||||||
value === "__ALL__" ||
|
actualValue === "__ALL__" ||
|
||||||
value === "" ||
|
actualValue === "" ||
|
||||||
value === null ||
|
actualValue === null ||
|
||||||
value === undefined
|
actualValue === undefined
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1083,12 +1099,22 @@ export class TableManagementService {
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
||||||
|
|
||||||
if (!columnInfo) {
|
if (!columnInfo) {
|
||||||
// 컬럼 정보가 없으면 기본 문자열 검색
|
// 컬럼 정보가 없으면 operator에 따른 기본 검색
|
||||||
return {
|
switch (operator) {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
case "equals":
|
||||||
values: [`%${value}%`],
|
return {
|
||||||
paramCount: 1,
|
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;
|
const webType = columnInfo.webType;
|
||||||
|
|
@ -1097,17 +1123,17 @@ export class TableManagementService {
|
||||||
switch (webType) {
|
switch (webType) {
|
||||||
case "date":
|
case "date":
|
||||||
case "datetime":
|
case "datetime":
|
||||||
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
return this.buildDateRangeCondition(columnName, actualValue, paramIndex);
|
||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
case "decimal":
|
case "decimal":
|
||||||
return this.buildNumberRangeCondition(columnName, value, paramIndex);
|
return this.buildNumberRangeCondition(columnName, actualValue, paramIndex);
|
||||||
|
|
||||||
case "code":
|
case "code":
|
||||||
return await this.buildCodeSearchCondition(
|
return await this.buildCodeSearchCondition(
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
value,
|
actualValue,
|
||||||
paramIndex
|
paramIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1115,15 +1141,15 @@ export class TableManagementService {
|
||||||
return await this.buildEntitySearchCondition(
|
return await this.buildEntitySearchCondition(
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
value,
|
actualValue,
|
||||||
paramIndex
|
paramIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 기본 문자열 검색
|
// 기본 문자열 검색 (actualValue 사용)
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||||
values: [`%${value}%`],
|
values: [`%${actualValue}%`],
|
||||||
paramCount: 1,
|
paramCount: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1133,9 +1159,14 @@ export class TableManagementService {
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
// 오류 시 기본 검색으로 폴백
|
// 오류 시 기본 검색으로 폴백
|
||||||
|
let fallbackValue = value;
|
||||||
|
if (typeof value === "object" && value !== null && "value" in value) {
|
||||||
|
fallbackValue = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||||
values: [`%${value}%`],
|
values: [`%${fallbackValue}%`],
|
||||||
paramCount: 1,
|
paramCount: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,15 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
table?.onColumnVisibilityChange(localColumns);
|
table?.onColumnVisibilityChange(localColumns);
|
||||||
|
|
||||||
|
// 컬럼 순서 변경 콜백 호출
|
||||||
|
if (table?.onColumnOrderChange) {
|
||||||
|
const newOrder = localColumns
|
||||||
|
.map((col) => col.columnName)
|
||||||
|
.filter((name) => name !== "__checkbox__");
|
||||||
|
table.onColumnOrderChange(newOrder);
|
||||||
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
||||||
const effectiveMenuObjid = menuObjid || props.menuObjid;
|
const effectiveMenuObjid = menuObjid || props.menuObjid;
|
||||||
|
|
||||||
const [selectedColumn, setSelectedColumn] = useState<{
|
const [selectedColumn, setSelectedColumn] = useState<{
|
||||||
|
uniqueKey: string; // 테이블명.컬럼명 형식
|
||||||
columnName: string;
|
columnName: string;
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
|
@ -98,10 +99,12 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
||||||
<div style={{ width: `${leftWidth}%` }} className="pr-3">
|
<div style={{ width: `${leftWidth}%` }} className="pr-3">
|
||||||
<CategoryColumnList
|
<CategoryColumnList
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
selectedColumn={selectedColumn?.columnName || null}
|
selectedColumn={selectedColumn?.uniqueKey || null}
|
||||||
onColumnSelect={(columnName, columnLabel, tableName) =>
|
onColumnSelect={(uniqueKey, columnLabel, tableName) => {
|
||||||
setSelectedColumn({ columnName, columnLabel, tableName })
|
// uniqueKey는 "테이블명.컬럼명" 형식
|
||||||
}
|
const columnName = uniqueKey.split('.')[1];
|
||||||
|
setSelectedColumn({ uniqueKey, columnName, columnLabel, tableName });
|
||||||
|
}}
|
||||||
menuObjid={effectiveMenuObjid}
|
menuObjid={effectiveMenuObjid}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,6 +121,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
|
||||||
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
||||||
{selectedColumn ? (
|
{selectedColumn ? (
|
||||||
<CategoryValueManager
|
<CategoryValueManager
|
||||||
|
key={selectedColumn.uniqueKey} // 테이블명.컬럼명으로 컴포넌트 재생성
|
||||||
tableName={selectedColumn.tableName}
|
tableName={selectedColumn.tableName}
|
||||||
columnName={selectedColumn.columnName}
|
columnName={selectedColumn.columnName}
|
||||||
columnLabel={selectedColumn.columnLabel}
|
columnLabel={selectedColumn.columnLabel}
|
||||||
|
|
|
||||||
|
|
@ -147,17 +147,18 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{columns.map((column) => {
|
{columns.map((column) => {
|
||||||
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
||||||
|
const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={uniqueKey}
|
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 ${
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<FolderTree
|
<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">
|
<div className="flex-1">
|
||||||
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
||||||
import { ComponentRendererProps } from "../../types";
|
import { ComponentRendererProps } from "../../types";
|
||||||
import { SplitPanelLayoutConfig } from "./types";
|
import { SplitPanelLayoutConfig } from "./types";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 { Input } from "@/components/ui/input";
|
||||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react";
|
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||||
import { TableFilter, ColumnVisibility } from "@/types/table-options";
|
import { TableFilter, ColumnVisibility } from "@/types/table-options";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||||
// 추가 props
|
// 추가 props
|
||||||
|
|
@ -44,6 +46,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [leftFilters, setLeftFilters] = useState<TableFilter[]>([]);
|
const [leftFilters, setLeftFilters] = useState<TableFilter[]>([]);
|
||||||
const [leftGrouping, setLeftGrouping] = useState<string[]>([]);
|
const [leftGrouping, setLeftGrouping] = useState<string[]>([]);
|
||||||
const [leftColumnVisibility, setLeftColumnVisibility] = useState<ColumnVisibility[]>([]);
|
const [leftColumnVisibility, setLeftColumnVisibility] = useState<ColumnVisibility[]>([]);
|
||||||
|
const [leftColumnOrder, setLeftColumnOrder] = useState<string[]>([]); // 🔧 컬럼 순서
|
||||||
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
|
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
|
||||||
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
|
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
|
||||||
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
|
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
|
||||||
|
|
@ -160,6 +163,84 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return rootItems;
|
return rootItems;
|
||||||
}, [componentConfig.leftPanel?.itemAddConfig]);
|
}, [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 loadLeftData = useCallback(async () => {
|
||||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||||
|
|
@ -167,12 +248,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
|
|
||||||
setIsLoadingLeft(true);
|
setIsLoadingLeft(true);
|
||||||
try {
|
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,
|
page: 1,
|
||||||
size: 100,
|
size: 100,
|
||||||
// searchTerm 제거 - 클라이언트 사이드에서 필터링
|
search: filters, // 필터 조건 전달
|
||||||
|
enableEntityJoin: true, // 엔티티 조인 활성화
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
||||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||||
if (leftColumn && result.data.length > 0) {
|
if (leftColumn && result.data.length > 0) {
|
||||||
|
|
@ -196,7 +283,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingLeft(false);
|
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(
|
const loadRightData = useCallback(
|
||||||
|
|
@ -283,67 +370,101 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
[rightTableColumns],
|
[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에 등록)
|
// 좌측 테이블 등록 (Context에 등록)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||||
if (!leftTableName || isDesignMode) return;
|
if (!leftTableName || isDesignMode) return;
|
||||||
|
|
||||||
const leftTableId = `split-panel-left-${component.id}`;
|
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 (leftColumns.length > 0) {
|
// 화면에 설정된 컬럼이 없으면 등록하지 않음
|
||||||
registerTable({
|
if (displayColumns.length === 0) return;
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => unregisterTable(leftTableId);
|
// 테이블명이 있으면 등록
|
||||||
}
|
registerTable({
|
||||||
}, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.displayColumns, leftColumnLabels, component.title, isDesignMode]);
|
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, // 🔧 고유값 가져오기 함수 추가
|
||||||
|
});
|
||||||
|
|
||||||
// 우측 테이블 등록 (Context에 등록)
|
return () => unregisterTable(leftTableId);
|
||||||
useEffect(() => {
|
}, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode, getLeftColumnUniqueValues]);
|
||||||
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);
|
// useEffect(() => {
|
||||||
|
// const rightTableName = componentConfig.rightPanel?.tableName;
|
||||||
if (rightColumns.length > 0) {
|
// if (!rightTableName || isDesignMode) return;
|
||||||
registerTable({
|
//
|
||||||
tableId: rightTableId,
|
// const rightTableId = `split-panel-right-${component.id}`;
|
||||||
label: `${component.title || "분할 패널"} (우측)`,
|
// // 🔧 화면에 표시되는 컬럼만 등록 (displayColumns 또는 columns)
|
||||||
tableName: rightTableName,
|
// const displayColumns = componentConfig.rightPanel?.columns || [];
|
||||||
columns: rightColumns.map((col: string) => ({
|
// const rightColumns = displayColumns.map((col: any) => col.columnName || col.name || col).filter(Boolean);
|
||||||
columnName: col,
|
//
|
||||||
columnLabel: rightColumnLabels[col] || col,
|
// if (rightColumns.length > 0) {
|
||||||
inputType: "text",
|
// registerTable({
|
||||||
visible: true,
|
// tableId: rightTableId,
|
||||||
width: 150,
|
// label: `${component.title || "분할 패널"} (우측)`,
|
||||||
sortable: true,
|
// tableName: rightTableName,
|
||||||
filterable: true,
|
// columns: rightColumns.map((col: string) => ({
|
||||||
})),
|
// columnName: col,
|
||||||
onFilterChange: setRightFilters,
|
// columnLabel: rightColumnLabels[col] || col,
|
||||||
onGroupChange: setRightGrouping,
|
// inputType: "text",
|
||||||
onColumnVisibilityChange: setRightColumnVisibility,
|
// visible: true,
|
||||||
});
|
// width: 150,
|
||||||
|
// sortable: true,
|
||||||
return () => unregisterTable(rightTableId);
|
// filterable: true,
|
||||||
}
|
// })),
|
||||||
}, [component.id, componentConfig.rightPanel?.tableName, rightTableColumns, rightColumnLabels, component.title, isDesignMode]);
|
// onFilterChange: setRightFilters,
|
||||||
|
// onGroupChange: setRightGrouping,
|
||||||
|
// onColumnVisibilityChange: setRightColumnVisibility,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return () => unregisterTable(rightTableId);
|
||||||
|
// }
|
||||||
|
// }, [component.id, componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, rightColumnLabels, component.title, isDesignMode]);
|
||||||
|
|
||||||
// 좌측 테이블 컬럼 라벨 로드
|
// 좌측 테이블 컬럼 라벨 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -786,6 +907,43 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}
|
}
|
||||||
}, [addModalPanel, componentConfig, addModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
||||||
|
|
@ -794,6 +952,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isDesignMode, componentConfig.autoLoad]);
|
}, [isDesignMode, componentConfig.autoLoad]);
|
||||||
|
|
||||||
|
// 🔄 필터 변경 시 데이터 다시 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesignMode && componentConfig.autoLoad !== false) {
|
||||||
|
loadLeftData();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [leftFilters]);
|
||||||
|
|
||||||
// 리사이저 드래그 핸들러
|
// 리사이저 드래그 핸들러
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
if (!resizable) return;
|
if (!resizable) return;
|
||||||
|
|
@ -933,6 +1099,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
|
// 🔧 로컬 검색 필터 적용
|
||||||
const filteredData = leftSearchQuery
|
const filteredData = leftSearchQuery
|
||||||
? leftData.filter((item) => {
|
? leftData.filter((item) => {
|
||||||
const searchLower = leftSearchQuery.toLowerCase();
|
const searchLower = leftSearchQuery.toLowerCase();
|
||||||
|
|
@ -943,12 +1110,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
})
|
})
|
||||||
: leftData;
|
: leftData;
|
||||||
|
|
||||||
const displayColumns = componentConfig.leftPanel?.columns || [];
|
// 🔧 가시성 처리된 컬럼 사용
|
||||||
const columnsToShow = displayColumns.length > 0
|
const columnsToShow = visibleLeftColumns.length > 0
|
||||||
? displayColumns.map(col => ({
|
? visibleLeftColumns.map((col: any) => {
|
||||||
...col,
|
const colName = typeof col === 'string' ? col : (col.name || col.columnName);
|
||||||
label: leftColumnLabels[col.name] || col.label || col.name
|
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 => ({
|
: Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({
|
||||||
name: key,
|
name: key,
|
||||||
label: leftColumnLabels[key] || key,
|
label: leftColumnLabels[key] || key,
|
||||||
|
|
@ -956,6 +1128,66 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
align: "left" as const
|
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 (
|
return (
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,15 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tables = Array.from(registeredTables.values());
|
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) {
|
if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) {
|
||||||
|
console.log("✅ [TableSearchWidget] 첫 번째 테이블 자동 선택:", tables[0].tableId);
|
||||||
setSelectedTableId(tables[0].tableId);
|
setSelectedTableId(tables[0].tableId);
|
||||||
}
|
}
|
||||||
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue