Compare commits
7 Commits
4247c3bb70
...
d3b4c4c42e
| Author | SHA1 | Date |
|---|---|---|
|
|
d3b4c4c42e | |
|
|
f2b0ac8fd5 | |
|
|
a4cf11264d | |
|
|
215242b676 | |
|
|
fc5ffb03b2 | |
|
|
2327dbe35c | |
|
|
b9ee860e71 |
|
|
@ -134,8 +134,8 @@ export class EntityJoinService {
|
||||||
`🔧 기존 display_column 사용: ${column.column_name} → ${displayColumn}`
|
`🔧 기존 display_column 사용: ${column.column_name} → ${displayColumn}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// display_column이 "none"이거나 없는 경우 참조 테이블의 모든 컬럼 가져오기
|
// display_column이 "none"이거나 없는 경우 참조 테이블의 표시용 컬럼 자동 감지
|
||||||
logger.info(`🔍 ${referenceTable}의 모든 컬럼 조회 중...`);
|
logger.info(`🔍 ${referenceTable}의 표시 컬럼 자동 감지 중...`);
|
||||||
|
|
||||||
// 참조 테이블의 모든 컬럼 이름 가져오기
|
// 참조 테이블의 모든 컬럼 이름 가져오기
|
||||||
const tableColumnsResult = await query<{ column_name: string }>(
|
const tableColumnsResult = await query<{ column_name: string }>(
|
||||||
|
|
@ -148,10 +148,34 @@ export class EntityJoinService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tableColumnsResult.length > 0) {
|
if (tableColumnsResult.length > 0) {
|
||||||
displayColumns = tableColumnsResult.map((col) => col.column_name);
|
const allColumns = tableColumnsResult.map((col) => col.column_name);
|
||||||
|
|
||||||
|
// 🆕 표시용 컬럼 자동 감지 (우선순위 순서)
|
||||||
|
// 1. *_name 컬럼 (item_name, customer_name 등)
|
||||||
|
// 2. name 컬럼
|
||||||
|
// 3. label 컬럼
|
||||||
|
// 4. title 컬럼
|
||||||
|
// 5. 참조 컬럼 (referenceColumn)
|
||||||
|
const nameColumn = allColumns.find(
|
||||||
|
(col) => col.endsWith("_name") && col !== "company_name"
|
||||||
|
);
|
||||||
|
const simpleNameColumn = allColumns.find((col) => col === "name");
|
||||||
|
const labelColumn = allColumns.find(
|
||||||
|
(col) => col === "label" || col.endsWith("_label")
|
||||||
|
);
|
||||||
|
const titleColumn = allColumns.find((col) => col === "title");
|
||||||
|
|
||||||
|
// 우선순위에 따라 표시 컬럼 선택
|
||||||
|
const displayColumn =
|
||||||
|
nameColumn ||
|
||||||
|
simpleNameColumn ||
|
||||||
|
labelColumn ||
|
||||||
|
titleColumn ||
|
||||||
|
referenceColumn;
|
||||||
|
displayColumns = [displayColumn];
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`✅ ${referenceTable}의 모든 컬럼 자동 포함 (${displayColumns.length}개):`,
|
`✅ ${referenceTable}의 표시 컬럼 자동 감지: ${displayColumn} (전체 ${allColumns.length}개 중)`
|
||||||
displayColumns.join(", ")
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 테이블 컬럼을 못 찾으면 기본값 사용
|
// 테이블 컬럼을 못 찾으면 기본값 사용
|
||||||
|
|
|
||||||
|
|
@ -798,7 +798,12 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🔥 해당 컬럼을 사용하는 화면 레이아웃의 widgetType도 업데이트
|
// 🔥 해당 컬럼을 사용하는 화면 레이아웃의 widgetType도 업데이트
|
||||||
await this.syncScreenLayoutsInputType(tableName, columnName, inputType, companyCode);
|
await this.syncScreenLayoutsInputType(
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
inputType,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
|
||||||
// 🔥 캐시 무효화: 해당 테이블의 컬럼 캐시 삭제
|
// 🔥 캐시 무효화: 해당 테이블의 컬럼 캐시 삭제
|
||||||
const cacheKeyPattern = `${CacheKeys.TABLE_COLUMNS(tableName, 1, 1000)}_${companyCode}`;
|
const cacheKeyPattern = `${CacheKeys.TABLE_COLUMNS(tableName, 1, 1000)}_${companyCode}`;
|
||||||
|
|
@ -928,7 +933,11 @@ export class TableManagementService {
|
||||||
`UPDATE screen_layouts
|
`UPDATE screen_layouts
|
||||||
SET properties = $1, component_type = $2
|
SET properties = $1, component_type = $2
|
||||||
WHERE layout_id = $3`,
|
WHERE layout_id = $3`,
|
||||||
[JSON.stringify(updatedProperties), newComponentType, layout.layout_id]
|
[
|
||||||
|
JSON.stringify(updatedProperties),
|
||||||
|
newComponentType,
|
||||||
|
layout.layout_id,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -1299,18 +1308,30 @@ export class TableManagementService {
|
||||||
try {
|
try {
|
||||||
// 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
|
// 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
|
||||||
if (typeof value === "string" && value.includes("|")) {
|
if (typeof value === "string" && value.includes("|")) {
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
const columnInfo = await this.getColumnWebTypeInfo(
|
||||||
|
tableName,
|
||||||
|
columnName
|
||||||
|
);
|
||||||
|
|
||||||
// 날짜 타입이면 날짜 범위로 처리
|
// 날짜 타입이면 날짜 범위로 처리
|
||||||
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
|
if (
|
||||||
|
columnInfo &&
|
||||||
|
(columnInfo.webType === "date" || columnInfo.webType === "datetime")
|
||||||
|
) {
|
||||||
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 그 외 타입이면 다중선택(IN 조건)으로 처리
|
// 그 외 타입이면 다중선택(IN 조건)으로 처리
|
||||||
const multiValues = value.split("|").filter((v: string) => v.trim() !== "");
|
const multiValues = value
|
||||||
|
.split("|")
|
||||||
|
.filter((v: string) => v.trim() !== "");
|
||||||
if (multiValues.length > 0) {
|
if (multiValues.length > 0) {
|
||||||
const placeholders = multiValues.map((_: string, idx: number) => `$${paramIndex + idx}`).join(", ");
|
const placeholders = multiValues
|
||||||
logger.info(`🔍 다중선택 필터 적용: ${columnName} IN (${multiValues.join(", ")})`);
|
.map((_: string, idx: number) => `$${paramIndex + idx}`)
|
||||||
|
.join(", ");
|
||||||
|
logger.info(
|
||||||
|
`🔍 다중선택 필터 적용: ${columnName} IN (${multiValues.join(", ")})`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text IN (${placeholders})`,
|
whereClause: `${columnName}::text IN (${placeholders})`,
|
||||||
values: multiValues,
|
values: multiValues,
|
||||||
|
|
@ -1320,10 +1341,20 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 날짜 범위 객체 {from, to} 체크
|
// 🔧 날짜 범위 객체 {from, to} 체크
|
||||||
if (typeof value === "object" && value !== null && ("from" in value || "to" in value)) {
|
if (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
("from" in value || "to" in value)
|
||||||
|
) {
|
||||||
// 날짜 범위 객체는 그대로 전달
|
// 날짜 범위 객체는 그대로 전달
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
const columnInfo = await this.getColumnWebTypeInfo(
|
||||||
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
|
tableName,
|
||||||
|
columnName
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
columnInfo &&
|
||||||
|
(columnInfo.webType === "date" || columnInfo.webType === "datetime")
|
||||||
|
) {
|
||||||
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1356,9 +1387,10 @@ export class TableManagementService {
|
||||||
|
|
||||||
// 컬럼 타입 정보 조회
|
// 컬럼 타입 정보 조회
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
||||||
logger.info(`🔍 [buildAdvancedSearchCondition] ${tableName}.${columnName}`,
|
logger.info(
|
||||||
`webType=${columnInfo?.webType || 'NULL'}`,
|
`🔍 [buildAdvancedSearchCondition] ${tableName}.${columnName}`,
|
||||||
`inputType=${columnInfo?.inputType || 'NULL'}`,
|
`webType=${columnInfo?.webType || "NULL"}`,
|
||||||
|
`inputType=${columnInfo?.inputType || "NULL"}`,
|
||||||
`actualValue=${JSON.stringify(actualValue)}`,
|
`actualValue=${JSON.stringify(actualValue)}`,
|
||||||
`operator=${operator}`
|
`operator=${operator}`
|
||||||
);
|
);
|
||||||
|
|
@ -1467,13 +1499,17 @@ export class TableManagementService {
|
||||||
|
|
||||||
if (fromStr && fromStr.trim() !== "") {
|
if (fromStr && fromStr.trim() !== "") {
|
||||||
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
|
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
|
||||||
conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
|
conditions.push(
|
||||||
|
`${columnName}::date >= $${paramIndex + paramCount}::date`
|
||||||
|
);
|
||||||
values.push(fromStr.trim());
|
values.push(fromStr.trim());
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
if (toStr && toStr.trim() !== "") {
|
if (toStr && toStr.trim() !== "") {
|
||||||
// 종료일은 해당 날짜의 23:59:59까지 포함
|
// 종료일은 해당 날짜의 23:59:59까지 포함
|
||||||
conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
|
conditions.push(
|
||||||
|
`${columnName}::date <= $${paramIndex + paramCount}::date`
|
||||||
|
);
|
||||||
values.push(toStr.trim());
|
values.push(toStr.trim());
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
|
|
@ -1482,13 +1518,17 @@ export class TableManagementService {
|
||||||
else if (typeof value === "object" && value !== null) {
|
else if (typeof value === "object" && value !== null) {
|
||||||
if (value.from) {
|
if (value.from) {
|
||||||
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
|
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
|
||||||
conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
|
conditions.push(
|
||||||
|
`${columnName}::date >= $${paramIndex + paramCount}::date`
|
||||||
|
);
|
||||||
values.push(value.from);
|
values.push(value.from);
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
if (value.to) {
|
if (value.to) {
|
||||||
// 종료일은 해당 날짜의 23:59:59까지 포함
|
// 종료일은 해당 날짜의 23:59:59까지 포함
|
||||||
conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
|
conditions.push(
|
||||||
|
`${columnName}::date <= $${paramIndex + paramCount}::date`
|
||||||
|
);
|
||||||
values.push(value.to);
|
values.push(value.to);
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
|
|
@ -1660,7 +1700,9 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IN 절로 여러 값 검색
|
// IN 절로 여러 값 검색
|
||||||
const placeholders = value.map((_, idx) => `$${paramIndex + idx}`).join(", ");
|
const placeholders = value
|
||||||
|
.map((_, idx) => `$${paramIndex + idx}`)
|
||||||
|
.join(", ");
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName} IN (${placeholders})`,
|
whereClause: `${columnName} IN (${placeholders})`,
|
||||||
values: value,
|
values: value,
|
||||||
|
|
@ -1776,14 +1818,19 @@ export class TableManagementService {
|
||||||
[tableName, columnName]
|
[tableName, columnName]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`, {
|
logger.info(
|
||||||
found: !!result,
|
`🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`,
|
||||||
web_type: result?.web_type,
|
{
|
||||||
input_type: result?.input_type,
|
found: !!result,
|
||||||
});
|
web_type: result?.web_type,
|
||||||
|
input_type: result?.input_type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
logger.warn(`⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`);
|
logger.warn(
|
||||||
|
`⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1799,7 +1846,9 @@ export class TableManagementService {
|
||||||
displayColumn: result.display_column || undefined,
|
displayColumn: result.display_column || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`);
|
logger.info(
|
||||||
|
`✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`
|
||||||
|
);
|
||||||
return columnInfo;
|
return columnInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -1913,6 +1962,15 @@ export class TableManagementService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 조인 테이블 컬럼 (테이블명.컬럼명)은 기본 데이터 조회에서 제외
|
||||||
|
// Entity 조인 조회에서만 처리됨
|
||||||
|
if (column.includes(".")) {
|
||||||
|
logger.info(
|
||||||
|
`🔍 조인 테이블 컬럼 ${column} 기본 조회에서 제외 (Entity 조인에서 처리)`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
|
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
|
||||||
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
|
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
|
||||||
|
|
||||||
|
|
@ -2741,7 +2799,11 @@ export class TableManagementService {
|
||||||
WHERE "${referenceColumn}" IS NOT NULL`;
|
WHERE "${referenceColumn}" IS NOT NULL`;
|
||||||
|
|
||||||
// 추가 필터 조건이 있으면 적용 (예: 특정 거래처의 품목만 제외)
|
// 추가 필터 조건이 있으면 적용 (예: 특정 거래처의 품목만 제외)
|
||||||
if (filterColumn && filterValue !== undefined && filterValue !== null) {
|
if (
|
||||||
|
filterColumn &&
|
||||||
|
filterValue !== undefined &&
|
||||||
|
filterValue !== null
|
||||||
|
) {
|
||||||
excludeSubquery += ` AND "${filterColumn}" = '${String(filterValue).replace(/'/g, "''")}'`;
|
excludeSubquery += ` AND "${filterColumn}" = '${String(filterValue).replace(/'/g, "''")}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2934,16 +2996,22 @@ export class TableManagementService {
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 🆕 테이블명.컬럼명 형식도 Entity 검색으로 인식
|
||||||
|
const hasJoinTableSearch =
|
||||||
|
options.search &&
|
||||||
|
Object.keys(options.search).some((key) => key.includes("."));
|
||||||
|
|
||||||
const hasEntitySearch =
|
const hasEntitySearch =
|
||||||
options.search &&
|
options.search &&
|
||||||
Object.keys(options.search).some((key) =>
|
(Object.keys(options.search).some((key) =>
|
||||||
allEntityColumns.includes(key)
|
allEntityColumns.includes(key)
|
||||||
);
|
) ||
|
||||||
|
hasJoinTableSearch);
|
||||||
|
|
||||||
if (hasEntitySearch) {
|
if (hasEntitySearch) {
|
||||||
const entitySearchKeys = options.search
|
const entitySearchKeys = options.search
|
||||||
? Object.keys(options.search).filter((key) =>
|
? Object.keys(options.search).filter(
|
||||||
allEntityColumns.includes(key)
|
(key) => allEntityColumns.includes(key) || key.includes(".")
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -2988,47 +3056,113 @@ export class TableManagementService {
|
||||||
|
|
||||||
if (options.search) {
|
if (options.search) {
|
||||||
for (const [key, value] of Object.entries(options.search)) {
|
for (const [key, value] of Object.entries(options.search)) {
|
||||||
|
// 검색값 추출 (객체 형태일 수 있음)
|
||||||
|
let searchValue = value;
|
||||||
|
if (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"value" in value
|
||||||
|
) {
|
||||||
|
searchValue = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빈 값이면 스킵
|
||||||
|
if (
|
||||||
|
searchValue === "__ALL__" ||
|
||||||
|
searchValue === "" ||
|
||||||
|
searchValue === null ||
|
||||||
|
searchValue === undefined
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeValue = String(searchValue).replace(/'/g, "''");
|
||||||
|
|
||||||
|
// 🆕 테이블명.컬럼명 형식 처리 (예: item_info.item_name)
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const [refTable, refColumn] = key.split(".");
|
||||||
|
|
||||||
|
// aliasMap에서 별칭 찾기 (테이블명:소스컬럼 형식)
|
||||||
|
let foundAlias: string | undefined;
|
||||||
|
for (const [aliasKey, alias] of aliasMap.entries()) {
|
||||||
|
if (aliasKey.startsWith(`${refTable}:`)) {
|
||||||
|
foundAlias = alias;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundAlias) {
|
||||||
|
whereConditions.push(
|
||||||
|
`${foundAlias}.${refColumn}::text ILIKE '%${safeValue}%'`
|
||||||
|
);
|
||||||
|
entitySearchColumns.push(`${key} (${refTable}.${refColumn})`);
|
||||||
|
logger.info(
|
||||||
|
`🎯 조인 테이블 검색: ${key} → ${refTable}.${refColumn} LIKE '%${safeValue}%' (별칭: ${foundAlias})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ 조인 테이블 검색 실패: ${key} - 별칭을 찾을 수 없음`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const joinConfig = joinConfigs.find(
|
const joinConfig = joinConfigs.find(
|
||||||
(config) => config.aliasColumn === key
|
(config) => config.aliasColumn === key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (joinConfig) {
|
if (joinConfig) {
|
||||||
// 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
|
// 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
|
||||||
const alias = aliasMap.get(joinConfig.referenceTable);
|
const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`;
|
||||||
|
const alias = aliasMap.get(aliasKey);
|
||||||
whereConditions.push(
|
whereConditions.push(
|
||||||
`${alias}.${joinConfig.displayColumn} ILIKE '%${value}%'`
|
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
|
||||||
);
|
);
|
||||||
entitySearchColumns.push(
|
entitySearchColumns.push(
|
||||||
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
|
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${value}%' (별칭: ${alias})`
|
`🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})`
|
||||||
);
|
);
|
||||||
} else if (key === "writer_dept_code") {
|
} else if (key === "writer_dept_code") {
|
||||||
// writer_dept_code: user_info.dept_code에서 검색
|
// writer_dept_code: user_info.dept_code에서 검색
|
||||||
const userAlias = aliasMap.get("user_info");
|
const userAliasKey = Array.from(aliasMap.keys()).find((k) =>
|
||||||
whereConditions.push(
|
k.startsWith("user_info:")
|
||||||
`${userAlias}.dept_code ILIKE '%${value}%'`
|
|
||||||
);
|
|
||||||
entitySearchColumns.push(`${key} (user_info.dept_code)`);
|
|
||||||
logger.info(
|
|
||||||
`🎯 추가 Entity 조인 검색: ${key} → user_info.dept_code LIKE '%${value}%' (별칭: ${userAlias})`
|
|
||||||
);
|
);
|
||||||
|
const userAlias = userAliasKey
|
||||||
|
? aliasMap.get(userAliasKey)
|
||||||
|
: undefined;
|
||||||
|
if (userAlias) {
|
||||||
|
whereConditions.push(
|
||||||
|
`${userAlias}.dept_code ILIKE '%${safeValue}%'`
|
||||||
|
);
|
||||||
|
entitySearchColumns.push(`${key} (user_info.dept_code)`);
|
||||||
|
logger.info(
|
||||||
|
`🎯 추가 Entity 조인 검색: ${key} → user_info.dept_code LIKE '%${safeValue}%' (별칭: ${userAlias})`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (key === "company_code_status") {
|
} else if (key === "company_code_status") {
|
||||||
// company_code_status: company_info.status에서 검색
|
// company_code_status: company_info.status에서 검색
|
||||||
const companyAlias = aliasMap.get("company_info");
|
const companyAliasKey = Array.from(aliasMap.keys()).find((k) =>
|
||||||
whereConditions.push(
|
k.startsWith("company_info:")
|
||||||
`${companyAlias}.status ILIKE '%${value}%'`
|
|
||||||
);
|
|
||||||
entitySearchColumns.push(`${key} (company_info.status)`);
|
|
||||||
logger.info(
|
|
||||||
`🎯 추가 Entity 조인 검색: ${key} → company_info.status LIKE '%${value}%' (별칭: ${companyAlias})`
|
|
||||||
);
|
);
|
||||||
|
const companyAlias = companyAliasKey
|
||||||
|
? aliasMap.get(companyAliasKey)
|
||||||
|
: undefined;
|
||||||
|
if (companyAlias) {
|
||||||
|
whereConditions.push(
|
||||||
|
`${companyAlias}.status ILIKE '%${safeValue}%'`
|
||||||
|
);
|
||||||
|
entitySearchColumns.push(`${key} (company_info.status)`);
|
||||||
|
logger.info(
|
||||||
|
`🎯 추가 Entity 조인 검색: ${key} → company_info.status LIKE '%${safeValue}%' (별칭: ${companyAlias})`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 일반 컬럼인 경우: 메인 테이블에서 검색
|
// 일반 컬럼인 경우: 메인 테이블에서 검색
|
||||||
whereConditions.push(`main.${key} ILIKE '%${value}%'`);
|
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
|
||||||
logger.info(
|
logger.info(
|
||||||
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${value}%'`
|
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3168,6 +3302,59 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 🆕 조인 테이블 컬럼 검색 처리 (예: item_info.item_name)
|
||||||
|
if (columnName.includes(".")) {
|
||||||
|
const [refTable, refColumn] = columnName.split(".");
|
||||||
|
|
||||||
|
// 검색값 추출
|
||||||
|
let searchValue = value;
|
||||||
|
if (typeof value === "object" && value !== null && "value" in value) {
|
||||||
|
searchValue = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchValue === "__ALL__" ||
|
||||||
|
searchValue === "" ||
|
||||||
|
searchValue === null
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 column_labels에서 해당 엔티티 설정 찾기
|
||||||
|
// 예: item_info 테이블을 참조하는 컬럼 찾기 (item_code → item_info)
|
||||||
|
const entityColumnResult = await query<{
|
||||||
|
column_name: string;
|
||||||
|
reference_table: string;
|
||||||
|
reference_column: string;
|
||||||
|
}>(
|
||||||
|
`SELECT column_name, reference_table, reference_column
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND input_type = 'entity'
|
||||||
|
AND reference_table = $2
|
||||||
|
LIMIT 1`,
|
||||||
|
[tableName, refTable]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entityColumnResult.length > 0) {
|
||||||
|
// 조인 별칭 생성 (entityJoinService.ts와 동일한 패턴: 테이블명 앞 3글자)
|
||||||
|
const joinAlias = refTable.substring(0, 3);
|
||||||
|
|
||||||
|
// 조인 테이블 컬럼으로 검색 조건 생성
|
||||||
|
const safeValue = String(searchValue).replace(/'/g, "''");
|
||||||
|
const condition = `${joinAlias}.${refColumn}::text ILIKE '%${safeValue}%'`;
|
||||||
|
|
||||||
|
logger.info(`🔍 조인 테이블 검색 조건: ${condition}`);
|
||||||
|
conditions.push(condition);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ 조인 테이블 검색 실패: ${columnName} - 엔티티 설정을 찾을 수 없음`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 고급 검색 조건 구성
|
// 고급 검색 조건 구성
|
||||||
const searchCondition = await this.buildAdvancedSearchCondition(
|
const searchCondition = await this.buildAdvancedSearchCondition(
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -4282,7 +4469,10 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
return result.length > 0;
|
return result.length > 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`컬럼 존재 여부 확인 실패: ${tableName}.${columnName}`, error);
|
logger.error(
|
||||||
|
`컬럼 존재 여부 확인 실패: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,13 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Layers } from "lucide-react";
|
||||||
import { TableFilter } from "@/types/table-options";
|
import { TableFilter, GroupSumConfig } from "@/types/table-options";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -77,6 +73,10 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFilterConfig[]>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFilterConfig[]>([]);
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
|
// 🆕 그룹별 합산 설정
|
||||||
|
const [groupSumEnabled, setGroupSumEnabled] = useState(false);
|
||||||
|
const [groupByColumn, setGroupByColumn] = useState<string>("");
|
||||||
|
|
||||||
// localStorage에서 저장된 필터 설정 불러오기
|
// localStorage에서 저장된 필터 설정 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (table?.columns && table?.tableName) {
|
if (table?.columns && table?.tableName) {
|
||||||
|
|
@ -86,6 +86,22 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
: `table_filters_${table.tableName}`;
|
: `table_filters_${table.tableName}`;
|
||||||
const savedFilters = localStorage.getItem(storageKey);
|
const savedFilters = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
// 🆕 그룹핑 설정도 불러오기
|
||||||
|
const groupSumKey = screenId
|
||||||
|
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
||||||
|
: `table_groupsum_${table.tableName}`;
|
||||||
|
const savedGroupSum = localStorage.getItem(groupSumKey);
|
||||||
|
|
||||||
|
if (savedGroupSum) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedGroupSum) as GroupSumConfig;
|
||||||
|
setGroupSumEnabled(parsed.enabled);
|
||||||
|
setGroupByColumn(parsed.groupByColumn || "");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("그룹핑 설정 불러오기 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filters: ColumnFilterConfig[];
|
let filters: ColumnFilterConfig[];
|
||||||
|
|
||||||
if (savedFilters) {
|
if (savedFilters) {
|
||||||
|
|
@ -96,13 +112,15 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
.filter((col) => col.filterable !== false)
|
.filter((col) => col.filterable !== false)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const saved = parsed.find((f) => f.columnName === col.columnName);
|
const saved = parsed.find((f) => f.columnName === col.columnName);
|
||||||
return saved || {
|
return (
|
||||||
columnName: col.columnName,
|
saved || {
|
||||||
columnLabel: col.columnLabel,
|
columnName: col.columnName,
|
||||||
inputType: col.inputType || "text",
|
columnLabel: col.columnLabel,
|
||||||
enabled: false,
|
inputType: col.inputType || "text",
|
||||||
filterType: mapInputTypeToFilterType(col.inputType || "text"),
|
enabled: false,
|
||||||
};
|
filterType: mapInputTypeToFilterType(col.inputType || "text"),
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("저장된 필터 설정 불러오기 실패:", error);
|
console.error("저장된 필터 설정 불러오기 실패:", error);
|
||||||
|
|
@ -133,20 +151,14 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
}, [table?.columns, table?.tableName]);
|
}, [table?.columns, table?.tableName]);
|
||||||
|
|
||||||
// inputType을 filterType으로 매핑
|
// inputType을 filterType으로 매핑
|
||||||
const mapInputTypeToFilterType = (
|
const mapInputTypeToFilterType = (inputType: string): "text" | "number" | "date" | "select" => {
|
||||||
inputType: string
|
|
||||||
): "text" | "number" | "date" | "select" => {
|
|
||||||
if (inputType.includes("number") || inputType.includes("decimal")) {
|
if (inputType.includes("number") || inputType.includes("decimal")) {
|
||||||
return "number";
|
return "number";
|
||||||
}
|
}
|
||||||
if (inputType.includes("date") || inputType.includes("time")) {
|
if (inputType.includes("date") || inputType.includes("time")) {
|
||||||
return "date";
|
return "date";
|
||||||
}
|
}
|
||||||
if (
|
if (inputType.includes("select") || inputType.includes("code") || inputType.includes("category")) {
|
||||||
inputType.includes("select") ||
|
|
||||||
inputType.includes("code") ||
|
|
||||||
inputType.includes("category")
|
|
||||||
) {
|
|
||||||
return "select";
|
return "select";
|
||||||
}
|
}
|
||||||
return "text";
|
return "text";
|
||||||
|
|
@ -155,31 +167,20 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
// 전체 선택/해제
|
// 전체 선택/해제
|
||||||
const toggleSelectAll = (checked: boolean) => {
|
const toggleSelectAll = (checked: boolean) => {
|
||||||
setSelectAll(checked);
|
setSelectAll(checked);
|
||||||
setColumnFilters((prev) =>
|
setColumnFilters((prev) => prev.map((filter) => ({ ...filter, enabled: checked })));
|
||||||
prev.map((filter) => ({ ...filter, enabled: checked }))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 개별 필터 토글
|
// 개별 필터 토글
|
||||||
const toggleFilter = (columnName: string) => {
|
const toggleFilter = (columnName: string) => {
|
||||||
setColumnFilters((prev) =>
|
setColumnFilters((prev) =>
|
||||||
prev.map((filter) =>
|
prev.map((filter) => (filter.columnName === columnName ? { ...filter, enabled: !filter.enabled } : filter)),
|
||||||
filter.columnName === columnName
|
|
||||||
? { ...filter, enabled: !filter.enabled }
|
|
||||||
: filter
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필터 타입 변경
|
// 필터 타입 변경
|
||||||
const updateFilterType = (
|
const updateFilterType = (columnName: string, filterType: "text" | "number" | "date" | "select") => {
|
||||||
columnName: string,
|
|
||||||
filterType: "text" | "number" | "date" | "select"
|
|
||||||
) => {
|
|
||||||
setColumnFilters((prev) =>
|
setColumnFilters((prev) =>
|
||||||
prev.map((filter) =>
|
prev.map((filter) => (filter.columnName === columnName ? { ...filter, filterType } : filter)),
|
||||||
filter.columnName === columnName ? { ...filter, filterType } : filter
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -202,6 +203,25 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
? `table_filters_${table.tableName}_screen_${screenId}`
|
? `table_filters_${table.tableName}_screen_${screenId}`
|
||||||
: `table_filters_${table.tableName}`;
|
: `table_filters_${table.tableName}`;
|
||||||
localStorage.setItem(storageKey, JSON.stringify(columnFilters));
|
localStorage.setItem(storageKey, JSON.stringify(columnFilters));
|
||||||
|
|
||||||
|
// 🆕 그룹핑 설정 저장
|
||||||
|
const groupSumKey = screenId
|
||||||
|
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
||||||
|
: `table_groupsum_${table.tableName}`;
|
||||||
|
|
||||||
|
if (groupSumEnabled && groupByColumn) {
|
||||||
|
const selectedColumn = columnFilters.find((f) => f.columnName === groupByColumn);
|
||||||
|
const groupSumConfig: GroupSumConfig = {
|
||||||
|
enabled: true,
|
||||||
|
groupByColumn: groupByColumn,
|
||||||
|
groupByColumnLabel: selectedColumn?.columnLabel,
|
||||||
|
};
|
||||||
|
localStorage.setItem(groupSumKey, JSON.stringify(groupSumConfig));
|
||||||
|
table?.onGroupSumChange?.(groupSumConfig);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(groupSumKey);
|
||||||
|
table?.onGroupSumChange?.(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table?.onFilterChange(activeFilters);
|
table?.onFilterChange(activeFilters);
|
||||||
|
|
@ -216,23 +236,36 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
const clearedFilters = columnFilters.map((filter) => ({
|
const clearedFilters = columnFilters.map((filter) => ({
|
||||||
...filter,
|
...filter,
|
||||||
enabled: false
|
enabled: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setColumnFilters(clearedFilters);
|
setColumnFilters(clearedFilters);
|
||||||
setSelectAll(false);
|
setSelectAll(false);
|
||||||
|
|
||||||
|
// 🆕 그룹핑 설정 초기화
|
||||||
|
setGroupSumEnabled(false);
|
||||||
|
setGroupByColumn("");
|
||||||
|
|
||||||
// localStorage에서 제거 (화면별로 독립적)
|
// localStorage에서 제거 (화면별로 독립적)
|
||||||
if (table?.tableName) {
|
if (table?.tableName) {
|
||||||
const storageKey = screenId
|
const storageKey = screenId
|
||||||
? `table_filters_${table.tableName}_screen_${screenId}`
|
? `table_filters_${table.tableName}_screen_${screenId}`
|
||||||
: `table_filters_${table.tableName}`;
|
: `table_filters_${table.tableName}`;
|
||||||
localStorage.removeItem(storageKey);
|
localStorage.removeItem(storageKey);
|
||||||
|
|
||||||
|
// 🆕 그룹핑 설정도 제거
|
||||||
|
const groupSumKey = screenId
|
||||||
|
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
||||||
|
: `table_groupsum_${table.tableName}`;
|
||||||
|
localStorage.removeItem(groupSumKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 빈 필터 배열로 적용
|
// 빈 필터 배열로 적용
|
||||||
table?.onFilterChange([]);
|
table?.onFilterChange([]);
|
||||||
|
|
||||||
|
// 🆕 그룹핑 해제
|
||||||
|
table?.onGroupSumChange?.(null);
|
||||||
|
|
||||||
// 콜백으로 빈 필터 정보 전달
|
// 콜백으로 빈 필터 정보 전달
|
||||||
onFiltersApplied?.([]);
|
onFiltersApplied?.([]);
|
||||||
|
|
||||||
|
|
@ -246,9 +279,7 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base sm:text-lg">
|
<DialogTitle className="text-base sm:text-lg">검색 필터 설정</DialogTitle>
|
||||||
검색 필터 설정
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="text-xs sm:text-sm">
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
|
검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
@ -256,17 +287,12 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
|
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
{/* 전체 선택/해제 */}
|
{/* 전체 선택/해제 */}
|
||||||
<div className="flex items-center justify-between rounded-lg border bg-muted/30 p-3">
|
<div className="bg-muted/30 flex items-center justify-between rounded-lg border p-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox checked={selectAll} onCheckedChange={(checked) => toggleSelectAll(checked as boolean)} />
|
||||||
checked={selectAll}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleSelectAll(checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium">전체 선택/해제</span>
|
<span className="text-sm font-medium">전체 선택/해제</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-muted-foreground text-xs">
|
||||||
{enabledCount} / {columnFilters.length}개
|
{enabledCount} / {columnFilters.length}개
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -277,30 +303,21 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
{columnFilters.map((filter) => (
|
{columnFilters.map((filter) => (
|
||||||
<div
|
<div
|
||||||
key={filter.columnName}
|
key={filter.columnName}
|
||||||
className="flex items-center gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
|
className="bg-background hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 transition-colors"
|
||||||
>
|
>
|
||||||
{/* 체크박스 */}
|
{/* 체크박스 */}
|
||||||
<Checkbox
|
<Checkbox checked={filter.enabled} onCheckedChange={() => toggleFilter(filter.columnName)} />
|
||||||
checked={filter.enabled}
|
|
||||||
onCheckedChange={() => toggleFilter(filter.columnName)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 컬럼 정보 */}
|
{/* 컬럼 정보 */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium">
|
<div className="truncate text-sm font-medium">{filter.columnLabel}</div>
|
||||||
{filter.columnLabel}
|
<div className="text-muted-foreground truncate text-xs">{filter.columnName}</div>
|
||||||
</div>
|
|
||||||
<div className="truncate text-xs text-muted-foreground">
|
|
||||||
{filter.columnName}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 타입 선택 */}
|
{/* 필터 타입 선택 */}
|
||||||
<Select
|
<Select
|
||||||
value={filter.filterType}
|
value={filter.filterType}
|
||||||
onValueChange={(val: any) =>
|
onValueChange={(val: any) => updateFilterType(filter.columnName, val)}
|
||||||
updateFilterType(filter.columnName, val)
|
|
||||||
}
|
|
||||||
disabled={!filter.enabled}
|
disabled={!filter.enabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[110px] text-xs sm:h-9 sm:text-sm">
|
<SelectTrigger className="h-8 w-[110px] text-xs sm:h-9 sm:text-sm">
|
||||||
|
|
@ -321,11 +338,7 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newWidth = parseInt(e.target.value) || 200;
|
const newWidth = parseInt(e.target.value) || 200;
|
||||||
setColumnFilters((prev) =>
|
setColumnFilters((prev) =>
|
||||||
prev.map((f) =>
|
prev.map((f) => (f.columnName === filter.columnName ? { ...f, width: newWidth } : f)),
|
||||||
f.columnName === filter.columnName
|
|
||||||
? { ...f, width: newWidth }
|
|
||||||
: f
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
disabled={!filter.enabled}
|
disabled={!filter.enabled}
|
||||||
|
|
@ -334,31 +347,56 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
min={50}
|
min={50}
|
||||||
max={500}
|
max={500}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">px</span>
|
<span className="text-muted-foreground text-xs">px</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* 🆕 그룹별 합산 설정 */}
|
||||||
|
<div className="bg-muted/30 space-y-3 rounded-lg border p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Layers className="text-muted-foreground h-4 w-4" />
|
||||||
|
<Label htmlFor="group-sum-toggle" className="cursor-pointer text-sm font-medium">
|
||||||
|
그룹별 합산
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Switch id="group-sum-toggle" checked={groupSumEnabled} onCheckedChange={setGroupSumEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{groupSumEnabled && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-muted-foreground text-xs">
|
||||||
|
그룹 기준 컬럼 (숫자 컬럼은 자동으로 합산됩니다)
|
||||||
|
</Label>
|
||||||
|
<Select value={groupByColumn} onValueChange={setGroupByColumn}>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="그룹 기준 컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{columnFilters.map((filter) => (
|
||||||
|
<SelectItem key={filter.columnName} value={filter.columnName}>
|
||||||
|
{filter.columnLabel}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
<div className="rounded-lg bg-muted/50 p-3 text-center text-xs text-muted-foreground">
|
<div className="bg-muted/50 text-muted-foreground rounded-lg p-3 text-center text-xs">
|
||||||
검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요
|
검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
<Button
|
<Button variant="ghost" onClick={clearFilters} className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
variant="ghost"
|
|
||||||
onClick={clearFilters}
|
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
||||||
>
|
|
||||||
초기화
|
초기화
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" onClick={onClose} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||||
variant="outline"
|
|
||||||
onClick={onClose}
|
|
||||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
||||||
>
|
|
||||||
취소
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -373,4 +411,3 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} 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, GroupSumConfig } from "@/types/table-options";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||||
|
|
@ -73,12 +73,69 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 엔티티 조인 컬럼명 변환 헬퍼
|
||||||
|
// "테이블명.컬럼명" 형식을 "원본컬럼_조인컬럼명" 형식으로 변환하여 데이터 접근
|
||||||
|
const getEntityJoinValue = useCallback(
|
||||||
|
(item: any, columnName: string, entityColumnMap?: Record<string, string>): any => {
|
||||||
|
// 직접 매칭 시도
|
||||||
|
if (item[columnName] !== undefined) {
|
||||||
|
return item[columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// "테이블명.컬럼명" 형식인 경우 (예: item_info.item_name)
|
||||||
|
if (columnName.includes(".")) {
|
||||||
|
const [tableName, fieldName] = columnName.split(".");
|
||||||
|
|
||||||
|
// 🔍 엔티티 조인 컬럼 값 추출
|
||||||
|
// 예: item_info.item_name, item_info.standard, item_info.unit
|
||||||
|
|
||||||
|
// 1️⃣ 소스 컬럼 추론 (item_info → item_code, warehouse_info → warehouse_id 등)
|
||||||
|
const inferredSourceColumn = tableName.replace("_info", "_code").replace("_mng", "_id");
|
||||||
|
|
||||||
|
// 2️⃣ 정확한 키 매핑 시도: 소스컬럼_필드명
|
||||||
|
// 예: item_code_item_name, item_code_standard, item_code_unit
|
||||||
|
const exactKey = `${inferredSourceColumn}_${fieldName}`;
|
||||||
|
if (item[exactKey] !== undefined) {
|
||||||
|
return item[exactKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ 별칭 패턴: 소스컬럼_name (기본 표시 컬럼용)
|
||||||
|
// 예: item_code_name (item_name의 별칭)
|
||||||
|
if (fieldName === "item_name" || fieldName === "name") {
|
||||||
|
const aliasKey = `${inferredSourceColumn}_name`;
|
||||||
|
if (item[aliasKey] !== undefined) {
|
||||||
|
return item[aliasKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4️⃣ entityColumnMap에서 매핑 찾기 (화면 설정에서 지정된 경우)
|
||||||
|
if (entityColumnMap && entityColumnMap[tableName]) {
|
||||||
|
const sourceColumn = entityColumnMap[tableName];
|
||||||
|
const joinedColumnName = `${sourceColumn}_${fieldName}`;
|
||||||
|
if (item[joinedColumnName] !== undefined) {
|
||||||
|
return item[joinedColumnName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5️⃣ 테이블명_컬럼명 형식으로 시도
|
||||||
|
const underscoreKey = `${tableName}_${fieldName}`;
|
||||||
|
if (item[underscoreKey] !== undefined) {
|
||||||
|
return item[underscoreKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// TableOptions Context
|
// TableOptions Context
|
||||||
const { registerTable, unregisterTable } = useTableOptions();
|
const { registerTable, unregisterTable } = useTableOptions();
|
||||||
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 [leftColumnOrder, setLeftColumnOrder] = useState<string[]>([]); // 🔧 컬럼 순서
|
||||||
|
const [leftGroupSumConfig, setLeftGroupSumConfig] = useState<GroupSumConfig | null>(null); // 🆕 그룹별 합산 설정
|
||||||
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[]>([]);
|
||||||
|
|
@ -125,6 +182,88 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [leftWidth, setLeftWidth] = useState(splitRatio);
|
const [leftWidth, setLeftWidth] = useState(splitRatio);
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 🆕 그룹별 합산된 데이터 계산
|
||||||
|
const summedLeftData = useMemo(() => {
|
||||||
|
console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig);
|
||||||
|
|
||||||
|
// 그룹핑이 비활성화되었거나 그룹 기준 컬럼이 없으면 원본 데이터 반환
|
||||||
|
if (!leftGroupSumConfig?.enabled || !leftGroupSumConfig?.groupByColumn) {
|
||||||
|
console.log("🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환");
|
||||||
|
return leftData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupByColumn = leftGroupSumConfig.groupByColumn;
|
||||||
|
const groupMap = new Map<string, any>();
|
||||||
|
|
||||||
|
// 조인 컬럼인지 확인하고 실제 키 추론
|
||||||
|
const getActualKey = (columnName: string, item: any): string => {
|
||||||
|
if (columnName.includes(".")) {
|
||||||
|
const [refTable, fieldName] = columnName.split(".");
|
||||||
|
const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
|
||||||
|
const exactKey = `${inferredSourceColumn}_${fieldName}`;
|
||||||
|
console.log("🔍 [그룹합산] 조인 컬럼 키 변환:", { columnName, exactKey, hasKey: item[exactKey] !== undefined });
|
||||||
|
if (item[exactKey] !== undefined) return exactKey;
|
||||||
|
if (fieldName === "item_name" || fieldName === "name") {
|
||||||
|
const aliasKey = `${inferredSourceColumn}_name`;
|
||||||
|
if (item[aliasKey] !== undefined) return aliasKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columnName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 숫자 타입인지 확인하는 함수
|
||||||
|
const isNumericValue = (value: any): boolean => {
|
||||||
|
if (value === null || value === undefined || value === "") return false;
|
||||||
|
const num = parseFloat(String(value));
|
||||||
|
return !isNaN(num) && isFinite(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 그룹핑 수행
|
||||||
|
leftData.forEach((item) => {
|
||||||
|
const actualKey = getActualKey(groupByColumn, item);
|
||||||
|
const groupValue = String(item[actualKey] || item[groupByColumn] || "");
|
||||||
|
|
||||||
|
// 원본 ID 추출 (id, ID, 또는 첫 번째 값)
|
||||||
|
const originalId = item.id || item.ID || Object.values(item)[0];
|
||||||
|
|
||||||
|
if (!groupMap.has(groupValue)) {
|
||||||
|
// 첫 번째 항목을 기준으로 초기화 + 원본 ID 배열 + 원본 데이터 배열
|
||||||
|
groupMap.set(groupValue, {
|
||||||
|
...item,
|
||||||
|
_groupCount: 1,
|
||||||
|
_originalIds: [originalId],
|
||||||
|
_originalItems: [item], // 🆕 원본 데이터 전체 저장
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existing = groupMap.get(groupValue);
|
||||||
|
existing._groupCount += 1;
|
||||||
|
existing._originalIds.push(originalId);
|
||||||
|
existing._originalItems.push(item); // 🆕 원본 데이터 추가
|
||||||
|
|
||||||
|
// 모든 키에 대해 숫자면 합산
|
||||||
|
Object.keys(item).forEach((key) => {
|
||||||
|
const value = item[key];
|
||||||
|
if (isNumericValue(value) && key !== groupByColumn && !key.endsWith("_id") && !key.includes("code")) {
|
||||||
|
const numValue = parseFloat(String(value));
|
||||||
|
const existingValue = parseFloat(String(existing[key] || 0));
|
||||||
|
existing[key] = existingValue + numValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
groupMap.set(groupValue, existing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Array.from(groupMap.values());
|
||||||
|
console.log("🔗 [분할패널] 그룹별 합산 결과:", {
|
||||||
|
원본개수: leftData.length,
|
||||||
|
그룹개수: result.length,
|
||||||
|
그룹기준: groupByColumn,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [leftData, leftGroupSumConfig]);
|
||||||
|
|
||||||
// 컴포넌트 스타일
|
// 컴포넌트 스타일
|
||||||
// height 처리: 이미 px 단위면 그대로, 숫자면 px 추가
|
// height 처리: 이미 px 단위면 그대로, 숫자면 px 추가
|
||||||
const getHeightValue = () => {
|
const getHeightValue = () => {
|
||||||
|
|
@ -433,14 +572,77 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 🎯 필터 조건을 API에 전달 (entityJoinApi 사용)
|
// 🎯 필터 조건을 API에 전달 (entityJoinApi 사용)
|
||||||
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
|
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
|
||||||
|
|
||||||
|
// 🆕 "테이블명.컬럼명" 형식의 조인 컬럼들을 additionalJoinColumns로 변환
|
||||||
|
const configuredColumns = componentConfig.leftPanel?.columns || [];
|
||||||
|
const additionalJoinColumns: Array<{
|
||||||
|
sourceTable: string;
|
||||||
|
sourceColumn: string;
|
||||||
|
referenceTable: string;
|
||||||
|
joinAlias: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// 소스 컬럼 매핑 (item_info → item_code, warehouse_info → warehouse_id 등)
|
||||||
|
const sourceColumnMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
configuredColumns.forEach((col: any) => {
|
||||||
|
const colName = typeof col === "string" ? col : col.name || col.columnName;
|
||||||
|
if (colName && colName.includes(".")) {
|
||||||
|
const [refTable, refColumn] = colName.split(".");
|
||||||
|
// 소스 컬럼 추론 (item_info → item_code)
|
||||||
|
const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
|
||||||
|
|
||||||
|
// 이미 추가된 조인인지 확인 (동일 테이블, 동일 소스컬럼)
|
||||||
|
const existingJoin = additionalJoinColumns.find(
|
||||||
|
(j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingJoin) {
|
||||||
|
// 새로운 조인 추가 (첫 번째 컬럼)
|
||||||
|
additionalJoinColumns.push({
|
||||||
|
sourceTable: leftTableName,
|
||||||
|
sourceColumn: inferredSourceColumn,
|
||||||
|
referenceTable: refTable,
|
||||||
|
joinAlias: `${inferredSourceColumn}_${refColumn}`,
|
||||||
|
});
|
||||||
|
sourceColumnMap[refTable] = inferredSourceColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 추가 컬럼도 별도로 요청 (item_code_standard, item_code_unit 등)
|
||||||
|
// 단, 첫 번째 컬럼과 다른 경우만
|
||||||
|
const existingAliases = additionalJoinColumns
|
||||||
|
.filter((j) => j.referenceTable === refTable)
|
||||||
|
.map((j) => j.joinAlias);
|
||||||
|
const newAlias = `${sourceColumnMap[refTable] || inferredSourceColumn}_${refColumn}`;
|
||||||
|
|
||||||
|
if (!existingAliases.includes(newAlias)) {
|
||||||
|
additionalJoinColumns.push({
|
||||||
|
sourceTable: leftTableName,
|
||||||
|
sourceColumn: sourceColumnMap[refTable] || inferredSourceColumn,
|
||||||
|
referenceTable: refTable,
|
||||||
|
joinAlias: newAlias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🔗 [분할패널] additionalJoinColumns:", additionalJoinColumns);
|
||||||
|
console.log("🔗 [분할패널] configuredColumns:", configuredColumns);
|
||||||
|
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(leftTableName, {
|
const result = await entityJoinApi.getTableDataWithJoins(leftTableName, {
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 100,
|
size: 100,
|
||||||
search: filters, // 필터 조건 전달
|
search: filters, // 필터 조건 전달
|
||||||
enableEntityJoin: true, // 엔티티 조인 활성화
|
enableEntityJoin: true, // 엔티티 조인 활성화
|
||||||
dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달
|
dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달
|
||||||
|
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 🆕 추가 조인 컬럼
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔍 디버깅: API 응답 데이터의 키 확인
|
||||||
|
if (result.data && result.data.length > 0) {
|
||||||
|
console.log("🔗 [분할패널] API 응답 첫 번째 데이터 키:", Object.keys(result.data[0]));
|
||||||
|
console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
||||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||||
if (leftColumn && result.data.length > 0) {
|
if (leftColumn && result.data.length > 0) {
|
||||||
|
|
@ -466,6 +668,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
componentConfig.leftPanel?.tableName,
|
componentConfig.leftPanel?.tableName,
|
||||||
|
componentConfig.leftPanel?.columns,
|
||||||
|
componentConfig.leftPanel?.dataFilter,
|
||||||
componentConfig.rightPanel?.relation?.leftColumn,
|
componentConfig.rightPanel?.relation?.leftColumn,
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
toast,
|
toast,
|
||||||
|
|
@ -502,6 +706,68 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const keys = componentConfig.rightPanel?.relation?.keys;
|
const keys = componentConfig.rightPanel?.relation?.keys;
|
||||||
const leftTable = componentConfig.leftPanel?.tableName;
|
const leftTable = componentConfig.leftPanel?.tableName;
|
||||||
|
|
||||||
|
// 🆕 그룹 합산된 항목인 경우: 원본 데이터들로 우측 패널 표시
|
||||||
|
if (leftItem._originalItems && leftItem._originalItems.length > 0) {
|
||||||
|
console.log("🔗 [분할패널] 그룹 합산 항목 - 원본 개수:", leftItem._originalItems.length);
|
||||||
|
|
||||||
|
// 정렬 기준 컬럼 (복합키의 leftColumn들)
|
||||||
|
const sortColumns = keys?.map((k: any) => k.leftColumn).filter(Boolean) || [];
|
||||||
|
console.log("🔗 [분할패널] 정렬 기준 컬럼:", sortColumns);
|
||||||
|
|
||||||
|
// 정렬 함수
|
||||||
|
const sortByKeys = (data: any[]) => {
|
||||||
|
if (sortColumns.length === 0) return data;
|
||||||
|
return [...data].sort((a, b) => {
|
||||||
|
for (const col of sortColumns) {
|
||||||
|
const aVal = String(a[col] || "");
|
||||||
|
const bVal = String(b[col] || "");
|
||||||
|
const cmp = aVal.localeCompare(bVal, "ko-KR");
|
||||||
|
if (cmp !== 0) return cmp;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 원본 데이터를 그대로 우측 패널에 표시 (이력 테이블과 동일 테이블인 경우)
|
||||||
|
if (leftTable === rightTableName) {
|
||||||
|
const sortedData = sortByKeys(leftItem._originalItems);
|
||||||
|
console.log("🔗 [분할패널] 동일 테이블 - 정렬된 원본 데이터:", sortedData.length);
|
||||||
|
setRightData(sortedData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다른 테이블인 경우: 원본 ID들로 조회
|
||||||
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
|
const allResults: any[] = [];
|
||||||
|
|
||||||
|
// 각 원본 항목에 대해 조회
|
||||||
|
for (const originalItem of leftItem._originalItems) {
|
||||||
|
const searchConditions: Record<string, any> = {};
|
||||||
|
keys?.forEach((key: any) => {
|
||||||
|
if (key.leftColumn && key.rightColumn && originalItem[key.leftColumn] !== undefined) {
|
||||||
|
searchConditions[key.rightColumn] = originalItem[key.leftColumn];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(searchConditions).length > 0) {
|
||||||
|
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
||||||
|
search: searchConditions,
|
||||||
|
enableEntityJoin: true,
|
||||||
|
size: 1000,
|
||||||
|
});
|
||||||
|
if (result.data) {
|
||||||
|
allResults.push(...result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬 적용
|
||||||
|
const sortedResults = sortByKeys(allResults);
|
||||||
|
console.log("🔗 [분할패널] 그룹 합산 - 우측 패널 정렬된 데이터:", sortedResults.length);
|
||||||
|
setRightData(sortedResults);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 복합키 지원
|
// 🆕 복합키 지원
|
||||||
if (keys && keys.length > 0 && leftTable) {
|
if (keys && keys.length > 0 && leftTable) {
|
||||||
// 복합키: 여러 조건으로 필터링
|
// 복합키: 여러 조건으로 필터링
|
||||||
|
|
@ -642,7 +908,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const uniqueValues = new Set<string>();
|
const uniqueValues = new Set<string>();
|
||||||
|
|
||||||
leftData.forEach((item) => {
|
leftData.forEach((item) => {
|
||||||
const value = item[columnName];
|
// 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard)
|
||||||
|
let value: any;
|
||||||
|
|
||||||
|
if (columnName.includes(".")) {
|
||||||
|
// 조인 컬럼: getEntityJoinValue와 동일한 로직 적용
|
||||||
|
const [refTable, fieldName] = columnName.split(".");
|
||||||
|
const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
|
||||||
|
|
||||||
|
// 정확한 키로 먼저 시도
|
||||||
|
const exactKey = `${inferredSourceColumn}_${fieldName}`;
|
||||||
|
value = item[exactKey];
|
||||||
|
|
||||||
|
// 기본 별칭 패턴 시도 (item_code_name)
|
||||||
|
if (value === undefined && (fieldName === "item_name" || fieldName === "name")) {
|
||||||
|
const aliasKey = `${inferredSourceColumn}_name`;
|
||||||
|
value = item[aliasKey];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 일반 컬럼
|
||||||
|
value = item[columnName];
|
||||||
|
}
|
||||||
|
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
// _name 필드 우선 사용 (category/entity type)
|
// _name 필드 우선 사용 (category/entity type)
|
||||||
const displayValue = item[`${columnName}_name`] || value;
|
const displayValue = item[`${columnName}_name`] || value;
|
||||||
|
|
@ -666,6 +953,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const leftTableId = `split-panel-left-${component.id}`;
|
const leftTableId = `split-panel-left-${component.id}`;
|
||||||
// 🔧 화면에 표시되는 컬럼 사용 (columns 속성)
|
// 🔧 화면에 표시되는 컬럼 사용 (columns 속성)
|
||||||
const configuredColumns = componentConfig.leftPanel?.columns || [];
|
const configuredColumns = componentConfig.leftPanel?.columns || [];
|
||||||
|
|
||||||
|
// 🆕 설정에서 지정한 라벨 맵 생성
|
||||||
|
const configuredLabels: Record<string, string> = {};
|
||||||
|
configuredColumns.forEach((col: any) => {
|
||||||
|
if (typeof col === "object" && col.name && col.label) {
|
||||||
|
configuredLabels[col.name] = col.label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const displayColumns = configuredColumns
|
const displayColumns = configuredColumns
|
||||||
.map((col: any) => {
|
.map((col: any) => {
|
||||||
if (typeof col === "string") return col;
|
if (typeof col === "string") return col;
|
||||||
|
|
@ -683,7 +979,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
tableName: leftTableName,
|
tableName: leftTableName,
|
||||||
columns: displayColumns.map((col: string) => ({
|
columns: displayColumns.map((col: string) => ({
|
||||||
columnName: col,
|
columnName: col,
|
||||||
columnLabel: leftColumnLabels[col] || col,
|
// 🆕 우선순위: 1) 설정에서 지정한 라벨 2) DB 라벨 3) 컬럼명
|
||||||
|
columnLabel: configuredLabels[col] || leftColumnLabels[col] || col,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
visible: true,
|
visible: true,
|
||||||
width: 150,
|
width: 150,
|
||||||
|
|
@ -695,6 +992,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
onColumnVisibilityChange: setLeftColumnVisibility,
|
onColumnVisibilityChange: setLeftColumnVisibility,
|
||||||
onColumnOrderChange: setLeftColumnOrder, // 🔧 컬럼 순서 변경 콜백 추가
|
onColumnOrderChange: setLeftColumnOrder, // 🔧 컬럼 순서 변경 콜백 추가
|
||||||
getColumnUniqueValues: getLeftColumnUniqueValues, // 🔧 고유값 가져오기 함수 추가
|
getColumnUniqueValues: getLeftColumnUniqueValues, // 🔧 고유값 가져오기 함수 추가
|
||||||
|
onGroupSumChange: setLeftGroupSumConfig, // 🆕 그룹별 합산 설정 콜백
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => unregisterTable(leftTableId);
|
return () => unregisterTable(leftTableId);
|
||||||
|
|
@ -1651,16 +1949,25 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
|
// 🆕 그룹별 합산된 데이터 사용
|
||||||
|
const dataSource = summedLeftData;
|
||||||
|
console.log(
|
||||||
|
"🔍 [테이블모드 렌더링] dataSource 개수:",
|
||||||
|
dataSource.length,
|
||||||
|
"leftGroupSumConfig:",
|
||||||
|
leftGroupSumConfig,
|
||||||
|
);
|
||||||
|
|
||||||
// 🔧 로컬 검색 필터 적용
|
// 🔧 로컬 검색 필터 적용
|
||||||
const filteredData = leftSearchQuery
|
const filteredData = leftSearchQuery
|
||||||
? leftData.filter((item) => {
|
? dataSource.filter((item) => {
|
||||||
const searchLower = leftSearchQuery.toLowerCase();
|
const searchLower = leftSearchQuery.toLowerCase();
|
||||||
return Object.entries(item).some(([key, value]) => {
|
return Object.entries(item).some(([key, value]) => {
|
||||||
if (value === null || value === undefined) return false;
|
if (value === null || value === undefined) return false;
|
||||||
return String(value).toLowerCase().includes(searchLower);
|
return String(value).toLowerCase().includes(searchLower);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
: leftData;
|
: dataSource;
|
||||||
|
|
||||||
// 🔧 가시성 처리된 컬럼 사용
|
// 🔧 가시성 처리된 컬럼 사용
|
||||||
const columnsToShow =
|
const columnsToShow =
|
||||||
|
|
@ -1737,7 +2044,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
{formatCellValue(
|
{formatCellValue(
|
||||||
col.name,
|
col.name,
|
||||||
item[col.name],
|
getEntityJoinValue(item, col.name),
|
||||||
leftCategoryMappings,
|
leftCategoryMappings,
|
||||||
col.format,
|
col.format,
|
||||||
)}
|
)}
|
||||||
|
|
@ -1796,7 +2103,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(col.name, item[col.name], leftCategoryMappings, col.format)}
|
{formatCellValue(
|
||||||
|
col.name,
|
||||||
|
getEntityJoinValue(item, col.name),
|
||||||
|
leftCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1851,16 +2163,25 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
|
// 🆕 그룹별 합산된 데이터 사용
|
||||||
|
const dataToDisplay = summedLeftData;
|
||||||
|
console.log(
|
||||||
|
"🔍 [렌더링] dataToDisplay 개수:",
|
||||||
|
dataToDisplay.length,
|
||||||
|
"leftGroupSumConfig:",
|
||||||
|
leftGroupSumConfig,
|
||||||
|
);
|
||||||
|
|
||||||
// 검색 필터링 (클라이언트 사이드)
|
// 검색 필터링 (클라이언트 사이드)
|
||||||
const filteredLeftData = leftSearchQuery
|
const filteredLeftData = leftSearchQuery
|
||||||
? leftData.filter((item) => {
|
? dataToDisplay.filter((item) => {
|
||||||
const searchLower = leftSearchQuery.toLowerCase();
|
const searchLower = leftSearchQuery.toLowerCase();
|
||||||
return Object.entries(item).some(([key, value]) => {
|
return Object.entries(item).some(([key, value]) => {
|
||||||
if (value === null || value === undefined) return false;
|
if (value === null || value === undefined) return false;
|
||||||
return String(value).toLowerCase().includes(searchLower);
|
return String(value).toLowerCase().includes(searchLower);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
: leftData;
|
: dataToDisplay;
|
||||||
|
|
||||||
// 재귀 렌더링 함수
|
// 재귀 렌더링 함수
|
||||||
const renderTreeItem = (item: any, index: number): React.ReactNode => {
|
const renderTreeItem = (item: any, index: number): React.ReactNode => {
|
||||||
|
|
@ -2108,23 +2429,53 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
if (isTableMode) {
|
if (isTableMode) {
|
||||||
// 테이블 모드 렌더링
|
// 테이블 모드 렌더링
|
||||||
const displayColumns = componentConfig.rightPanel?.columns || [];
|
const displayColumns = componentConfig.rightPanel?.columns || [];
|
||||||
const columnsToShow =
|
|
||||||
displayColumns.length > 0
|
// 🆕 그룹 합산 모드일 때: 복합키 컬럼을 우선 표시
|
||||||
? displayColumns.map((col) => ({
|
const relationKeys = componentConfig.rightPanel?.relation?.keys || [];
|
||||||
...col,
|
const keyColumns = relationKeys.map((k: any) => k.leftColumn).filter(Boolean);
|
||||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
const isGroupedMode = selectedLeftItem?._originalItems?.length > 0;
|
||||||
format: col.format, // 🆕 포맷 설정 유지
|
|
||||||
}))
|
let columnsToShow: any[] = [];
|
||||||
: Object.keys(filteredData[0] || {})
|
|
||||||
.filter((key) => shouldShowField(key))
|
if (displayColumns.length > 0) {
|
||||||
.slice(0, 5)
|
// 설정된 컬럼 사용
|
||||||
.map((key) => ({
|
columnsToShow = displayColumns.map((col) => ({
|
||||||
name: key,
|
...col,
|
||||||
label: rightColumnLabels[key] || key,
|
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||||
width: 150,
|
format: col.format,
|
||||||
align: "left" as const,
|
}));
|
||||||
format: undefined, // 🆕 기본값
|
|
||||||
}));
|
// 🆕 그룹 합산 모드이고, 키 컬럼이 표시 목록에 없으면 맨 앞에 추가
|
||||||
|
if (isGroupedMode && keyColumns.length > 0) {
|
||||||
|
const existingColNames = columnsToShow.map((c) => c.name);
|
||||||
|
const missingKeyColumns = keyColumns.filter((k: string) => !existingColNames.includes(k));
|
||||||
|
|
||||||
|
if (missingKeyColumns.length > 0) {
|
||||||
|
const keyColsToAdd = missingKeyColumns.map((colName: string) => ({
|
||||||
|
name: colName,
|
||||||
|
label: rightColumnLabels[colName] || colName,
|
||||||
|
width: 120,
|
||||||
|
align: "left" as const,
|
||||||
|
format: undefined,
|
||||||
|
_isKeyColumn: true, // 구분용 플래그
|
||||||
|
}));
|
||||||
|
columnsToShow = [...keyColsToAdd, ...columnsToShow];
|
||||||
|
console.log("🔗 [우측패널] 그룹모드 - 키 컬럼 추가:", missingKeyColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 기본 컬럼 자동 생성
|
||||||
|
columnsToShow = Object.keys(filteredData[0] || {})
|
||||||
|
.filter((key) => shouldShowField(key))
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((key) => ({
|
||||||
|
name: key,
|
||||||
|
label: rightColumnLabels[key] || key,
|
||||||
|
width: 150,
|
||||||
|
align: "left" as const,
|
||||||
|
format: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
@ -2150,11 +2501,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{col.label}
|
{col.label}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{!isDesignMode && (
|
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */}
|
||||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">
|
{!isDesignMode &&
|
||||||
작업
|
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||||
</th>
|
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||||
)}
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">
|
||||||
|
작업
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 bg-white">
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
|
@ -2169,43 +2523,51 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(col.name, item[col.name], rightCategoryMappings, col.format)}
|
{formatCellValue(
|
||||||
|
col.name,
|
||||||
|
getEntityJoinValue(item, col.name),
|
||||||
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{!isDesignMode && (
|
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
||||||
<td className="px-3 py-2 text-right text-sm whitespace-nowrap">
|
{!isDesignMode &&
|
||||||
<div className="flex justify-end gap-1">
|
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||||
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||||
<Button
|
<td className="px-3 py-2 text-right text-sm whitespace-nowrap">
|
||||||
variant={
|
<div className="flex justify-end gap-1">
|
||||||
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
||||||
}
|
<Button
|
||||||
size="sm"
|
variant={
|
||||||
onClick={(e) => {
|
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
||||||
e.stopPropagation();
|
}
|
||||||
handleEditClick("right", item);
|
size="sm"
|
||||||
}}
|
onClick={(e) => {
|
||||||
className="h-7"
|
e.stopPropagation();
|
||||||
>
|
handleEditClick("right", item);
|
||||||
<Pencil className="mr-1 h-3 w-3" />
|
}}
|
||||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
className="h-7"
|
||||||
</Button>
|
>
|
||||||
)}
|
<Pencil className="mr-1 h-3 w-3" />
|
||||||
{(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||||
<button
|
</Button>
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.stopPropagation();
|
{(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
||||||
handleDeleteClick("right", item);
|
<button
|
||||||
}}
|
onClick={(e) => {
|
||||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
e.stopPropagation();
|
||||||
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
handleDeleteClick("right", item);
|
||||||
>
|
}}
|
||||||
<Trash2 className="h-4 w-4 text-red-600" />
|
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||||
</button>
|
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
||||||
)}
|
>
|
||||||
</div>
|
<Trash2 className="h-4 w-4 text-red-600" />
|
||||||
</td>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -2240,78 +2602,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
firstValues = rightColumns
|
firstValues = rightColumns
|
||||||
.slice(0, summaryCount)
|
.slice(0, summaryCount)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
// 🆕 엔티티 조인 컬럼 처리 (예: item_info.item_number → item_number 또는 item_id_name)
|
// 🆕 엔티티 조인 컬럼 처리 (getEntityJoinValue 사용)
|
||||||
let value = item[col.name];
|
const value = getEntityJoinValue(item, col.name);
|
||||||
if (value === undefined && col.name.includes(".")) {
|
|
||||||
const columnName = col.name.split(".").pop();
|
|
||||||
// 1차: 컬럼명 그대로 (예: item_number)
|
|
||||||
value = item[columnName || ""];
|
|
||||||
// 2차: item_info.item_number → item_id_name 또는 item_id_item_number 형식 확인
|
|
||||||
if (value === undefined) {
|
|
||||||
const parts = col.name.split(".");
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const refTable = parts[0]; // item_info
|
|
||||||
const refColumn = parts[1]; // item_number 또는 item_name
|
|
||||||
// FK 컬럼명 추론: item_info → item_id
|
|
||||||
const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id";
|
|
||||||
|
|
||||||
// 백엔드에서 반환하는 별칭 패턴:
|
|
||||||
// 1) item_id_name (기본 referenceColumn)
|
|
||||||
// 2) item_id_item_name (추가 컬럼)
|
|
||||||
if (
|
|
||||||
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" ||
|
|
||||||
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code"
|
|
||||||
) {
|
|
||||||
// 기본 참조 컬럼 (item_number, customer_code 등)
|
|
||||||
const aliasKey = fkColumn + "_name";
|
|
||||||
value = item[aliasKey];
|
|
||||||
} else {
|
|
||||||
// 추가 컬럼 (item_name, customer_name 등)
|
|
||||||
const aliasKey = `${fkColumn}_${refColumn}`;
|
|
||||||
value = item[aliasKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [col.name, value, col.label] as [string, any, string];
|
return [col.name, value, col.label] as [string, any, string];
|
||||||
})
|
})
|
||||||
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
|
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
|
||||||
|
|
||||||
allValues = rightColumns
|
allValues = rightColumns
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
// 🆕 엔티티 조인 컬럼 처리
|
// 🆕 엔티티 조인 컬럼 처리 (getEntityJoinValue 사용)
|
||||||
let value = item[col.name];
|
const value = getEntityJoinValue(item, col.name);
|
||||||
if (value === undefined && col.name.includes(".")) {
|
|
||||||
const columnName = col.name.split(".").pop();
|
|
||||||
// 1차: 컬럼명 그대로
|
|
||||||
value = item[columnName || ""];
|
|
||||||
// 2차: {fk_column}_name 또는 {fk_column}_{ref_column} 형식 확인
|
|
||||||
if (value === undefined) {
|
|
||||||
const parts = col.name.split(".");
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const refTable = parts[0]; // item_info
|
|
||||||
const refColumn = parts[1]; // item_number 또는 item_name
|
|
||||||
// FK 컬럼명 추론: item_info → item_id
|
|
||||||
const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id";
|
|
||||||
|
|
||||||
// 백엔드에서 반환하는 별칭 패턴:
|
|
||||||
// 1) item_id_name (기본 referenceColumn)
|
|
||||||
// 2) item_id_item_name (추가 컬럼)
|
|
||||||
if (
|
|
||||||
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" ||
|
|
||||||
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code"
|
|
||||||
) {
|
|
||||||
// 기본 참조 컬럼
|
|
||||||
const aliasKey = fkColumn + "_name";
|
|
||||||
value = item[aliasKey];
|
|
||||||
} else {
|
|
||||||
// 추가 컬럼
|
|
||||||
const aliasKey = `${fkColumn}_${refColumn}`;
|
|
||||||
value = item[aliasKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [col.name, value, col.label] as [string, any, string];
|
return [col.name, value, col.label] as [string, any, string];
|
||||||
})
|
})
|
||||||
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
|
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,21 +7,21 @@
|
||||||
*/
|
*/
|
||||||
export interface TableFilter {
|
export interface TableFilter {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
operator:
|
operator: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "notEquals";
|
||||||
| "equals"
|
|
||||||
| "contains"
|
|
||||||
| "startsWith"
|
|
||||||
| "endsWith"
|
|
||||||
| "gt"
|
|
||||||
| "lt"
|
|
||||||
| "gte"
|
|
||||||
| "lte"
|
|
||||||
| "notEquals";
|
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
filterType?: "text" | "number" | "date" | "select"; // 필터 입력 타입
|
filterType?: "text" | "number" | "date" | "select"; // 필터 입력 타입
|
||||||
width?: number; // 필터 입력 필드 너비 (px)
|
width?: number; // 필터 입력 필드 너비 (px)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 그룹별 합산 설정
|
||||||
|
*/
|
||||||
|
export interface GroupSumConfig {
|
||||||
|
enabled: boolean; // 그룹핑 활성화 여부
|
||||||
|
groupByColumn: string; // 그룹 기준 컬럼
|
||||||
|
groupByColumnLabel?: string; // 그룹 기준 컬럼 라벨 (UI 표시용)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컬럼 표시 설정
|
* 컬럼 표시 설정
|
||||||
*/
|
*/
|
||||||
|
|
@ -60,6 +60,7 @@ export interface TableRegistration {
|
||||||
onFilterChange: (filters: TableFilter[]) => void;
|
onFilterChange: (filters: TableFilter[]) => void;
|
||||||
onGroupChange: (groups: string[]) => void;
|
onGroupChange: (groups: string[]) => void;
|
||||||
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
|
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
|
||||||
|
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 🆕 그룹별 합산 설정 변경
|
||||||
|
|
||||||
// 데이터 조회 함수 (선택 타입 필터용)
|
// 데이터 조회 함수 (선택 타입 필터용)
|
||||||
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
|
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
|
||||||
|
|
@ -77,4 +78,3 @@ export interface TableOptionsContextValue {
|
||||||
selectedTableId: string | null;
|
selectedTableId: string | null;
|
||||||
setSelectedTableId: (tableId: string | null) => void;
|
setSelectedTableId: (tableId: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue