Compare commits
11 Commits
4b148ee823
...
d52d6c129b
| Author | SHA1 | Date |
|---|---|---|
|
|
d52d6c129b | |
|
|
d5b63d1c9b | |
|
|
e75889a127 | |
|
|
a757034d86 | |
|
|
28109eb63b | |
|
|
4c5e0330ef | |
|
|
ad7f350f00 | |
|
|
f01be49f6a | |
|
|
9d346a3d3a | |
|
|
de6c7a8008 | |
|
|
4aefb5be6a |
|
|
@ -26,6 +26,7 @@ export class EntityJoinController {
|
||||||
sortOrder = "asc",
|
sortOrder = "asc",
|
||||||
enableEntityJoin = true,
|
enableEntityJoin = true,
|
||||||
additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열)
|
additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열)
|
||||||
|
screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열)
|
||||||
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
|
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
|
||||||
...otherParams
|
...otherParams
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
@ -65,6 +66,24 @@ export class EntityJoinController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 화면별 엔티티 설정 처리
|
||||||
|
let parsedScreenEntityConfigs: Record<string, any> = {};
|
||||||
|
if (screenEntityConfigs) {
|
||||||
|
try {
|
||||||
|
parsedScreenEntityConfigs =
|
||||||
|
typeof screenEntityConfigs === "string"
|
||||||
|
? JSON.parse(screenEntityConfigs)
|
||||||
|
: screenEntityConfigs;
|
||||||
|
logger.info(
|
||||||
|
"화면별 엔티티 설정 파싱 완료:",
|
||||||
|
parsedScreenEntityConfigs
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("화면별 엔티티 설정 파싱 오류:", error);
|
||||||
|
parsedScreenEntityConfigs = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tableManagementService.getTableDataWithEntityJoins(
|
const result = await tableManagementService.getTableDataWithEntityJoins(
|
||||||
tableName,
|
tableName,
|
||||||
{
|
{
|
||||||
|
|
@ -79,6 +98,7 @@ export class EntityJoinController {
|
||||||
enableEntityJoin:
|
enableEntityJoin:
|
||||||
enableEntityJoin === "true" || enableEntityJoin === true,
|
enableEntityJoin === "true" || enableEntityJoin === true,
|
||||||
additionalJoinColumns: parsedAdditionalJoinColumns,
|
additionalJoinColumns: parsedAdditionalJoinColumns,
|
||||||
|
screenEntityConfigs: parsedScreenEntityConfigs,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -348,14 +368,16 @@ export class EntityJoinController {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 현재 display_column으로 사용 중인 컬럼 제외
|
// 현재 display_column으로 사용 중인 컬럼 제외
|
||||||
|
const currentDisplayColumn =
|
||||||
|
config.displayColumn || config.displayColumns[0];
|
||||||
const availableColumns = columns.filter(
|
const availableColumns = columns.filter(
|
||||||
(col) => col.columnName !== config.displayColumn
|
(col) => col.columnName !== currentDisplayColumn
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
joinConfig: config,
|
joinConfig: config,
|
||||||
tableName: config.referenceTable,
|
tableName: config.referenceTable,
|
||||||
currentDisplayColumn: config.displayColumn,
|
currentDisplayColumn: currentDisplayColumn,
|
||||||
availableColumns: availableColumns.map((col) => ({
|
availableColumns: availableColumns.map((col) => ({
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
columnLabel: col.displayName || col.columnName,
|
columnLabel: col.displayName || col.columnName,
|
||||||
|
|
@ -373,7 +395,8 @@ export class EntityJoinController {
|
||||||
return {
|
return {
|
||||||
joinConfig: config,
|
joinConfig: config,
|
||||||
tableName: config.referenceTable,
|
tableName: config.referenceTable,
|
||||||
currentDisplayColumn: config.displayColumn,
|
currentDisplayColumn:
|
||||||
|
config.displayColumn || config.displayColumns[0],
|
||||||
availableColumns: [],
|
availableColumns: [],
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,13 @@ const prisma = new PrismaClient();
|
||||||
export class EntityJoinService {
|
export class EntityJoinService {
|
||||||
/**
|
/**
|
||||||
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
||||||
|
* @param tableName 테이블명
|
||||||
|
* @param screenEntityConfigs 화면별 엔티티 설정 (선택사항)
|
||||||
*/
|
*/
|
||||||
async detectEntityJoins(tableName: string): Promise<EntityJoinConfig[]> {
|
async detectEntityJoins(
|
||||||
|
tableName: string,
|
||||||
|
screenEntityConfigs?: Record<string, any>
|
||||||
|
): Promise<EntityJoinConfig[]> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
||||||
|
|
||||||
|
|
@ -48,8 +53,40 @@ export class EntityJoinService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// display_column이 없으면 reference_column 사용
|
// 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용
|
||||||
const displayColumn = column.display_column || column.reference_column;
|
const screenConfig = screenEntityConfigs?.[column.column_name];
|
||||||
|
let displayColumns: string[] = [];
|
||||||
|
let separator = " - ";
|
||||||
|
|
||||||
|
if (screenConfig && screenConfig.displayColumns) {
|
||||||
|
// 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원)
|
||||||
|
displayColumns = screenConfig.displayColumns;
|
||||||
|
separator = screenConfig.separator || " - ";
|
||||||
|
console.log(`🎯 화면별 엔티티 설정 적용: ${column.column_name}`, {
|
||||||
|
displayColumns,
|
||||||
|
separator,
|
||||||
|
screenConfig,
|
||||||
|
});
|
||||||
|
} else if (column.display_column && column.display_column !== "none") {
|
||||||
|
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
|
||||||
|
displayColumns = [column.display_column];
|
||||||
|
} else {
|
||||||
|
// 조인 탭에서 보여줄 기본 표시 컬럼 설정
|
||||||
|
// dept_info 테이블의 경우 dept_name을 기본으로 사용
|
||||||
|
let defaultDisplayColumn = column.reference_column;
|
||||||
|
if (column.reference_table === "dept_info") {
|
||||||
|
defaultDisplayColumn = "dept_name";
|
||||||
|
} else if (column.reference_table === "company_info") {
|
||||||
|
defaultDisplayColumn = "company_name";
|
||||||
|
} else if (column.reference_table === "user_info") {
|
||||||
|
defaultDisplayColumn = "user_name";
|
||||||
|
}
|
||||||
|
|
||||||
|
displayColumns = [defaultDisplayColumn];
|
||||||
|
console.log(
|
||||||
|
`🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 별칭 컬럼명 생성 (writer -> writer_name)
|
// 별칭 컬럼명 생성 (writer -> writer_name)
|
||||||
const aliasColumn = `${column.column_name}_name`;
|
const aliasColumn = `${column.column_name}_name`;
|
||||||
|
|
@ -59,8 +96,10 @@ export class EntityJoinService {
|
||||||
sourceColumn: column.column_name,
|
sourceColumn: column.column_name,
|
||||||
referenceTable: column.reference_table,
|
referenceTable: column.reference_table,
|
||||||
referenceColumn: column.reference_column,
|
referenceColumn: column.reference_column,
|
||||||
displayColumn: displayColumn,
|
displayColumns: displayColumns,
|
||||||
|
displayColumn: displayColumns[0], // 하위 호환성
|
||||||
aliasColumn: aliasColumn,
|
aliasColumn: aliasColumn,
|
||||||
|
separator: separator,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 조인 설정 유효성 검증
|
// 조인 설정 유효성 검증
|
||||||
|
|
@ -90,8 +129,10 @@ export class EntityJoinService {
|
||||||
offset?: number
|
offset?: number
|
||||||
): { query: string; aliasMap: Map<string, string> } {
|
): { query: string; aliasMap: Map<string, string> } {
|
||||||
try {
|
try {
|
||||||
// 기본 SELECT 컬럼들
|
// 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지)
|
||||||
const baseColumns = selectColumns.map((col) => `main.${col}`).join(", ");
|
const baseColumns = selectColumns
|
||||||
|
.map((col) => `main.${col}::TEXT AS ${col}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
// Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리)
|
// Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리)
|
||||||
// 별칭 매핑 생성 (JOIN 절과 동일한 로직)
|
// 별칭 매핑 생성 (JOIN 절과 동일한 로직)
|
||||||
|
|
@ -130,10 +171,63 @@ export class EntityJoinService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const joinColumns = joinConfigs
|
const joinColumns = joinConfigs
|
||||||
.map(
|
.map((config) => {
|
||||||
(config) =>
|
const alias = aliasMap.get(config.referenceTable);
|
||||||
`COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}`
|
const displayColumns = config.displayColumns || [
|
||||||
)
|
config.displayColumn,
|
||||||
|
];
|
||||||
|
const separator = config.separator || " - ";
|
||||||
|
|
||||||
|
if (displayColumns.length === 1) {
|
||||||
|
// 단일 컬럼인 경우
|
||||||
|
const col = displayColumns[0];
|
||||||
|
const isJoinTableColumn = [
|
||||||
|
"dept_name",
|
||||||
|
"dept_code",
|
||||||
|
"master_user_id",
|
||||||
|
"location_name",
|
||||||
|
"parent_dept_code",
|
||||||
|
"master_sabun",
|
||||||
|
"location",
|
||||||
|
"data_type",
|
||||||
|
].includes(col);
|
||||||
|
|
||||||
|
if (isJoinTableColumn) {
|
||||||
|
return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||||
|
} else {
|
||||||
|
return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 여러 컬럼인 경우 CONCAT으로 연결
|
||||||
|
// 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리
|
||||||
|
const concatParts = displayColumns
|
||||||
|
.map((col) => {
|
||||||
|
// 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용)
|
||||||
|
// 현재는 dept_info 테이블의 컬럼들을 확인
|
||||||
|
const isJoinTableColumn = [
|
||||||
|
"dept_name",
|
||||||
|
"dept_code",
|
||||||
|
"master_user_id",
|
||||||
|
"location_name",
|
||||||
|
"parent_dept_code",
|
||||||
|
"master_sabun",
|
||||||
|
"location",
|
||||||
|
"data_type",
|
||||||
|
].includes(col);
|
||||||
|
|
||||||
|
if (isJoinTableColumn) {
|
||||||
|
// 조인 테이블 컬럼은 조인 별칭 사용
|
||||||
|
return `COALESCE(${alias}.${col}::TEXT, '')`;
|
||||||
|
} else {
|
||||||
|
// 기본 테이블 컬럼은 main 별칭 사용
|
||||||
|
return `COALESCE(main.${col}::TEXT, '')`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(` || '${separator}' || `);
|
||||||
|
|
||||||
|
return `(${concatParts}) AS ${config.aliasColumn}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
// SELECT 절 구성
|
// SELECT 절 구성
|
||||||
|
|
@ -199,11 +293,20 @@ export class EntityJoinService {
|
||||||
try {
|
try {
|
||||||
const strategies = await Promise.all(
|
const strategies = await Promise.all(
|
||||||
joinConfigs.map(async (config) => {
|
joinConfigs.map(async (config) => {
|
||||||
|
// 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가
|
||||||
|
if (config.displayColumns && config.displayColumns.length > 1) {
|
||||||
|
console.log(
|
||||||
|
`🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${config.sourceColumn}`,
|
||||||
|
config.displayColumns
|
||||||
|
);
|
||||||
|
return "join";
|
||||||
|
}
|
||||||
|
|
||||||
// 참조 테이블의 캐시 가능성 확인
|
// 참조 테이블의 캐시 가능성 확인
|
||||||
const cachedData = await referenceCacheService.getCachedReference(
|
const cachedData = await referenceCacheService.getCachedReference(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn
|
config.displayColumn || config.displayColumns[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
return cachedData ? "cache" : "join";
|
return cachedData ? "cache" : "join";
|
||||||
|
|
@ -245,17 +348,23 @@ export class EntityJoinService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 참조 컬럼 존재 확인
|
// 참조 컬럼 존재 확인 (displayColumns[0] 사용)
|
||||||
|
const displayColumn = config.displayColumns?.[0] || config.displayColumn;
|
||||||
|
if (!displayColumn) {
|
||||||
|
logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const columnExists = await prisma.$queryRaw`
|
const columnExists = await prisma.$queryRaw`
|
||||||
SELECT 1 FROM information_schema.columns
|
SELECT 1 FROM information_schema.columns
|
||||||
WHERE table_name = ${config.referenceTable}
|
WHERE table_name = ${config.referenceTable}
|
||||||
AND column_name = ${config.displayColumn}
|
AND column_name = ${displayColumn}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}`
|
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2023,6 +2023,7 @@ export class TableManagementService {
|
||||||
sourceColumn: string;
|
sourceColumn: string;
|
||||||
joinAlias: string;
|
joinAlias: string;
|
||||||
}>;
|
}>;
|
||||||
|
screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
|
||||||
}
|
}
|
||||||
): Promise<EntityJoinResponse> {
|
): Promise<EntityJoinResponse> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
@ -2042,8 +2043,11 @@ export class TableManagementService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entity 조인 설정 감지
|
// Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
|
||||||
let joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
let joinConfigs = await entityJoinService.detectEntityJoins(
|
||||||
|
tableName,
|
||||||
|
options.screenEntityConfigs
|
||||||
|
);
|
||||||
|
|
||||||
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
|
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
|
||||||
if (
|
if (
|
||||||
|
|
@ -2061,19 +2065,27 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (baseJoinConfig) {
|
if (baseJoinConfig) {
|
||||||
|
// joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name)
|
||||||
|
// sourceColumn을 제거한 나머지 부분이 실제 컬럼명
|
||||||
|
const sourceColumn = baseJoinConfig.sourceColumn; // dept_code
|
||||||
|
const joinAlias = additionalColumn.joinAlias; // dept_code_location_name
|
||||||
|
const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // location_name
|
||||||
|
|
||||||
// 추가 조인 컬럼 설정 생성
|
// 추가 조인 컬럼 설정 생성
|
||||||
const additionalJoinConfig: EntityJoinConfig = {
|
const additionalJoinConfig: EntityJoinConfig = {
|
||||||
sourceTable: tableName,
|
sourceTable: tableName,
|
||||||
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer)
|
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code)
|
||||||
referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info)
|
referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info)
|
||||||
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id)
|
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code)
|
||||||
displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email)
|
displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name)
|
||||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email)
|
displayColumn: actualColumnName, // 하위 호환성
|
||||||
|
aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name)
|
||||||
|
separator: " - ", // 기본 구분자
|
||||||
};
|
};
|
||||||
|
|
||||||
joinConfigs.push(additionalJoinConfig);
|
joinConfigs.push(additionalJoinConfig);
|
||||||
logger.info(
|
logger.info(
|
||||||
`추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn}`
|
`추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2242,7 +2254,7 @@ export class TableManagementService {
|
||||||
await referenceCacheService.getCachedReference(
|
await referenceCacheService.getCachedReference(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn
|
config.displayColumn || config.displayColumns[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2429,7 +2441,7 @@ export class TableManagementService {
|
||||||
const lookupValue = referenceCacheService.getLookupValue(
|
const lookupValue = referenceCacheService.getLookupValue(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn,
|
config.displayColumn || config.displayColumns[0],
|
||||||
String(sourceValue)
|
String(sourceValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2723,7 +2735,7 @@ export class TableManagementService {
|
||||||
const cachedData = await referenceCacheService.getCachedReference(
|
const cachedData = await referenceCacheService.getCachedReference(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn
|
config.displayColumn || config.displayColumns[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedData && cachedData.size > 0) {
|
if (cachedData && cachedData.size > 0) {
|
||||||
|
|
@ -2807,7 +2819,7 @@ export class TableManagementService {
|
||||||
const cachedData = await referenceCacheService.getCachedReference(
|
const cachedData = await referenceCacheService.getCachedReference(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn
|
config.displayColumn || config.displayColumns[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
|
|
@ -2846,7 +2858,7 @@ export class TableManagementService {
|
||||||
const hitRate = referenceCacheService.getCacheHitRate(
|
const hitRate = referenceCacheService.getCacheHitRate(
|
||||||
config.referenceTable,
|
config.referenceTable,
|
||||||
config.referenceColumn,
|
config.referenceColumn,
|
||||||
config.displayColumn
|
config.displayColumn || config.displayColumns[0]
|
||||||
);
|
);
|
||||||
totalHitRate += hitRate;
|
totalHitRate += hitRate;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,10 @@ export interface EntityJoinConfig {
|
||||||
sourceColumn: string; // writer
|
sourceColumn: string; // writer
|
||||||
referenceTable: string; // user_info
|
referenceTable: string; // user_info
|
||||||
referenceColumn: string; // user_id (조인 키)
|
referenceColumn: string; // user_id (조인 키)
|
||||||
displayColumn: string; // user_name (표시할 값)
|
displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들)
|
||||||
|
displayColumn?: string; // user_name (하위 호환성용, deprecated)
|
||||||
aliasColumn: string; // writer_name (결과 컬럼명)
|
aliasColumn: string; // writer_name (결과 컬럼명)
|
||||||
|
separator?: string; // ' - ' (여러 컬럼 연결 시 구분자)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityJoinResponse {
|
export interface EntityJoinResponse {
|
||||||
|
|
|
||||||
|
|
@ -867,46 +867,6 @@ export default function TableManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 표시 컬럼 */}
|
|
||||||
{column.referenceTable && column.referenceTable !== "none" && (
|
|
||||||
<div>
|
|
||||||
<label className="mb-1 block text-xs text-gray-600">표시 컬럼</label>
|
|
||||||
<Select
|
|
||||||
value={column.displayColumn || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleDetailSettingsChange(
|
|
||||||
column.columnName,
|
|
||||||
"entity_display_column",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-7 bg-white text-xs">
|
|
||||||
<SelectValue placeholder="선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
|
||||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
|
||||||
<SelectItem
|
|
||||||
key={`display-col-${refCol.columnName}-${index}`}
|
|
||||||
value={refCol.columnName}
|
|
||||||
>
|
|
||||||
<span className="font-medium">{refCol.columnName}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
{(!referenceTableColumns[column.referenceTable] ||
|
|
||||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
|
||||||
<SelectItem value="loading" disabled>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-3 w-3 animate-spin rounded-full border border-blue-500 border-t-transparent"></div>
|
|
||||||
로딩중
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 설정 완료 표시 - 간소화 */}
|
{/* 설정 완료 표시 - 간소화 */}
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||||
dataType: col.dataType || col.data_type || col.dbType,
|
dataType: col.dataType || col.data_type || col.dbType,
|
||||||
webType: col.webType || col.web_type,
|
webType: col.webType || col.web_type,
|
||||||
|
input_type: col.inputType || col.input_type, // 🎯 input_type 필드 추가
|
||||||
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
|
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
|
||||||
isNullable: col.isNullable || col.is_nullable,
|
isNullable: col.isNullable || col.is_nullable,
|
||||||
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
||||||
|
|
|
||||||
|
|
@ -1013,6 +1013,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
currentTable,
|
currentTable,
|
||||||
columns: currentTable?.columns,
|
columns: currentTable?.columns,
|
||||||
columnsLength: currentTable?.columns?.length,
|
columnsLength: currentTable?.columns?.length,
|
||||||
|
sampleColumn: currentTable?.columns?.[0],
|
||||||
|
deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
|
||||||
});
|
});
|
||||||
return currentTable?.columns || [];
|
return currentTable?.columns || [];
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -18,40 +18,36 @@ interface EntityTypeConfigPanelProps {
|
||||||
export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ config, onConfigChange }) => {
|
export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ config, onConfigChange }) => {
|
||||||
// 기본값이 설정된 config 사용
|
// 기본값이 설정된 config 사용
|
||||||
const safeConfig = {
|
const safeConfig = {
|
||||||
entityName: "",
|
referenceTable: "",
|
||||||
displayField: "name",
|
referenceColumn: "id",
|
||||||
valueField: "id",
|
displayColumns: config.displayColumns || (config.displayColumn ? [config.displayColumn] : ["name"]), // 호환성 처리
|
||||||
searchable: true,
|
searchColumns: [],
|
||||||
multiple: false,
|
filters: {},
|
||||||
allowClear: true,
|
|
||||||
placeholder: "",
|
placeholder: "",
|
||||||
apiEndpoint: "",
|
|
||||||
filters: [],
|
|
||||||
displayFormat: "simple",
|
displayFormat: "simple",
|
||||||
maxSelections: undefined,
|
separator: " - ",
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 로컬 상태로 실시간 입력 관리
|
// 로컬 상태로 실시간 입력 관리
|
||||||
const [localValues, setLocalValues] = useState({
|
const [localValues, setLocalValues] = useState({
|
||||||
entityName: safeConfig.entityName,
|
referenceTable: safeConfig.referenceTable,
|
||||||
displayField: safeConfig.displayField,
|
referenceColumn: safeConfig.referenceColumn,
|
||||||
valueField: safeConfig.valueField,
|
displayColumns: [...safeConfig.displayColumns],
|
||||||
searchable: safeConfig.searchable,
|
searchColumns: [...(safeConfig.searchColumns || [])],
|
||||||
multiple: safeConfig.multiple,
|
|
||||||
allowClear: safeConfig.allowClear,
|
|
||||||
placeholder: safeConfig.placeholder,
|
placeholder: safeConfig.placeholder,
|
||||||
apiEndpoint: safeConfig.apiEndpoint,
|
|
||||||
displayFormat: safeConfig.displayFormat,
|
displayFormat: safeConfig.displayFormat,
|
||||||
maxSelections: safeConfig.maxSelections?.toString() || "",
|
separator: safeConfig.separator,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" });
|
const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" });
|
||||||
|
const [newDisplayColumn, setNewDisplayColumn] = useState("");
|
||||||
|
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
|
||||||
|
|
||||||
// 표시 형식 옵션
|
// 표시 형식 옵션
|
||||||
const displayFormats = [
|
const displayFormats = [
|
||||||
{ value: "simple", label: "단순 (이름만)" },
|
{ value: "simple", label: "단순 (첫 번째 컬럼만)" },
|
||||||
{ value: "detailed", label: "상세 (이름 + 설명)" },
|
{ value: "detailed", label: "상세 (모든 컬럼 표시)" },
|
||||||
{ value: "custom", label: "사용자 정의" },
|
{ value: "custom", label: "사용자 정의" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -71,37 +67,27 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
// config가 변경될 때 로컬 상태 동기화
|
// config가 변경될 때 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalValues({
|
setLocalValues({
|
||||||
entityName: safeConfig.entityName,
|
referenceTable: safeConfig.referenceTable,
|
||||||
displayField: safeConfig.displayField,
|
referenceColumn: safeConfig.referenceColumn,
|
||||||
valueField: safeConfig.valueField,
|
displayColumns: [...safeConfig.displayColumns],
|
||||||
searchable: safeConfig.searchable,
|
searchColumns: [...(safeConfig.searchColumns || [])],
|
||||||
multiple: safeConfig.multiple,
|
|
||||||
allowClear: safeConfig.allowClear,
|
|
||||||
placeholder: safeConfig.placeholder,
|
placeholder: safeConfig.placeholder,
|
||||||
apiEndpoint: safeConfig.apiEndpoint,
|
|
||||||
displayFormat: safeConfig.displayFormat,
|
displayFormat: safeConfig.displayFormat,
|
||||||
maxSelections: safeConfig.maxSelections?.toString() || "",
|
separator: safeConfig.separator,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
safeConfig.entityName,
|
safeConfig.referenceTable,
|
||||||
safeConfig.displayField,
|
safeConfig.referenceColumn,
|
||||||
safeConfig.valueField,
|
safeConfig.displayColumns,
|
||||||
safeConfig.searchable,
|
safeConfig.searchColumns,
|
||||||
safeConfig.multiple,
|
|
||||||
safeConfig.allowClear,
|
|
||||||
safeConfig.placeholder,
|
safeConfig.placeholder,
|
||||||
safeConfig.apiEndpoint,
|
|
||||||
safeConfig.displayFormat,
|
safeConfig.displayFormat,
|
||||||
safeConfig.maxSelections,
|
safeConfig.separator,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateConfig = (key: keyof EntityTypeConfig, value: any) => {
|
const updateConfig = (key: keyof EntityTypeConfig, value: any) => {
|
||||||
// 로컬 상태 즉시 업데이트
|
// 로컬 상태 즉시 업데이트
|
||||||
if (key === "maxSelections") {
|
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||||
setLocalValues((prev) => ({ ...prev, [key]: value?.toString() || "" }));
|
|
||||||
} else {
|
|
||||||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 실제 config 업데이트
|
// 실제 config 업데이트
|
||||||
const newConfig = { ...safeConfig, [key]: value };
|
const newConfig = { ...safeConfig, [key]: value };
|
||||||
|
|
@ -114,82 +100,131 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 표시 컬럼 추가
|
||||||
|
const addDisplayColumn = () => {
|
||||||
|
if (newDisplayColumn.trim() && !localValues.displayColumns.includes(newDisplayColumn.trim())) {
|
||||||
|
const updatedColumns = [...localValues.displayColumns, newDisplayColumn.trim()];
|
||||||
|
updateConfig("displayColumns", updatedColumns);
|
||||||
|
setNewDisplayColumn("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 표시 컬럼 제거
|
||||||
|
const removeDisplayColumn = (index: number) => {
|
||||||
|
const updatedColumns = localValues.displayColumns.filter((_, i) => i !== index);
|
||||||
|
updateConfig("displayColumns", updatedColumns);
|
||||||
|
};
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
if (newFilter.field.trim() && newFilter.value.trim()) {
|
if (newFilter.field.trim() && newFilter.value.trim()) {
|
||||||
const updatedFilters = [...(safeConfig.filters || []), { ...newFilter }];
|
const updatedFilters = { ...safeConfig.filters, [newFilter.field]: newFilter.value };
|
||||||
updateConfig("filters", updatedFilters);
|
updateConfig("filters", updatedFilters);
|
||||||
setNewFilter({ field: "", operator: "=", value: "" });
|
setNewFilter({ field: "", operator: "=", value: "" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFilter = (index: number) => {
|
const removeFilter = (field: string) => {
|
||||||
const updatedFilters = (safeConfig.filters || []).filter((_, i) => i !== index);
|
const updatedFilters = { ...safeConfig.filters };
|
||||||
|
delete updatedFilters[field];
|
||||||
updateConfig("filters", updatedFilters);
|
updateConfig("filters", updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFilter = (index: number, field: keyof typeof newFilter, value: string) => {
|
const updateFilter = (oldField: string, field: string, value: string) => {
|
||||||
const updatedFilters = [...(safeConfig.filters || [])];
|
const updatedFilters = { ...safeConfig.filters };
|
||||||
updatedFilters[index] = { ...updatedFilters[index], [field]: value };
|
if (oldField !== field) {
|
||||||
|
delete updatedFilters[oldField];
|
||||||
|
}
|
||||||
|
updatedFilters[field] = value;
|
||||||
updateConfig("filters", updatedFilters);
|
updateConfig("filters", updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 엔터티 이름 */}
|
{/* 참조 테이블 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="entityName" className="text-sm font-medium">
|
<Label htmlFor="referenceTable" className="text-sm font-medium">
|
||||||
엔터티 이름
|
참조 테이블
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="entityName"
|
id="referenceTable"
|
||||||
value={localValues.entityName}
|
value={localValues.referenceTable}
|
||||||
onChange={(e) => updateConfig("entityName", e.target.value)}
|
onChange={(e) => updateConfig("referenceTable", e.target.value)}
|
||||||
placeholder="예: User, Company, Product"
|
placeholder="예: user_info, company_info"
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API 엔드포인트 */}
|
{/* 조인 컬럼 (값 필드) */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="apiEndpoint" className="text-sm font-medium">
|
<Label htmlFor="referenceColumn" className="text-sm font-medium">
|
||||||
API 엔드포인트
|
조인 컬럼 (값 필드)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="apiEndpoint"
|
id="referenceColumn"
|
||||||
value={localValues.apiEndpoint}
|
value={localValues.referenceColumn}
|
||||||
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
|
onChange={(e) => updateConfig("referenceColumn", e.target.value)}
|
||||||
placeholder="예: /api/users"
|
placeholder="id, user_id, company_code"
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필드 설정 */}
|
{/* 표시 컬럼들 (다중 선택) */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<Label className="text-sm font-medium">표시 컬럼들</Label>
|
||||||
<Label htmlFor="valueField" className="text-sm font-medium">
|
|
||||||
값 필드
|
{/* 현재 선택된 표시 컬럼들 */}
|
||||||
</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
{localValues.displayColumns.map((column, index) => (
|
||||||
id="valueField"
|
<div key={index} className="flex items-center space-x-2 rounded border bg-gray-50 p-2">
|
||||||
value={localValues.valueField}
|
<Database className="h-4 w-4 text-gray-500" />
|
||||||
onChange={(e) => updateConfig("valueField", e.target.value)}
|
<span className="flex-1 text-sm font-medium">{column}</span>
|
||||||
placeholder="id"
|
<Button size="sm" variant="outline" onClick={() => removeDisplayColumn(index)}>
|
||||||
className="mt-1"
|
<X className="h-3 w-3" />
|
||||||
/>
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{localValues.displayColumns.length === 0 && (
|
||||||
|
<div className="text-sm text-gray-500 italic">표시할 컬럼을 추가해주세요</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* 새 표시 컬럼 추가 */}
|
||||||
<Label htmlFor="displayField" className="text-sm font-medium">
|
<div className="flex items-center space-x-2">
|
||||||
표시 필드
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="displayField"
|
value={newDisplayColumn}
|
||||||
value={localValues.displayField}
|
onChange={(e) => setNewDisplayColumn(e.target.value)}
|
||||||
onChange={(e) => updateConfig("displayField", e.target.value)}
|
placeholder="컬럼명 입력 (예: user_name, dept_name)"
|
||||||
placeholder="name"
|
className="flex-1"
|
||||||
className="mt-1"
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={addDisplayColumn}
|
||||||
|
disabled={!newDisplayColumn.trim() || localValues.displayColumns.includes(newDisplayColumn.trim())}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
• 여러 컬럼을 선택하면 "{localValues.separator || " - "}"로 구분하여 표시됩니다
|
||||||
|
<br />• 예: 이름{localValues.separator || " - "}부서명
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 구분자 설정 */}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="separator" className="text-sm font-medium">
|
||||||
|
구분자
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="separator"
|
||||||
|
value={localValues.separator}
|
||||||
|
onChange={(e) => updateConfig("separator", e.target.value)}
|
||||||
|
placeholder=" - "
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 표시 형식 */}
|
{/* 표시 형식 */}
|
||||||
|
|
@ -225,93 +260,28 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 옵션들 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="searchable" className="text-sm font-medium">
|
|
||||||
검색 가능
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="searchable"
|
|
||||||
checked={localValues.searchable}
|
|
||||||
onCheckedChange={(checked) => updateConfig("searchable", !!checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="multiple" className="text-sm font-medium">
|
|
||||||
다중 선택
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="multiple"
|
|
||||||
checked={localValues.multiple}
|
|
||||||
onCheckedChange={(checked) => updateConfig("multiple", !!checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="allowClear" className="text-sm font-medium">
|
|
||||||
선택 해제 허용
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="allowClear"
|
|
||||||
checked={localValues.allowClear}
|
|
||||||
onCheckedChange={(checked) => updateConfig("allowClear", !!checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 최대 선택 개수 (다중 선택 시) */}
|
|
||||||
{localValues.multiple && (
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="maxSelections" className="text-sm font-medium">
|
|
||||||
최대 선택 개수
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="maxSelections"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={localValues.maxSelections}
|
|
||||||
onChange={(e) => updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)}
|
|
||||||
className="mt-1"
|
|
||||||
placeholder="제한 없음"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 필터 관리 */}
|
{/* 필터 관리 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-sm font-medium">데이터 필터</Label>
|
<Label className="text-sm font-medium">데이터 필터</Label>
|
||||||
|
|
||||||
{/* 기존 필터 목록 */}
|
{/* 기존 필터 목록 */}
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{(safeConfig.filters || []).map((filter, index) => (
|
{Object.entries(safeConfig.filters || {}).map(([field, value]) => (
|
||||||
<div key={index} className="flex items-center space-x-2 rounded border p-2 text-sm">
|
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-sm">
|
||||||
<Input
|
<Input
|
||||||
value={filter.field}
|
value={field}
|
||||||
onChange={(e) => updateFilter(index, "field", e.target.value)}
|
onChange={(e) => updateFilter(field, e.target.value, value as string)}
|
||||||
placeholder="필드명"
|
placeholder="필드명"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Select value={filter.operator} onValueChange={(value) => updateFilter(index, "operator", value)}>
|
<span className="text-gray-500">=</span>
|
||||||
<SelectTrigger className="w-20">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{operators.map((op) => (
|
|
||||||
<SelectItem key={op.value} value={op.value}>
|
|
||||||
{op.value}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Input
|
<Input
|
||||||
value={filter.value}
|
value={value as string}
|
||||||
onChange={(e) => updateFilter(index, "value", e.target.value)}
|
onChange={(e) => updateFilter(field, field, e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline" onClick={() => removeFilter(index)}>
|
<Button size="sm" variant="outline" onClick={() => removeFilter(field)}>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,21 +296,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
placeholder="필드명"
|
placeholder="필드명"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Select
|
<span className="text-gray-500">=</span>
|
||||||
value={newFilter.operator}
|
|
||||||
onValueChange={(value) => setNewFilter((prev) => ({ ...prev, operator: value }))}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-20">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{operators.map((op) => (
|
|
||||||
<SelectItem key={op.value} value={op.value}>
|
|
||||||
{op.value}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Input
|
<Input
|
||||||
value={newFilter.value}
|
value={newFilter.value}
|
||||||
onChange={(e) => setNewFilter((prev) => ({ ...prev, value: e.target.value }))}
|
onChange={(e) => setNewFilter((prev) => ({ ...prev, value: e.target.value }))}
|
||||||
|
|
@ -352,7 +308,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500">총 {(safeConfig.filters || []).length}개 필터</div>
|
<div className="text-xs text-gray-500">총 {Object.keys(safeConfig.filters || {}).length}개 필터</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 미리보기 */}
|
{/* 미리보기 */}
|
||||||
|
|
@ -360,31 +316,33 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="flex items-center space-x-2 rounded border bg-white p-2">
|
<div className="flex items-center space-x-2 rounded border bg-white p-2">
|
||||||
{localValues.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
<div className="flex-1 text-sm text-gray-600">
|
<div className="flex-1 text-sm text-gray-600">
|
||||||
{localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`}
|
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
|
||||||
</div>
|
</div>
|
||||||
<Database className="h-4 w-4 text-gray-400" />
|
<Database className="h-4 w-4 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
엔터티: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, 값필드:{" "}
|
참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn}
|
||||||
{localValues.valueField}, 표시필드: {localValues.displayField}
|
<br />
|
||||||
{localValues.multiple && `, 다중선택`}
|
표시컬럼:{" "}
|
||||||
{localValues.searchable && `, 검색가능`}
|
{localValues.displayColumns.length > 0
|
||||||
|
? localValues.displayColumns.join(localValues.separator || " - ")
|
||||||
|
: "없음"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
<div className="rounded-md bg-blue-50 p-3">
|
<div className="rounded-md bg-blue-50 p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">엔터티 참조 설정</div>
|
<div className="text-sm font-medium text-blue-900">엔터티 타입 설정 가이드</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
• 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다
|
• <strong>참조 테이블</strong>: 데이터를 가져올 다른 테이블 이름
|
||||||
|
<br />• <strong>조인 컬럼</strong>: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID)
|
||||||
|
<br />• <strong>표시 컬럼</strong>: 사용자에게 보여질 컬럼들 (여러 개 가능)
|
||||||
<br />
|
<br />
|
||||||
• API 엔드포인트를 통해 데이터를 동적으로 로드합니다
|
• 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다
|
||||||
<br />
|
<br />• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
|
||||||
• 필터를 사용하여 표시할 데이터를 제한할 수 있습니다
|
|
||||||
<br />• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export const entityJoinApi = {
|
||||||
sourceColumn: string;
|
sourceColumn: string;
|
||||||
joinAlias: string;
|
joinAlias: string;
|
||||||
}>;
|
}>;
|
||||||
|
screenEntityConfigs?: Record<string, any>; // 🎯 화면별 엔티티 설정
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<EntityJoinResponse> => {
|
): Promise<EntityJoinResponse> => {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
@ -93,6 +94,7 @@ export const entityJoinApi = {
|
||||||
...params,
|
...params,
|
||||||
search: params.search ? JSON.stringify(params.search) : undefined,
|
search: params.search ? JSON.stringify(params.search) : undefined,
|
||||||
additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined,
|
additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined,
|
||||||
|
screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
...componentConfig,
|
...componentConfig,
|
||||||
} as TableListConfig;
|
} as TableListConfig;
|
||||||
|
|
||||||
|
// 🎯 디버깅: 초기 컬럼 설정 확인
|
||||||
|
console.log(
|
||||||
|
"🔍 초기 tableConfig.columns:",
|
||||||
|
tableConfig.columns?.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
|
||||||
// 상태 관리
|
// 상태 관리
|
||||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -98,6 +104,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [tableLabel, setTableLabel] = useState<string>("");
|
const [tableLabel, setTableLabel] = useState<string>("");
|
||||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태
|
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태
|
||||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
|
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
|
||||||
|
|
||||||
|
// 🎯 조인 컬럼 매핑 상태
|
||||||
|
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
||||||
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
|
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
|
||||||
|
|
||||||
// 고급 필터 관련 state
|
// 고급 필터 관련 state
|
||||||
|
|
@ -192,12 +201,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용
|
// 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용
|
||||||
let displayLabel = column.displayName || column.columnName;
|
let displayLabel = column.displayName || column.columnName;
|
||||||
|
|
||||||
// Entity 타입이고 display_column이 있는 경우
|
// Entity 타입인 경우
|
||||||
if (column.webType === "entity" && column.displayColumn) {
|
if (column.webType === "entity") {
|
||||||
// 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 displayColumn 사용
|
// 우선 기준 테이블의 컬럼 라벨을 사용
|
||||||
displayLabel = column.displayColumnLabel || column.displayColumn;
|
displayLabel = column.displayName || column.columnName;
|
||||||
console.log(
|
console.log(
|
||||||
`🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`,
|
`🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,13 +263,77 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들)
|
// Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들)
|
||||||
const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || [];
|
const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || [];
|
||||||
const additionalJoinColumns = entityJoinColumns.map((col) => ({
|
|
||||||
sourceTable: col.entityJoinInfo!.sourceTable,
|
|
||||||
sourceColumn: col.entityJoinInfo!.sourceColumn,
|
|
||||||
joinAlias: col.entityJoinInfo!.joinAlias,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
// 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만)
|
||||||
|
const joinTabColumns =
|
||||||
|
tableConfig.columns?.filter(
|
||||||
|
(col) =>
|
||||||
|
!col.isEntityJoin &&
|
||||||
|
col.columnName.includes("_") &&
|
||||||
|
(col.columnName.includes("dept_code_") ||
|
||||||
|
col.columnName.includes("_dept_code") ||
|
||||||
|
col.columnName.includes("_company_") ||
|
||||||
|
col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"🔍 조인 탭 컬럼들:",
|
||||||
|
joinTabColumns.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
|
||||||
|
const additionalJoinColumns = [
|
||||||
|
...entityJoinColumns.map((col) => ({
|
||||||
|
sourceTable: col.entityJoinInfo!.sourceTable,
|
||||||
|
sourceColumn: col.entityJoinInfo!.sourceColumn,
|
||||||
|
joinAlias: col.entityJoinInfo!.joinAlias,
|
||||||
|
})),
|
||||||
|
// 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만)
|
||||||
|
...joinTabColumns
|
||||||
|
.filter((col) => {
|
||||||
|
// 실제 API 응답에 존재하는 컬럼만 필터링
|
||||||
|
const validJoinColumns = ["dept_code_name", "dept_name"];
|
||||||
|
const isValid = validJoinColumns.includes(col.columnName);
|
||||||
|
if (!isValid) {
|
||||||
|
console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`);
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
})
|
||||||
|
.map((col) => {
|
||||||
|
// 실제 존재하는 조인 컬럼만 처리
|
||||||
|
let sourceTable = tableConfig.selectedTable;
|
||||||
|
let sourceColumn = col.columnName;
|
||||||
|
|
||||||
|
if (col.columnName === "dept_code_name" || col.columnName === "dept_name") {
|
||||||
|
sourceTable = "dept_info";
|
||||||
|
sourceColumn = "dept_code";
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceTable: sourceTable,
|
||||||
|
sourceColumn: sourceColumn,
|
||||||
|
joinAlias: col.columnName,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🎯 화면별 엔티티 표시 설정 생성
|
||||||
|
const screenEntityConfigs: Record<string, any> = {};
|
||||||
|
entityJoinColumns.forEach((col) => {
|
||||||
|
if (col.entityDisplayConfig) {
|
||||||
|
const sourceColumn = col.entityJoinInfo!.sourceColumn;
|
||||||
|
screenEntityConfigs[sourceColumn] = {
|
||||||
|
displayColumns: col.entityDisplayConfig.displayColumns,
|
||||||
|
separator: col.entityDisplayConfig.separator || " - ",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🔗 Entity 조인 컬럼:", entityJoinColumns);
|
||||||
|
console.log("🔗 조인 탭 컬럼:", joinTabColumns);
|
||||||
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
|
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
|
||||||
|
console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs);
|
||||||
|
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
|
|
@ -329,9 +402,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
sortOrder: sortDirection,
|
sortOrder: sortDirection,
|
||||||
enableEntityJoin: true, // 🎯 Entity 조인 활성화
|
enableEntityJoin: true, // 🎯 Entity 조인 활성화
|
||||||
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
|
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
|
||||||
|
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
console.log("🎯 API 응답 결과:", result);
|
||||||
|
console.log("🎯 데이터 개수:", result.data?.length || 0);
|
||||||
|
console.log("🎯 전체 페이지:", result.totalPages);
|
||||||
|
console.log("🎯 총 아이템:", result.total);
|
||||||
setData(result.data || []);
|
setData(result.data || []);
|
||||||
setTotalPages(result.totalPages || 1);
|
setTotalPages(result.totalPages || 1);
|
||||||
setTotalItems(result.total || 0);
|
setTotalItems(result.total || 0);
|
||||||
|
|
@ -369,12 +447,88 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 Entity 조인된 컬럼 처리
|
// 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용
|
||||||
let processedColumns = [...(tableConfig.columns || [])];
|
let processedColumns = [...(tableConfig.columns || [])];
|
||||||
|
|
||||||
// 초기 컬럼이 있으면 먼저 설정
|
// 초기 컬럼이 있으면 먼저 설정
|
||||||
if (processedColumns.length > 0) {
|
if (processedColumns.length > 0) {
|
||||||
setDisplayColumns(processedColumns);
|
console.log(
|
||||||
|
"🔍 사용자 설정 컬럼들:",
|
||||||
|
processedColumns.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링
|
||||||
|
if (result.data.length > 0) {
|
||||||
|
const actualApiColumns = Object.keys(result.data[0]);
|
||||||
|
console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns);
|
||||||
|
|
||||||
|
// 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답)
|
||||||
|
// 실제 API 응답에 존재하는 컬럼만 매핑
|
||||||
|
const newJoinColumnMapping: Record<string, string> = {
|
||||||
|
dept_code_dept_code: "dept_code", // user_info.dept_code
|
||||||
|
dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음)
|
||||||
|
dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음)
|
||||||
|
dept_code_name: "dept_code_name", // dept_info.dept_name
|
||||||
|
dept_name: "dept_name", // dept_info.dept_name
|
||||||
|
status: "status", // user_info.status
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 조인 컬럼 매핑 상태 업데이트
|
||||||
|
setJoinColumnMapping(newJoinColumnMapping);
|
||||||
|
|
||||||
|
console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping);
|
||||||
|
console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns);
|
||||||
|
|
||||||
|
// 🎯 컬럼명 매핑 및 유효성 검사
|
||||||
|
const validColumns = processedColumns
|
||||||
|
.map((col) => {
|
||||||
|
// 체크박스는 그대로 유지
|
||||||
|
if (col.columnName === "__checkbox__") return col;
|
||||||
|
|
||||||
|
// 조인 컬럼 매핑 적용
|
||||||
|
const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName;
|
||||||
|
|
||||||
|
console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`);
|
||||||
|
|
||||||
|
// API 응답에 존재하는지 확인
|
||||||
|
const existsInApi = actualApiColumns.includes(mappedColumnName);
|
||||||
|
|
||||||
|
if (!existsInApi) {
|
||||||
|
console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컬럼명이 변경된 경우 업데이트
|
||||||
|
if (mappedColumnName !== col.columnName) {
|
||||||
|
console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`);
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
columnName: mappedColumnName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 컬럼 유지: ${col.columnName}`);
|
||||||
|
return col;
|
||||||
|
})
|
||||||
|
.filter((col) => col !== null) as ColumnConfig[];
|
||||||
|
|
||||||
|
if (validColumns.length !== processedColumns.length) {
|
||||||
|
console.log(
|
||||||
|
"🔍 필터링된 컬럼들:",
|
||||||
|
validColumns.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"🔍 제거된 컬럼들:",
|
||||||
|
processedColumns
|
||||||
|
.filter((col) => {
|
||||||
|
const mappedName = newJoinColumnMapping[col.columnName] || col.columnName;
|
||||||
|
return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__";
|
||||||
|
})
|
||||||
|
.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
processedColumns = validColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (result.entityJoinInfo?.joinConfigs) {
|
if (result.entityJoinInfo?.joinConfigs) {
|
||||||
result.entityJoinInfo.joinConfigs.forEach((joinConfig) => {
|
result.entityJoinInfo.joinConfigs.forEach((joinConfig) => {
|
||||||
|
|
@ -398,11 +552,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 정보가 없으면 첫 번째 데이터 행에서 추출
|
// 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성
|
||||||
if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) {
|
if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) {
|
||||||
const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({
|
const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({
|
||||||
columnName: key,
|
columnName: key,
|
||||||
displayName: columnLabels[key] || key, // 라벨명 우선 사용
|
displayName: columnLabels[key] || key,
|
||||||
visible: true,
|
visible: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
|
|
@ -411,6 +565,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
order: index,
|
order: index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"🎯 자동 생성된 컬럼들:",
|
||||||
|
autoColumns.map((c) => c.columnName),
|
||||||
|
);
|
||||||
|
|
||||||
// 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림)
|
// 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림)
|
||||||
if (onFormDataChange) {
|
if (onFormDataChange) {
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
|
|
@ -426,6 +585,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 🎯 표시할 컬럼 상태 업데이트
|
// 🎯 표시할 컬럼 상태 업데이트
|
||||||
setDisplayColumns(processedColumns);
|
setDisplayColumns(processedColumns);
|
||||||
|
console.log("🎯 displayColumns 업데이트됨:", processedColumns);
|
||||||
|
console.log("🎯 데이터 개수:", result.data?.length || 0);
|
||||||
|
console.log("🎯 전체 데이터:", result.data);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("테이블 데이터 로딩 오류:", err);
|
console.error("테이블 데이터 로딩 오류:", err);
|
||||||
|
|
@ -614,9 +776,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
let columns: ColumnConfig[] = [];
|
let columns: ColumnConfig[] = [];
|
||||||
|
|
||||||
if (!displayColumns || displayColumns.length === 0) {
|
// displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들)
|
||||||
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
|
if (displayColumns && displayColumns.length > 0) {
|
||||||
if (!tableConfig.columns) return [];
|
console.log("🎯 displayColumns 사용:", displayColumns);
|
||||||
|
const filteredColumns = displayColumns.filter((col) => {
|
||||||
|
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
||||||
|
if (isDesignMode) {
|
||||||
|
return col.visible; // 디자인 모드에서는 visible만 체크
|
||||||
|
} else {
|
||||||
|
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("🎯 필터링된 컬럼:", filteredColumns);
|
||||||
|
columns = filteredColumns.sort((a, b) => a.order - b.order);
|
||||||
|
} else if (tableConfig.columns && tableConfig.columns.length > 0) {
|
||||||
|
// displayColumns가 없으면 기본 컬럼 사용
|
||||||
|
console.log("🎯 tableConfig.columns 사용:", tableConfig.columns);
|
||||||
columns = tableConfig.columns
|
columns = tableConfig.columns
|
||||||
.filter((col) => {
|
.filter((col) => {
|
||||||
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
||||||
|
|
@ -628,16 +803,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
} else {
|
} else {
|
||||||
columns = displayColumns
|
console.log("🎯 사용할 컬럼이 없음");
|
||||||
.filter((col) => {
|
return [];
|
||||||
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
|
||||||
if (isDesignMode) {
|
|
||||||
return col.visible; // 디자인 모드에서는 visible만 체크
|
|
||||||
} else {
|
|
||||||
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.order - b.order);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가
|
// 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가
|
||||||
|
|
@ -663,8 +830,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("🎯 최종 visibleColumns:", columns);
|
||||||
|
console.log("🎯 visibleColumns 개수:", columns.length);
|
||||||
|
console.log(
|
||||||
|
"🎯 visibleColumns 컬럼명들:",
|
||||||
|
columns.map((c) => c.columnName),
|
||||||
|
);
|
||||||
return columns;
|
return columns;
|
||||||
}, [displayColumns, tableConfig.columns, tableConfig.checkbox]);
|
}, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]);
|
||||||
|
|
||||||
// columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거
|
// columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거
|
||||||
// 기존 테이블에서만 필요한 경우 다시 추가 가능
|
// 기존 테이블에서만 필요한 경우 다시 추가 가능
|
||||||
|
|
@ -1036,7 +1209,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
>
|
>
|
||||||
{column.columnName === "__checkbox__"
|
{column.columnName === "__checkbox__"
|
||||||
? renderCheckboxCell(row, index)
|
? renderCheckboxCell(row, index)
|
||||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
: (() => {
|
||||||
|
// 🎯 매핑된 컬럼명으로 데이터 찾기
|
||||||
|
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||||
|
const cellValue = row[mappedColumnName];
|
||||||
|
if (index === 0) {
|
||||||
|
// 첫 번째 행만 로그 출력
|
||||||
|
console.log(
|
||||||
|
`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`,
|
||||||
|
cellValue,
|
||||||
|
"전체 row:",
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||||
|
})()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Badge } from "@/components/ui/badge";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { TableListConfig, ColumnConfig } from "./types";
|
import { TableListConfig, ColumnConfig } from "./types";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
|
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
|
||||||
|
|
||||||
export interface TableListConfigPanelProps {
|
export interface TableListConfigPanelProps {
|
||||||
|
|
@ -31,7 +32,12 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
screenTableName,
|
screenTableName,
|
||||||
tableColumns,
|
tableColumns,
|
||||||
}) => {
|
}) => {
|
||||||
console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns });
|
console.log("🔍 TableListConfigPanel props:", {
|
||||||
|
config: config?.selectedTable,
|
||||||
|
screenTableName,
|
||||||
|
tableColumns: tableColumns?.length,
|
||||||
|
tableColumnsSample: tableColumns?.[0],
|
||||||
|
});
|
||||||
|
|
||||||
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
|
@ -58,8 +64,22 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>({ availableColumns: [], joinTables: [] });
|
}>({ availableColumns: [], joinTables: [] });
|
||||||
|
|
||||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||||
|
|
||||||
|
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
|
||||||
|
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||||||
|
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||||||
|
selectedColumns: string[];
|
||||||
|
separator: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>({});
|
||||||
|
|
||||||
// 화면 테이블명이 있으면 자동으로 설정
|
// 화면 테이블명이 있으면 자동으로 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
|
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
|
||||||
|
|
@ -73,18 +93,14 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
const fetchTables = async () => {
|
const fetchTables = async () => {
|
||||||
setLoadingTables(true);
|
setLoadingTables(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/tables");
|
// API 클라이언트를 사용하여 올바른 포트로 호출
|
||||||
if (response.ok) {
|
const response = await tableTypeApi.getTables();
|
||||||
const result = await response.json();
|
setAvailableTables(
|
||||||
if (result.success && result.data) {
|
response.map((table: any) => ({
|
||||||
setAvailableTables(
|
tableName: table.tableName,
|
||||||
result.data.map((table: any) => ({
|
displayName: table.displayName || table.tableName,
|
||||||
tableName: table.tableName,
|
})),
|
||||||
displayName: table.displayName || table.tableName,
|
);
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("테이블 목록 가져오기 실패:", error);
|
console.error("테이블 목록 가져오기 실패:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -228,30 +244,26 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
handleChange("columns", [...(config.columns || []), newColumn]);
|
handleChange("columns", [...(config.columns || []), newColumn]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Entity 조인 컬럼 추가
|
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
|
||||||
const addEntityJoinColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
|
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
|
||||||
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
|
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
|
||||||
if (existingColumn) return;
|
if (existingColumn) return;
|
||||||
|
|
||||||
|
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
|
||||||
const newColumn: ColumnConfig = {
|
const newColumn: ColumnConfig = {
|
||||||
columnName: joinColumn.joinAlias,
|
columnName: joinColumn.joinAlias,
|
||||||
displayName: joinColumn.columnLabel, // 라벨명만 사용
|
displayName: joinColumn.columnLabel,
|
||||||
visible: true,
|
visible: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
align: "left",
|
align: "left",
|
||||||
format: "text",
|
format: "text",
|
||||||
order: config.columns?.length || 0,
|
order: config.columns?.length || 0,
|
||||||
isEntityJoin: true, // Entity 조인 컬럼임을 표시
|
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
|
||||||
entityJoinInfo: {
|
|
||||||
sourceTable: joinColumn.tableName,
|
|
||||||
sourceColumn: joinColumn.columnName,
|
|
||||||
joinAlias: joinColumn.joinAlias,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange("columns", [...(config.columns || []), newColumn]);
|
handleChange("columns", [...(config.columns || []), newColumn]);
|
||||||
console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn);
|
console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 컬럼 제거
|
// 컬럼 제거
|
||||||
|
|
@ -267,6 +279,333 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
handleChange("columns", updatedColumns);
|
handleChange("columns", updatedColumns);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
|
||||||
|
hasColumns: !!config.columns,
|
||||||
|
columnsCount: config.columns?.length || 0,
|
||||||
|
hasTableColumns: !!tableColumns,
|
||||||
|
tableColumnsCount: tableColumns?.length || 0,
|
||||||
|
selectedTable: config.selectedTable,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.columns || !tableColumns) {
|
||||||
|
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedColumns = config.columns.map((column) => {
|
||||||
|
// 이미 isEntityJoin이 설정된 경우 스킵
|
||||||
|
if (column.isEntityJoin) {
|
||||||
|
console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName);
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블 컬럼 정보에서 해당 컬럼 찾기
|
||||||
|
const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName);
|
||||||
|
console.log("🔍 컬럼 검색:", {
|
||||||
|
columnName: column.columnName,
|
||||||
|
found: !!tableColumn,
|
||||||
|
inputType: tableColumn?.input_type,
|
||||||
|
webType: tableColumn?.web_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인)
|
||||||
|
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
|
||||||
|
console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", {
|
||||||
|
columnName: column.columnName,
|
||||||
|
referenceTable: tableColumn.reference_table,
|
||||||
|
referenceTableAlt: tableColumn.referenceTable,
|
||||||
|
allTableColumnKeys: Object.keys(tableColumn),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
isEntityJoin: true,
|
||||||
|
entityJoinInfo: {
|
||||||
|
sourceTable: config.selectedTable || "",
|
||||||
|
sourceColumn: column.columnName,
|
||||||
|
joinAlias: column.columnName,
|
||||||
|
},
|
||||||
|
entityDisplayConfig: {
|
||||||
|
displayColumns: [], // 빈 배열로 초기화
|
||||||
|
separator: " - ",
|
||||||
|
sourceTable: config.selectedTable || "",
|
||||||
|
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 변경사항이 있는 경우에만 업데이트
|
||||||
|
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns);
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
} else {
|
||||||
|
console.log("ℹ️ 엔티티 컬럼 변경사항 없음");
|
||||||
|
}
|
||||||
|
}, [config.columns, tableColumns, config.selectedTable]);
|
||||||
|
|
||||||
|
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
|
||||||
|
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
|
||||||
|
console.log("🔍 loadEntityDisplayConfig 시작:", {
|
||||||
|
columnName: column.columnName,
|
||||||
|
isEntityJoin: column.isEntityJoin,
|
||||||
|
entityJoinInfo: column.entityJoinInfo,
|
||||||
|
entityDisplayConfig: column.entityDisplayConfig,
|
||||||
|
configSelectedTable: config.selectedTable,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!column.isEntityJoin || !column.entityJoinInfo) {
|
||||||
|
console.log("⚠️ 엔티티 컬럼 조건 불만족:", {
|
||||||
|
isEntityJoin: column.isEntityJoin,
|
||||||
|
entityJoinInfo: column.entityJoinInfo,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// entityDisplayConfig가 없으면 초기화
|
||||||
|
if (!column.entityDisplayConfig) {
|
||||||
|
console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
|
||||||
|
const updatedColumns = config.columns?.map((col) => {
|
||||||
|
if (col.columnName === column.columnName) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
entityDisplayConfig: {
|
||||||
|
displayColumns: [],
|
||||||
|
separator: " - ",
|
||||||
|
sourceTable: config.selectedTable || "",
|
||||||
|
joinTable: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedColumns) {
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
// 업데이트된 컬럼으로 다시 시도
|
||||||
|
const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName);
|
||||||
|
if (updatedColumn) {
|
||||||
|
console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig);
|
||||||
|
return loadEntityDisplayConfig(updatedColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
|
||||||
|
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
|
||||||
|
|
||||||
|
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기
|
||||||
|
let sourceTable = column.entityDisplayConfig.sourceTable;
|
||||||
|
let joinTable = column.entityDisplayConfig.joinTable;
|
||||||
|
|
||||||
|
if (!sourceTable && column.entityJoinInfo) {
|
||||||
|
sourceTable = column.entityJoinInfo.sourceTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!joinTable) {
|
||||||
|
// joinTable이 없으면 tableTypeApi로 조회해서 설정
|
||||||
|
try {
|
||||||
|
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
|
||||||
|
const columnList = await tableTypeApi.getColumns(sourceTable);
|
||||||
|
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
||||||
|
|
||||||
|
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
||||||
|
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
||||||
|
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable);
|
||||||
|
|
||||||
|
// entityDisplayConfig 업데이트
|
||||||
|
const updatedConfig = {
|
||||||
|
...column.entityDisplayConfig,
|
||||||
|
sourceTable: sourceTable,
|
||||||
|
joinTable: joinTable,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
const updatedColumns = config.columns?.map((col) =>
|
||||||
|
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedColumns) {
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
|
||||||
|
const configKey = `${column.columnName}`;
|
||||||
|
|
||||||
|
// 이미 로드된 경우 스킵
|
||||||
|
if (entityDisplayConfigs[configKey]) return;
|
||||||
|
|
||||||
|
// joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기
|
||||||
|
let actualJoinTable = joinTable;
|
||||||
|
if (!actualJoinTable && sourceTable) {
|
||||||
|
try {
|
||||||
|
console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", {
|
||||||
|
tableName: sourceTable,
|
||||||
|
columnName: column.columnName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnList = await tableTypeApi.getColumns(sourceTable);
|
||||||
|
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
||||||
|
|
||||||
|
console.log("🔍 컬럼 정보 조회 결과:", {
|
||||||
|
columnInfo: columnInfo,
|
||||||
|
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
|
||||||
|
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
||||||
|
actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
||||||
|
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable);
|
||||||
|
|
||||||
|
// entityDisplayConfig 업데이트
|
||||||
|
const updatedConfig = {
|
||||||
|
...column.entityDisplayConfig,
|
||||||
|
joinTable: actualJoinTable,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
const updatedColumns = config.columns?.map((col) =>
|
||||||
|
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedColumns) {
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", {
|
||||||
|
columnName: column.columnName,
|
||||||
|
columnInfo: columnInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceTable과 joinTable이 모두 있어야 로드
|
||||||
|
if (!sourceTable || !actualJoinTable) {
|
||||||
|
console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
|
||||||
|
const [sourceResult, joinResult] = await Promise.all([
|
||||||
|
entityJoinApi.getReferenceTableColumns(sourceTable),
|
||||||
|
entityJoinApi.getReferenceTableColumns(actualJoinTable),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sourceColumns = sourceResult.columns || [];
|
||||||
|
const joinColumns = joinResult.columns || [];
|
||||||
|
|
||||||
|
setEntityDisplayConfigs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[configKey]: {
|
||||||
|
sourceColumns,
|
||||||
|
joinColumns,
|
||||||
|
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
||||||
|
separator: column.entityDisplayConfig?.separator || " - ",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 엔티티 표시 컬럼 선택 토글
|
||||||
|
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
|
||||||
|
const configKey = `${columnName}`;
|
||||||
|
const localConfig = entityDisplayConfigs[configKey];
|
||||||
|
if (!localConfig) return;
|
||||||
|
|
||||||
|
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
|
||||||
|
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
|
||||||
|
: [...localConfig.selectedColumns, selectedColumn];
|
||||||
|
|
||||||
|
// 로컬 상태 업데이트
|
||||||
|
setEntityDisplayConfigs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[configKey]: {
|
||||||
|
...prev[configKey],
|
||||||
|
selectedColumns: newSelectedColumns,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 실제 컬럼 설정도 업데이트
|
||||||
|
const updatedColumns = config.columns?.map((col) => {
|
||||||
|
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
entityDisplayConfig: {
|
||||||
|
...col.entityDisplayConfig,
|
||||||
|
displayColumns: newSelectedColumns,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedColumns) {
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", {
|
||||||
|
columnName,
|
||||||
|
selectedColumns: newSelectedColumns,
|
||||||
|
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 엔티티 표시 구분자 업데이트
|
||||||
|
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
|
||||||
|
const configKey = `${columnName}`;
|
||||||
|
const localConfig = entityDisplayConfigs[configKey];
|
||||||
|
if (!localConfig) return;
|
||||||
|
|
||||||
|
// 로컬 상태 업데이트
|
||||||
|
setEntityDisplayConfigs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[configKey]: {
|
||||||
|
...prev[configKey],
|
||||||
|
separator,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 실제 컬럼 설정도 업데이트
|
||||||
|
const updatedColumns = config.columns?.map((col) => {
|
||||||
|
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
entityDisplayConfig: {
|
||||||
|
...col.entityDisplayConfig,
|
||||||
|
separator,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedColumns) {
|
||||||
|
handleChange("columns", updatedColumns);
|
||||||
|
console.log("🎯 엔티티 표시 구분자 설정 업데이트:", {
|
||||||
|
columnName,
|
||||||
|
separator,
|
||||||
|
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 컬럼 순서 변경
|
// 컬럼 순서 변경
|
||||||
const moveColumn = (columnName: string, direction: "up" | "down") => {
|
const moveColumn = (columnName: string, direction: "up" | "down") => {
|
||||||
const columns = [...(config.columns || [])];
|
const columns = [...(config.columns || [])];
|
||||||
|
|
@ -296,7 +635,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
if (!column) return;
|
if (!column) return;
|
||||||
|
|
||||||
// tableColumns에서 해당 컬럼의 메타정보 찾기
|
// tableColumns에서 해당 컬럼의 메타정보 찾기
|
||||||
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName || tc.column_name === columnName);
|
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName);
|
||||||
|
|
||||||
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
|
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
|
||||||
const inferWidgetType = (dataType: string, webType?: string): string => {
|
const inferWidgetType = (dataType: string, webType?: string): string => {
|
||||||
|
|
@ -690,6 +1029,135 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
{/* 컬럼 설정 탭 */}
|
{/* 컬럼 설정 탭 */}
|
||||||
<TabsContent value="columns" className="space-y-4">
|
<TabsContent value="columns" className="space-y-4">
|
||||||
<ScrollArea className="h-[600px] pr-4">
|
<ScrollArea className="h-[600px] pr-4">
|
||||||
|
{/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */}
|
||||||
|
{config.columns?.some((col) => col.isEntityJoin) && (
|
||||||
|
<Card className="border-l-4 border-l-orange-500">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">🎯 엔티티 컬럼 표시 설정</CardTitle>
|
||||||
|
<CardDescription>엔티티 타입 컬럼의 표시할 컬럼들을 조합하여 설정하세요</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{config.columns
|
||||||
|
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
|
||||||
|
.map((column) => (
|
||||||
|
<div key={column.columnName} className="space-y-3 rounded-lg border bg-orange-50 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
||||||
|
{column.columnName}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm font-medium">{column.displayName}</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => loadEntityDisplayConfig(column)}
|
||||||
|
className="h-6 text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
컬럼 로드
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{entityDisplayConfigs[column.columnName] && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 구분자 설정 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">구분자</Label>
|
||||||
|
<Input
|
||||||
|
value={entityDisplayConfigs[column.columnName].separator}
|
||||||
|
onChange={(e) => updateEntityDisplaySeparator(column.columnName, e.target.value)}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
placeholder=" - "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 기본 테이블 컬럼 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-blue-600">
|
||||||
|
기본 테이블: {column.entityDisplayConfig?.sourceTable}
|
||||||
|
</Label>
|
||||||
|
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
||||||
|
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
|
||||||
|
<div key={col.columnName} className="flex items-center space-x-1">
|
||||||
|
<Checkbox
|
||||||
|
id={`source-${column.columnName}-${col.columnName}`}
|
||||||
|
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
||||||
|
col.columnName,
|
||||||
|
)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
||||||
|
}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`source-${column.columnName}-${col.columnName}`}
|
||||||
|
className="flex-1 cursor-pointer text-xs"
|
||||||
|
>
|
||||||
|
{col.displayName}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 조인 테이블 컬럼 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-green-600">
|
||||||
|
조인 테이블: {column.entityDisplayConfig?.joinTable}
|
||||||
|
</Label>
|
||||||
|
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
||||||
|
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
|
||||||
|
<div key={col.columnName} className="flex items-center space-x-1">
|
||||||
|
<Checkbox
|
||||||
|
id={`join-${column.columnName}-${col.columnName}`}
|
||||||
|
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
||||||
|
col.columnName,
|
||||||
|
)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
||||||
|
}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`join-${column.columnName}-${col.columnName}`}
|
||||||
|
className="flex-1 cursor-pointer text-xs"
|
||||||
|
>
|
||||||
|
{col.displayName}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 선택된 컬럼 미리보기 */}
|
||||||
|
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">미리보기</Label>
|
||||||
|
<div className="flex flex-wrap gap-1 rounded bg-gray-50 p-2 text-xs">
|
||||||
|
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
|
||||||
|
<React.Fragment key={colName}>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{colName}
|
||||||
|
</Badge>
|
||||||
|
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{entityDisplayConfigs[column.columnName].separator}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{!screenTableName ? (
|
{!screenTableName ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
|
|
@ -820,6 +1288,20 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 엔티티 타입 컬럼 표시 */}
|
||||||
|
{column.isEntityJoin && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="flex items-center gap-2 rounded bg-orange-50 p-2">
|
||||||
|
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
||||||
|
엔티티 타입
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-orange-600">
|
||||||
|
표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">정렬</Label>
|
<Label className="text-xs">정렬</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -1018,7 +1500,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => addEntityJoinColumn(matchingJoinColumn)}
|
onClick={() => addEntityColumn(matchingJoinColumn)}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
|
@ -1057,7 +1539,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
key={index}
|
key={index}
|
||||||
variant={isAlreadyAdded ? "secondary" : "outline"}
|
variant={isAlreadyAdded ? "secondary" : "outline"}
|
||||||
className="cursor-pointer text-xs"
|
className="cursor-pointer text-xs"
|
||||||
onClick={() => !isAlreadyAdded && addEntityJoinColumn(column)}
|
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
|
||||||
>
|
>
|
||||||
{column.columnLabel}
|
{column.columnLabel}
|
||||||
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
|
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ export interface ColumnConfig {
|
||||||
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
|
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
|
||||||
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
|
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
|
||||||
|
|
||||||
|
// 🎯 엔티티 컬럼 표시 설정 (화면별 동적 설정)
|
||||||
|
entityDisplayConfig?: {
|
||||||
|
displayColumns: string[]; // 표시할 컬럼들 (기본 테이블 + 조인 테이블)
|
||||||
|
separator?: string; // 구분자 (기본: " - ")
|
||||||
|
sourceTable?: string; // 기본 테이블명
|
||||||
|
joinTable?: string; // 조인 테이블명
|
||||||
|
};
|
||||||
|
|
||||||
// 컬럼 고정 관련 속성
|
// 컬럼 고정 관련 속성
|
||||||
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
|
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
|
||||||
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
|
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
|
||||||
|
|
|
||||||
|
|
@ -192,10 +192,13 @@ export interface FileTypeConfig {
|
||||||
export interface EntityTypeConfig {
|
export interface EntityTypeConfig {
|
||||||
referenceTable: string;
|
referenceTable: string;
|
||||||
referenceColumn: string;
|
referenceColumn: string;
|
||||||
displayColumn: string;
|
displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경
|
||||||
|
displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated)
|
||||||
searchColumns?: string[];
|
searchColumns?: string[];
|
||||||
filters?: Record<string, unknown>;
|
filters?: Record<string, unknown>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
||||||
|
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export interface UnifiedColumnInfo {
|
||||||
|
|
||||||
// 입력 설정
|
// 입력 설정
|
||||||
inputType: "direct" | "auto";
|
inputType: "direct" | "auto";
|
||||||
|
input_type?: string; // 🎯 데이터베이스의 input_type 필드 (entity, text, number 등)
|
||||||
detailSettings?: Record<string, unknown>; // JSON 파싱된 객체
|
detailSettings?: Record<string, unknown>; // JSON 파싱된 객체
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue