feat: Enhance entity join functionality with company code support
- Updated the EntityJoinController to log the company code during entity join configuration retrieval. - Modified the entityJoinService to accept company code as a parameter, allowing for company-specific entity join detection. - Enhanced the TableManagementService to pass the company code when detecting entity joins and retrieving reference table columns. - Implemented a helper function in the SplitPanelLayoutComponent to extract additional join columns based on the entity join configuration. - Improved the SplitPanelLayoutConfigPanel to display entity join columns dynamically, enhancing user experience and functionality.
This commit is contained in:
parent
9e1a54c738
commit
3c8c2ebcf4
|
|
@ -193,10 +193,11 @@ export class EntityJoinController {
|
||||||
async getEntityJoinConfigs(req: Request, res: Response): Promise<void> {
|
async getEntityJoinConfigs(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
|
const companyCode = (req as any).user?.companyCode;
|
||||||
|
|
||||||
logger.info(`Entity 조인 설정 조회: ${tableName}`);
|
logger.info(`Entity 조인 설정 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||||
|
|
||||||
const joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
const joinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -224,11 +225,12 @@ export class EntityJoinController {
|
||||||
async getReferenceTableColumns(req: Request, res: Response): Promise<void> {
|
async getReferenceTableColumns(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
|
const companyCode = (req as any).user?.companyCode;
|
||||||
|
|
||||||
logger.info(`참조 테이블 컬럼 조회: ${tableName}`);
|
logger.info(`참조 테이블 컬럼 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||||
|
|
||||||
const columns =
|
const columns =
|
||||||
await tableManagementService.getReferenceTableColumns(tableName);
|
await tableManagementService.getReferenceTableColumns(tableName, companyCode);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -408,11 +410,12 @@ export class EntityJoinController {
|
||||||
async getEntityJoinColumns(req: Request, res: Response): Promise<void> {
|
async getEntityJoinColumns(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
|
const companyCode = (req as any).user?.companyCode;
|
||||||
|
|
||||||
logger.info(`Entity 조인 컬럼 조회: ${tableName}`);
|
logger.info(`Entity 조인 컬럼 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||||
|
|
||||||
// 1. 현재 테이블의 Entity 조인 설정 조회
|
// 1. 현재 테이블의 Entity 조인 설정 조회
|
||||||
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
||||||
|
|
||||||
// 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외
|
// 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외
|
||||||
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
|
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
|
||||||
|
|
@ -439,7 +442,7 @@ export class EntityJoinController {
|
||||||
try {
|
try {
|
||||||
const columns =
|
const columns =
|
||||||
await tableManagementService.getReferenceTableColumns(
|
await tableManagementService.getReferenceTableColumns(
|
||||||
config.referenceTable
|
config.referenceTable, companyCode
|
||||||
);
|
);
|
||||||
|
|
||||||
// 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
|
// 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,18 @@ export class EntityJoinService {
|
||||||
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
||||||
* @param tableName 테이블명
|
* @param tableName 테이블명
|
||||||
* @param screenEntityConfigs 화면별 엔티티 설정 (선택사항)
|
* @param screenEntityConfigs 화면별 엔티티 설정 (선택사항)
|
||||||
|
* @param companyCode 회사코드 (회사별 설정 우선, 없으면 전체 조회)
|
||||||
*/
|
*/
|
||||||
async detectEntityJoins(
|
async detectEntityJoins(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
screenEntityConfigs?: Record<string, any>
|
screenEntityConfigs?: Record<string, any>,
|
||||||
|
companyCode?: string
|
||||||
): Promise<EntityJoinConfig[]> {
|
): Promise<EntityJoinConfig[]> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
logger.info(`Entity 컬럼 감지 시작: ${tableName} (companyCode: ${companyCode || 'all'})`);
|
||||||
|
|
||||||
// table_type_columns에서 entity 및 category 타입인 컬럼들 조회
|
// table_type_columns에서 entity 및 category 타입인 컬럼들 조회
|
||||||
// company_code = '*' (공통 설정) 우선 조회
|
// 회사코드가 있으면 해당 회사 + '*' 만 조회, 회사별 우선
|
||||||
const entityColumns = await query<{
|
const entityColumns = await query<{
|
||||||
column_name: string;
|
column_name: string;
|
||||||
input_type: string;
|
input_type: string;
|
||||||
|
|
@ -33,14 +35,17 @@ export class EntityJoinService {
|
||||||
reference_column: string;
|
reference_column: string;
|
||||||
display_column: string | null;
|
display_column: string | null;
|
||||||
}>(
|
}>(
|
||||||
`SELECT column_name, input_type, reference_table, reference_column, display_column
|
`SELECT DISTINCT ON (column_name)
|
||||||
|
column_name, input_type, reference_table, reference_column, display_column
|
||||||
FROM table_type_columns
|
FROM table_type_columns
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND input_type IN ('entity', 'category')
|
AND input_type IN ('entity', 'category')
|
||||||
AND company_code = '*'
|
|
||||||
AND reference_table IS NOT NULL
|
AND reference_table IS NOT NULL
|
||||||
AND reference_table != ''`,
|
AND reference_table != ''
|
||||||
[tableName]
|
${companyCode ? `AND company_code IN ($2, '*')` : ''}
|
||||||
|
ORDER BY column_name,
|
||||||
|
CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||||
|
companyCode ? [tableName, companyCode] : [tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
||||||
|
|
@ -272,7 +277,8 @@ export class EntityJoinService {
|
||||||
orderBy: string = "",
|
orderBy: string = "",
|
||||||
limit?: number,
|
limit?: number,
|
||||||
offset?: number,
|
offset?: number,
|
||||||
columnTypes?: Map<string, string> // 컬럼명 → 데이터 타입 매핑
|
columnTypes?: Map<string, string>, // 컬럼명 → 데이터 타입 매핑
|
||||||
|
referenceTableColumns?: Map<string, string[]> // 🆕 참조 테이블별 전체 컬럼 목록
|
||||||
): { query: string; aliasMap: Map<string, string> } {
|
): { query: string; aliasMap: Map<string, string> } {
|
||||||
try {
|
try {
|
||||||
// 기본 SELECT 컬럼들 (날짜는 YYYY-MM-DD 형식, 나머지는 TEXT 캐스팅)
|
// 기본 SELECT 컬럼들 (날짜는 YYYY-MM-DD 형식, 나머지는 TEXT 캐스팅)
|
||||||
|
|
@ -338,115 +344,100 @@ export class EntityJoinService {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔧 _label 별칭 중복 방지를 위한 Set
|
// 🔧 생성된 별칭 중복 방지를 위한 Set
|
||||||
// 같은 sourceColumn에서 여러 조인 설정이 있을 때 _label은 첫 번째만 생성
|
const generatedAliases = new Set<string>();
|
||||||
const generatedLabelAliases = new Set<string>();
|
|
||||||
|
|
||||||
const joinColumns = joinConfigs
|
const joinColumns = uniqueReferenceTableConfigs
|
||||||
.map((config) => {
|
.map((config) => {
|
||||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
const alias = aliasMap.get(aliasKey);
|
const alias = aliasMap.get(aliasKey);
|
||||||
const displayColumns = config.displayColumns || [
|
|
||||||
config.displayColumn,
|
|
||||||
];
|
|
||||||
const separator = config.separator || " - ";
|
|
||||||
|
|
||||||
// 결과 컬럼 배열 (aliasColumn + _label 필드)
|
|
||||||
const resultColumns: string[] = [];
|
const resultColumns: string[] = [];
|
||||||
|
|
||||||
if (displayColumns.length === 0 || !displayColumns[0]) {
|
// 🆕 참조 테이블의 전체 컬럼 목록이 있으면 모든 컬럼을 SELECT
|
||||||
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
|
const refTableCols = referenceTableColumns?.get(
|
||||||
// 조인 테이블의 referenceColumn을 기본값으로 사용
|
`${config.referenceTable}:${config.sourceColumn}`
|
||||||
resultColumns.push(
|
) || referenceTableColumns?.get(config.referenceTable);
|
||||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`
|
|
||||||
);
|
|
||||||
} else if (displayColumns.length === 1) {
|
|
||||||
// 단일 컬럼인 경우
|
|
||||||
const col = displayColumns[0];
|
|
||||||
|
|
||||||
// ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴
|
if (refTableCols && refTableCols.length > 0) {
|
||||||
// 이렇게 하면 item_info.size, item_info.material 등 모든 조인 테이블 컬럼 지원
|
// 메타 컬럼은 제외 (메인 테이블과 중복되거나 불필요)
|
||||||
const isJoinTableColumn =
|
const skipColumns = new Set(["company_code", "created_date", "updated_date", "writer"]);
|
||||||
config.referenceTable && config.referenceTable !== tableName;
|
|
||||||
|
for (const col of refTableCols) {
|
||||||
|
if (skipColumns.has(col)) continue;
|
||||||
|
|
||||||
|
const colAlias = `${config.sourceColumn}_${col}`;
|
||||||
|
if (generatedAliases.has(colAlias)) continue;
|
||||||
|
|
||||||
if (isJoinTableColumn) {
|
|
||||||
resultColumns.push(
|
resultColumns.push(
|
||||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`
|
`COALESCE(${alias}."${col}"::TEXT, '') AS "${colAlias}"`
|
||||||
);
|
);
|
||||||
|
generatedAliases.add(colAlias);
|
||||||
|
}
|
||||||
|
|
||||||
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
|
// _label 필드도 추가 (기존 호환성)
|
||||||
// sourceColumn_label 형식으로 추가
|
const labelAlias = `${config.sourceColumn}_label`;
|
||||||
// 🔧 중복 방지: 같은 sourceColumn에서 _label은 첫 번째만 생성
|
if (!generatedAliases.has(labelAlias)) {
|
||||||
const labelAlias = `${config.sourceColumn}_label`;
|
// 표시용 컬럼 자동 감지: *_name > name > label > referenceColumn
|
||||||
if (!generatedLabelAliases.has(labelAlias)) {
|
const nameCol = refTableCols.find((c) => c.endsWith("_name") && c !== "company_name");
|
||||||
resultColumns.push(
|
const displayCol = nameCol || refTableCols.find((c) => c === "name") || config.referenceColumn;
|
||||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}`
|
|
||||||
);
|
|
||||||
generatedLabelAliases.add(labelAlias);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 referenceColumn (PK)도 항상 SELECT (parentDataMapping용)
|
|
||||||
// 예: customer_code, item_number 등
|
|
||||||
// col과 동일해도 별도의 alias로 추가 (customer_code as customer_code)
|
|
||||||
// 🔧 중복 방지: referenceColumn도 한 번만 추가
|
|
||||||
const refColAlias = config.referenceColumn;
|
|
||||||
if (!generatedLabelAliases.has(refColAlias)) {
|
|
||||||
resultColumns.push(
|
|
||||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}`
|
|
||||||
);
|
|
||||||
generatedLabelAliases.add(refColAlias);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resultColumns.push(
|
resultColumns.push(
|
||||||
`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`
|
`COALESCE(${alias}."${displayCol}"::TEXT, '') AS "${labelAlias}"`
|
||||||
);
|
);
|
||||||
|
generatedAliases.add(labelAlias);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음)
|
// 🔄 기존 로직 (참조 테이블 컬럼 목록이 없는 경우 - fallback)
|
||||||
// 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price)
|
const displayColumns = config.displayColumns || [config.displayColumn];
|
||||||
displayColumns.forEach((col) => {
|
|
||||||
|
if (displayColumns.length === 0 || !displayColumns[0]) {
|
||||||
|
resultColumns.push(
|
||||||
|
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`
|
||||||
|
);
|
||||||
|
} else if (displayColumns.length === 1) {
|
||||||
|
const col = displayColumns[0];
|
||||||
const isJoinTableColumn =
|
const isJoinTableColumn =
|
||||||
config.referenceTable && config.referenceTable !== tableName;
|
config.referenceTable && config.referenceTable !== tableName;
|
||||||
|
|
||||||
const individualAlias = `${config.sourceColumn}_${col}`;
|
|
||||||
|
|
||||||
// 🔧 중복 방지: 같은 alias가 이미 생성되었으면 스킵
|
|
||||||
if (generatedLabelAliases.has(individualAlias)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isJoinTableColumn) {
|
if (isJoinTableColumn) {
|
||||||
// 조인 테이블 컬럼은 조인 별칭 사용
|
|
||||||
resultColumns.push(
|
resultColumns.push(
|
||||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
|
`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||||
);
|
);
|
||||||
|
const labelAlias = `${config.sourceColumn}_label`;
|
||||||
|
if (!generatedAliases.has(labelAlias)) {
|
||||||
|
resultColumns.push(
|
||||||
|
`COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}`
|
||||||
|
);
|
||||||
|
generatedAliases.add(labelAlias);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 기본 테이블 컬럼은 main 별칭 사용
|
|
||||||
resultColumns.push(
|
resultColumns.push(
|
||||||
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
|
`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
generatedLabelAliases.add(individualAlias);
|
} else {
|
||||||
});
|
displayColumns.forEach((col) => {
|
||||||
|
const isJoinTableColumn =
|
||||||
|
config.referenceTable && config.referenceTable !== tableName;
|
||||||
|
const individualAlias = `${config.sourceColumn}_${col}`;
|
||||||
|
if (generatedAliases.has(individualAlias)) return;
|
||||||
|
|
||||||
// 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용)
|
if (isJoinTableColumn) {
|
||||||
const isJoinTableColumn =
|
resultColumns.push(
|
||||||
config.referenceTable && config.referenceTable !== tableName;
|
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
|
||||||
if (
|
);
|
||||||
isJoinTableColumn &&
|
} else {
|
||||||
!displayColumns.includes(config.referenceColumn) &&
|
resultColumns.push(
|
||||||
!generatedLabelAliases.has(config.referenceColumn) // 🔧 중복 방지
|
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
|
||||||
) {
|
);
|
||||||
resultColumns.push(
|
}
|
||||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}`
|
generatedAliases.add(individualAlias);
|
||||||
);
|
});
|
||||||
generatedLabelAliases.add(config.referenceColumn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 resultColumns를 반환
|
|
||||||
return resultColumns.join(", ");
|
return resultColumns.join(", ");
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
// SELECT 절 구성
|
// SELECT 절 구성
|
||||||
|
|
@ -725,7 +716,7 @@ export class EntityJoinService {
|
||||||
/**
|
/**
|
||||||
* 참조 테이블의 컬럼 목록 조회 (UI용)
|
* 참조 테이블의 컬럼 목록 조회 (UI용)
|
||||||
*/
|
*/
|
||||||
async getReferenceTableColumns(tableName: string): Promise<
|
async getReferenceTableColumns(tableName: string, companyCode?: string): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
columnName: string;
|
columnName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
@ -750,16 +741,19 @@ export class EntityJoinService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. table_type_columns 테이블에서 라벨과 input_type 정보 조회
|
// 2. table_type_columns 테이블에서 라벨과 input_type 정보 조회
|
||||||
|
// 회사코드가 있으면 해당 회사 + '*' 만, 회사별 우선
|
||||||
const columnLabels = await query<{
|
const columnLabels = await query<{
|
||||||
column_name: string;
|
column_name: string;
|
||||||
column_label: string | null;
|
column_label: string | null;
|
||||||
input_type: string | null;
|
input_type: string | null;
|
||||||
}>(
|
}>(
|
||||||
`SELECT column_name, column_label, input_type
|
`SELECT DISTINCT ON (column_name) column_name, column_label, input_type
|
||||||
FROM table_type_columns
|
FROM table_type_columns
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND company_code = '*'`,
|
${companyCode ? `AND company_code IN ($2, '*')` : ''}
|
||||||
[tableName]
|
ORDER BY column_name,
|
||||||
|
CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||||
|
companyCode ? [tableName, companyCode] : [tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. 라벨 및 inputType 정보를 맵으로 변환
|
// 3. 라벨 및 inputType 정보를 맵으로 변환
|
||||||
|
|
|
||||||
|
|
@ -2875,10 +2875,11 @@ export class TableManagementService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
|
// Entity 조인 설정 감지 (화면별 엔티티 설정 + 회사코드 전달)
|
||||||
let joinConfigs = await entityJoinService.detectEntityJoins(
|
let joinConfigs = await entityJoinService.detectEntityJoins(
|
||||||
tableName,
|
tableName,
|
||||||
options.screenEntityConfigs
|
options.screenEntityConfigs,
|
||||||
|
options.companyCode
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -3258,6 +3259,28 @@ export class TableManagementService {
|
||||||
startTime: number
|
startTime: number
|
||||||
): Promise<EntityJoinResponse> {
|
): Promise<EntityJoinResponse> {
|
||||||
try {
|
try {
|
||||||
|
// 🆕 참조 테이블별 전체 컬럼 목록 미리 조회
|
||||||
|
const referenceTableColumns = new Map<string, string[]>();
|
||||||
|
const uniqueRefTables = new Set(
|
||||||
|
joinConfigs
|
||||||
|
.filter((c) => c.referenceTable !== "table_column_category_values") // 카테고리는 제외
|
||||||
|
.map((c) => `${c.referenceTable}:${c.sourceColumn}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const key of uniqueRefTables) {
|
||||||
|
const refTable = key.split(":")[0];
|
||||||
|
if (!referenceTableColumns.has(key)) {
|
||||||
|
const cols = await query<{ column_name: string }>(
|
||||||
|
`SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = $1 AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position`,
|
||||||
|
[refTable]
|
||||||
|
);
|
||||||
|
referenceTableColumns.set(key, cols.map((c) => c.column_name));
|
||||||
|
logger.info(`🔍 참조 테이블 컬럼 조회: ${refTable} → ${cols.length}개`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 데이터 조회 쿼리
|
// 데이터 조회 쿼리
|
||||||
const dataQuery = entityJoinService.buildJoinQuery(
|
const dataQuery = entityJoinService.buildJoinQuery(
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -3266,7 +3289,9 @@ export class TableManagementService {
|
||||||
whereClause,
|
whereClause,
|
||||||
orderBy,
|
orderBy,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
undefined,
|
||||||
|
referenceTableColumns // 🆕 참조 테이블 전체 컬럼 전달
|
||||||
).query;
|
).query;
|
||||||
|
|
||||||
// 카운트 쿼리
|
// 카운트 쿼리
|
||||||
|
|
@ -3767,12 +3792,12 @@ export class TableManagementService {
|
||||||
reference_table: string;
|
reference_table: string;
|
||||||
reference_column: string;
|
reference_column: string;
|
||||||
}>(
|
}>(
|
||||||
`SELECT column_name, reference_table, reference_column
|
`SELECT DISTINCT ON (column_name) column_name, reference_table, reference_column
|
||||||
FROM table_type_columns
|
FROM table_type_columns
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND input_type = 'entity'
|
AND input_type = 'entity'
|
||||||
AND reference_table = $2
|
AND reference_table = $2
|
||||||
AND company_code = '*'
|
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[tableName, refTable]
|
[tableName, refTable]
|
||||||
);
|
);
|
||||||
|
|
@ -3883,7 +3908,7 @@ export class TableManagementService {
|
||||||
/**
|
/**
|
||||||
* 참조 테이블의 표시 컬럼 목록 조회
|
* 참조 테이블의 표시 컬럼 목록 조회
|
||||||
*/
|
*/
|
||||||
async getReferenceTableColumns(tableName: string): Promise<
|
async getReferenceTableColumns(tableName: string, companyCode?: string): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
columnName: string;
|
columnName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
@ -3891,7 +3916,7 @@ export class TableManagementService {
|
||||||
inputType?: string;
|
inputType?: string;
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
return await entityJoinService.getReferenceTableColumns(tableName);
|
return await entityJoinService.getReferenceTableColumns(tableName, companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -5005,14 +5030,14 @@ export class TableManagementService {
|
||||||
input_type: string;
|
input_type: string;
|
||||||
display_column: string | null;
|
display_column: string | null;
|
||||||
}>(
|
}>(
|
||||||
`SELECT column_name, reference_column, input_type, display_column
|
`SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
|
||||||
FROM table_type_columns
|
FROM table_type_columns
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND input_type IN ('entity', 'category')
|
AND input_type IN ('entity', 'category')
|
||||||
AND reference_table = $2
|
AND reference_table = $2
|
||||||
AND reference_column IS NOT NULL
|
AND reference_column IS NOT NULL
|
||||||
AND reference_column != ''
|
AND reference_column != ''
|
||||||
AND company_code = '*'`,
|
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||||
[rightTable, leftTable]
|
[rightTable, leftTable]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -5034,14 +5059,14 @@ export class TableManagementService {
|
||||||
input_type: string;
|
input_type: string;
|
||||||
display_column: string | null;
|
display_column: string | null;
|
||||||
}>(
|
}>(
|
||||||
`SELECT column_name, reference_column, input_type, display_column
|
`SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
|
||||||
FROM table_type_columns
|
FROM table_type_columns
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND input_type IN ('entity', 'category')
|
AND input_type IN ('entity', 'category')
|
||||||
AND reference_table = $2
|
AND reference_table = $2
|
||||||
AND reference_column IS NOT NULL
|
AND reference_column IS NOT NULL
|
||||||
AND reference_column != ''
|
AND reference_column != ''
|
||||||
AND company_code = '*'`,
|
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||||
[leftTable, rightTable]
|
[leftTable, rightTable]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -957,6 +957,67 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
[formatDateValue, formatNumberValue],
|
[formatDateValue, formatNumberValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🆕 패널 config의 columns에서 additionalJoinColumns 추출하는 헬퍼
|
||||||
|
const extractAdditionalJoinColumns = useCallback((columns: any[] | undefined, tableName: string) => {
|
||||||
|
if (!columns || columns.length === 0) return undefined;
|
||||||
|
|
||||||
|
const joinColumns: Array<{
|
||||||
|
sourceTable: string;
|
||||||
|
sourceColumn: string;
|
||||||
|
referenceTable: string;
|
||||||
|
joinAlias: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
columns.forEach((col: any) => {
|
||||||
|
// 방법 1: isEntityJoin 플래그가 있는 경우 (설정 패널에서 Entity 조인 컬럼으로 추가한 경우)
|
||||||
|
if (col.isEntityJoin && col.joinInfo) {
|
||||||
|
const existing = joinColumns.find(
|
||||||
|
(j) => j.referenceTable === col.joinInfo.referenceTable && j.joinAlias === col.joinInfo.joinAlias
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
joinColumns.push({
|
||||||
|
sourceTable: col.joinInfo.sourceTable || tableName,
|
||||||
|
sourceColumn: col.joinInfo.sourceColumn,
|
||||||
|
referenceTable: col.joinInfo.referenceTable,
|
||||||
|
joinAlias: col.joinInfo.joinAlias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방법 2: "테이블명.컬럼명" 형식 (기존 좌측 패널 방식)
|
||||||
|
const colName = typeof col === "string" ? col : col.name || col.columnName;
|
||||||
|
if (colName && colName.includes(".")) {
|
||||||
|
const [refTable, refColumn] = colName.split(".");
|
||||||
|
const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
|
||||||
|
const existing = joinColumns.find(
|
||||||
|
(j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn
|
||||||
|
);
|
||||||
|
if (!existing) {
|
||||||
|
joinColumns.push({
|
||||||
|
sourceTable: tableName,
|
||||||
|
sourceColumn: inferredSourceColumn,
|
||||||
|
referenceTable: refTable,
|
||||||
|
joinAlias: `${inferredSourceColumn}_${refColumn}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 이미 추가된 테이블이면 별칭만 추가
|
||||||
|
const newAlias = `${inferredSourceColumn}_${refColumn}`;
|
||||||
|
if (!joinColumns.find((j) => j.joinAlias === newAlias)) {
|
||||||
|
joinColumns.push({
|
||||||
|
sourceTable: tableName,
|
||||||
|
sourceColumn: inferredSourceColumn,
|
||||||
|
referenceTable: refTable,
|
||||||
|
joinAlias: newAlias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return joinColumns.length > 0 ? joinColumns : undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 좌측 데이터 로드
|
// 좌측 데이터 로드
|
||||||
const loadLeftData = useCallback(async () => {
|
const loadLeftData = useCallback(async () => {
|
||||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||||
|
|
@ -967,74 +1028,22 @@ 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로 변환
|
// 🆕 좌측 패널 config의 Entity 조인 컬럼 추출 (헬퍼 함수 사용)
|
||||||
const configuredColumns = componentConfig.leftPanel?.columns || [];
|
const leftJoinColumns = extractAdditionalJoinColumns(
|
||||||
const additionalJoinColumns: Array<{
|
componentConfig.leftPanel?.columns,
|
||||||
sourceTable: string;
|
leftTableName,
|
||||||
sourceColumn: string;
|
);
|
||||||
referenceTable: string;
|
|
||||||
joinAlias: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
// 소스 컬럼 매핑 (item_info → item_code, warehouse_info → warehouse_id 등)
|
console.log("🔗 [분할패널] 좌측 additionalJoinColumns:", leftJoinColumns);
|
||||||
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 또는 warehouse_info → warehouse_id)
|
|
||||||
// 기본: _info → _code, 백업: _info → _id
|
|
||||||
const primarySourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
|
|
||||||
const secondarySourceColumn = refTable.replace("_info", "_id").replace("_mng", "_id");
|
|
||||||
// 실제 존재하는 소스 컬럼은 백엔드에서 결정 (프론트엔드는 두 패턴 모두 전달)
|
|
||||||
const inferredSourceColumn = primarySourceColumn;
|
|
||||||
|
|
||||||
// 이미 추가된 조인인지 확인 (동일 테이블, 동일 소스컬럼)
|
|
||||||
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, // 🆕 추가 조인 컬럼
|
additionalJoinColumns: leftJoinColumns,
|
||||||
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
|
companyCodeOverride: companyCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔍 디버깅: API 응답 데이터의 키 확인
|
// 🔍 디버깅: API 응답 데이터의 키 확인
|
||||||
|
|
@ -1093,11 +1102,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
|
|
||||||
// 🆕 엔티티 조인 API 사용
|
// 🆕 엔티티 조인 API 사용
|
||||||
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
|
const rightDetailJoinColumns = extractAdditionalJoinColumns(
|
||||||
|
componentConfig.rightPanel?.columns,
|
||||||
|
rightTableName,
|
||||||
|
);
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
||||||
search: { id: primaryKey },
|
search: { id: primaryKey },
|
||||||
enableEntityJoin: true, // 엔티티 조인 활성화
|
enableEntityJoin: true,
|
||||||
size: 1,
|
size: 1,
|
||||||
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
|
companyCodeOverride: companyCode,
|
||||||
|
additionalJoinColumns: rightDetailJoinColumns, // 🆕 Entity 조인 컬럼 전달
|
||||||
});
|
});
|
||||||
|
|
||||||
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
|
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
|
||||||
|
|
@ -1141,6 +1155,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
const allResults: any[] = [];
|
const allResults: any[] = [];
|
||||||
|
|
||||||
|
// 🆕 우측 패널 Entity 조인 컬럼 추출 (그룹 합산용)
|
||||||
|
const rightJoinColumnsForGroup = extractAdditionalJoinColumns(
|
||||||
|
componentConfig.rightPanel?.columns,
|
||||||
|
rightTableName,
|
||||||
|
);
|
||||||
|
|
||||||
// 각 원본 항목에 대해 조회
|
// 각 원본 항목에 대해 조회
|
||||||
for (const originalItem of leftItem._originalItems) {
|
for (const originalItem of leftItem._originalItems) {
|
||||||
const searchConditions: Record<string, any> = {};
|
const searchConditions: Record<string, any> = {};
|
||||||
|
|
@ -1155,7 +1175,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
search: searchConditions,
|
search: searchConditions,
|
||||||
enableEntityJoin: true,
|
enableEntityJoin: true,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
|
companyCodeOverride: companyCode,
|
||||||
|
additionalJoinColumns: rightJoinColumnsForGroup, // 🆕 Entity 조인 컬럼 전달
|
||||||
});
|
});
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
allResults.push(...result.data);
|
allResults.push(...result.data);
|
||||||
|
|
@ -1185,12 +1206,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
|
|
||||||
console.log("🔗 [분할패널] 복합키 조건:", searchConditions);
|
console.log("🔗 [분할패널] 복합키 조건:", searchConditions);
|
||||||
|
|
||||||
|
// 🆕 우측 패널 config의 Entity 조인 컬럼 추출
|
||||||
|
const rightJoinColumns = extractAdditionalJoinColumns(
|
||||||
|
componentConfig.rightPanel?.columns,
|
||||||
|
rightTableName,
|
||||||
|
);
|
||||||
|
if (rightJoinColumns) {
|
||||||
|
console.log("🔗 [분할패널] 우측 패널 additionalJoinColumns:", rightJoinColumns);
|
||||||
|
}
|
||||||
|
|
||||||
// 엔티티 조인 API로 데이터 조회
|
// 엔티티 조인 API로 데이터 조회
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
||||||
search: searchConditions,
|
search: searchConditions,
|
||||||
enableEntityJoin: true,
|
enableEntityJoin: true,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
|
companyCodeOverride: companyCode,
|
||||||
|
additionalJoinColumns: rightJoinColumns, // 🆕 Entity 조인 컬럼 전달
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
|
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
|
||||||
|
|
@ -1275,6 +1306,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
|
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
|
||||||
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
|
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
|
||||||
|
|
||||||
|
// 🆕 탭 config의 Entity 조인 컬럼 추출
|
||||||
|
const tabJoinColumns = extractAdditionalJoinColumns(tabConfig.columns, tabTableName);
|
||||||
|
if (tabJoinColumns) {
|
||||||
|
console.log(`🔗 [분할패널] 탭 ${tabIndex} additionalJoinColumns:`, tabJoinColumns);
|
||||||
|
}
|
||||||
|
|
||||||
let resultData: any[] = [];
|
let resultData: any[] = [];
|
||||||
|
|
||||||
if (leftColumn && rightColumn) {
|
if (leftColumn && rightColumn) {
|
||||||
|
|
@ -1303,12 +1340,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
search: searchConditions,
|
search: searchConditions,
|
||||||
enableEntityJoin: true,
|
enableEntityJoin: true,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
|
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
|
||||||
});
|
});
|
||||||
resultData = result.data || [];
|
resultData = result.data || [];
|
||||||
} else {
|
} else {
|
||||||
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
||||||
enableEntityJoin: true,
|
enableEntityJoin: true,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
|
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
|
||||||
});
|
});
|
||||||
resultData = result.data || [];
|
resultData = result.data || [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,13 @@ import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
// 드래그 가능한 컬럼 아이템
|
// 드래그 가능한 컬럼 아이템
|
||||||
function SortableColumnRow({
|
function SortableColumnRow({
|
||||||
id, col, index, isNumeric, onLabelChange, onWidthChange, onFormatChange, onRemove,
|
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
col: { name: string; label: string; width?: number; format?: any };
|
col: { name: string; label: string; width?: number; format?: any };
|
||||||
index: number;
|
index: number;
|
||||||
isNumeric: boolean;
|
isNumeric: boolean;
|
||||||
|
isEntityJoin?: boolean;
|
||||||
onLabelChange: (value: string) => void;
|
onLabelChange: (value: string) => void;
|
||||||
onWidthChange: (value: number) => void;
|
onWidthChange: (value: number) => void;
|
||||||
onFormatChange: (checked: boolean) => void;
|
onFormatChange: (checked: boolean) => void;
|
||||||
|
|
@ -49,12 +50,17 @@ function SortableColumnRow({
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
||||||
isDragging && "z-50 opacity-50 shadow-md",
|
isDragging && "z-50 opacity-50 shadow-md",
|
||||||
|
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||||
<GripVertical className="h-3 w-3" />
|
<GripVertical className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
{isEntityJoin ? (
|
||||||
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
value={col.label}
|
value={col.label}
|
||||||
onChange={(e) => onLabelChange(e.target.value)}
|
onChange={(e) => onLabelChange(e.target.value)}
|
||||||
|
|
@ -1975,6 +1981,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
col={col}
|
col={col}
|
||||||
index={index}
|
index={index}
|
||||||
isNumeric={!!isNumeric}
|
isNumeric={!!isNumeric}
|
||||||
|
isEntityJoin={!!(col as any).isEntityJoin}
|
||||||
onLabelChange={(value) => {
|
onLabelChange={(value) => {
|
||||||
const newColumns = [...selectedColumns];
|
const newColumns = [...selectedColumns];
|
||||||
newColumns[index] = { ...newColumns[index], label: value };
|
newColumns[index] = { ...newColumns[index], label: value };
|
||||||
|
|
@ -2021,6 +2028,78 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 좌측 패널 - Entity 조인 컬럼 아코디언 */}
|
||||||
|
{(() => {
|
||||||
|
const leftTable = config.leftPanel?.tableName || screenTableName;
|
||||||
|
const joinData = leftTable ? entityJoinColumns[leftTable] : null;
|
||||||
|
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||||
|
|
||||||
|
return joinData.joinTables.map((joinTable, tableIndex) => {
|
||||||
|
const joinColumnsToShow = joinTable.availableColumns.filter((column) => {
|
||||||
|
const matchingJoinColumn = joinData.availableColumns.find(
|
||||||
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||||
|
);
|
||||||
|
if (!matchingJoinColumn) return false;
|
||||||
|
return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
|
||||||
|
});
|
||||||
|
const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length;
|
||||||
|
|
||||||
|
if (joinColumnsToShow.length === 0 && addedCount === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details key={`join-${tableIndex}`} className="group">
|
||||||
|
<summary className="border-border/60 my-2 flex cursor-pointer list-none items-center gap-2 border-t pt-2 select-none">
|
||||||
|
<ChevronRight className="h-3 w-3 shrink-0 text-blue-500 transition-transform group-open:rotate-90" />
|
||||||
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
||||||
|
<span className="text-[10px] font-medium text-blue-600">{joinTable.tableName}</span>
|
||||||
|
{addedCount > 0 && (
|
||||||
|
<span className="rounded-full bg-blue-100 px-1.5 text-[9px] font-medium text-blue-600">{addedCount}개 선택</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[9px] text-gray-400">{joinColumnsToShow.length}개 남음</span>
|
||||||
|
</summary>
|
||||||
|
<div className="space-y-0.5 pt-1">
|
||||||
|
{joinColumnsToShow.map((column, colIndex) => {
|
||||||
|
const matchingJoinColumn = joinData.availableColumns.find(
|
||||||
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||||
|
);
|
||||||
|
if (!matchingJoinColumn) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={colIndex}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-blue-50/60"
|
||||||
|
onClick={() => {
|
||||||
|
updateLeftPanel({
|
||||||
|
columns: [...selectedColumns, {
|
||||||
|
name: matchingJoinColumn.joinAlias,
|
||||||
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
|
width: 100,
|
||||||
|
isEntityJoin: true,
|
||||||
|
joinInfo: {
|
||||||
|
sourceTable: leftTable!,
|
||||||
|
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
|
||||||
|
referenceTable: matchingJoinColumn.tableName,
|
||||||
|
joinAlias: matchingJoinColumn.joinAlias,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||||
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
||||||
|
<span className="truncate text-xs text-blue-700">{column.columnLabel || column.columnName}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{joinColumnsToShow.length === 0 && (
|
||||||
|
<p className="px-2 py-1 text-[10px] text-gray-400">모든 컬럼이 이미 추가되었습니다</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2029,76 +2108,6 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 좌측 패널 Entity 조인 컬럼 */}
|
|
||||||
{(() => {
|
|
||||||
const leftTable = config.leftPanel?.tableName || screenTableName;
|
|
||||||
const joinData = leftTable ? entityJoinColumns[leftTable] : null;
|
|
||||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
|
||||||
const selectedColumns = config.leftPanel?.columns || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
||||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">Entity 조인 컬럼</h3>
|
|
||||||
<p className="text-muted-foreground text-[10px]">연관 테이블의 컬럼을 표시 컬럼에 추가합니다</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{joinData.joinTables.map((joinTable, tableIndex) => (
|
|
||||||
<div key={tableIndex} className="space-y-1">
|
|
||||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-blue-600">
|
|
||||||
<Link2 className="h-3 w-3" />
|
|
||||||
<span>{joinTable.tableName}</span>
|
|
||||||
<Badge variant="outline" className="text-[10px]">{joinTable.currentDisplayColumn}</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-32 space-y-0.5 overflow-y-auto rounded-md border p-2">
|
|
||||||
{joinTable.availableColumns.map((column, colIndex) => {
|
|
||||||
const matchingJoinColumn = joinData.availableColumns.find(
|
|
||||||
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
|
||||||
);
|
|
||||||
if (!matchingJoinColumn) return null;
|
|
||||||
const isAdded = selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={colIndex}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-blue-50",
|
|
||||||
isAdded && "bg-blue-50",
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (isAdded) {
|
|
||||||
updateLeftPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) });
|
|
||||||
} else {
|
|
||||||
updateLeftPanel({
|
|
||||||
columns: [...selectedColumns, {
|
|
||||||
name: matchingJoinColumn.joinAlias,
|
|
||||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
|
||||||
width: 100,
|
|
||||||
isEntityJoin: true,
|
|
||||||
joinInfo: {
|
|
||||||
sourceTable: leftTable!,
|
|
||||||
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
|
|
||||||
referenceTable: matchingJoinColumn.tableName,
|
|
||||||
joinAlias: matchingJoinColumn.joinAlias,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox checked={isAdded} className="pointer-events-none h-3.5 w-3.5" />
|
|
||||||
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
|
||||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
|
||||||
<span className="ml-auto text-[10px] text-blue-400">{column.dataType}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* 좌측 패널 데이터 필터링 */}
|
{/* 좌측 패널 데이터 필터링 */}
|
||||||
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">좌측 패널 데이터 필터링</h3>
|
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">좌측 패널 데이터 필터링</h3>
|
||||||
|
|
@ -2351,64 +2360,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 엔티티 설정 선택 - 조건 필터 모드에서만 표시 */}
|
{/* 필터 연결 컬럼 제거됨 - Entity 조인이 자동으로 관계를 처리 */}
|
||||||
{relationshipType !== "detail" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-medium">필터 연결 컬럼</Label>
|
|
||||||
<p className="text-muted-foreground text-[10px]">
|
|
||||||
우측 테이블에서 좌측 테이블을 참조하는 컬럼을 선택하세요
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
value={config.rightPanel?.relation?.foreignKey || ""}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
// 선택된 엔티티 컬럼 정보 찾기
|
|
||||||
const entityColumn = rightTableColumns.find((col) => col.columnName === value);
|
|
||||||
if (entityColumn) {
|
|
||||||
updateRightPanel({
|
|
||||||
relation: {
|
|
||||||
...config.rightPanel?.relation,
|
|
||||||
foreignKey: value,
|
|
||||||
// 참조 테이블과 컬럼은 엔티티 설정에서 자동으로 가져옴
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
|
||||||
<SelectValue placeholder="엔티티 컬럼 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{rightTableColumns
|
|
||||||
.filter((col) => {
|
|
||||||
// 엔티티 타입 컬럼만 표시 (input_type이 entity인 경우)
|
|
||||||
const inputType = col.input_type?.toLowerCase() || col.webType?.toLowerCase() || "";
|
|
||||||
return inputType === "entity" || inputType === "code";
|
|
||||||
})
|
|
||||||
.map((column) => (
|
|
||||||
<SelectItem key={column.columnName} value={column.columnName}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>{column.columnLabel || column.columnName}</span>
|
|
||||||
<span className="text-muted-foreground text-[10px]">({column.columnName})</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
{rightTableColumns.filter((col) => {
|
|
||||||
const inputType = col.input_type?.toLowerCase() || col.webType?.toLowerCase() || "";
|
|
||||||
return inputType === "entity" || inputType === "code";
|
|
||||||
}).length === 0 && (
|
|
||||||
<div className="text-muted-foreground px-2 py-4 text-center text-xs">
|
|
||||||
엔티티 타입 컬럼이 없습니다.
|
|
||||||
<br />
|
|
||||||
테이블 타입관리에서 엔티티 설정을 추가하세요.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{config.rightPanel?.relation?.foreignKey && (
|
|
||||||
<p className="text-muted-foreground text-[10px]">선택된 컬럼의 엔티티 설정이 자동으로 적용됩니다.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 우측 패널 표시 컬럼 설정 - 드래그앤드롭 */}
|
{/* 우측 패널 표시 컬럼 설정 - 드래그앤드롭 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
@ -2455,6 +2407,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
col={col}
|
col={col}
|
||||||
index={index}
|
index={index}
|
||||||
isNumeric={!!isNumeric}
|
isNumeric={!!isNumeric}
|
||||||
|
isEntityJoin={!!(col as any).isEntityJoin}
|
||||||
onLabelChange={(value) => {
|
onLabelChange={(value) => {
|
||||||
const newColumns = [...selectedColumns];
|
const newColumns = [...selectedColumns];
|
||||||
newColumns[index] = { ...newColumns[index], label: value };
|
newColumns[index] = { ...newColumns[index], label: value };
|
||||||
|
|
@ -2499,6 +2452,78 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
||||||
|
{(() => {
|
||||||
|
const rightTable = config.rightPanel?.tableName;
|
||||||
|
const joinData = rightTable ? entityJoinColumns[rightTable] : null;
|
||||||
|
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||||
|
|
||||||
|
return joinData.joinTables.map((joinTable, tableIndex) => {
|
||||||
|
const joinColumnsToShow = joinTable.availableColumns.filter((column) => {
|
||||||
|
const matchingJoinColumn = joinData.availableColumns.find(
|
||||||
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||||
|
);
|
||||||
|
if (!matchingJoinColumn) return false;
|
||||||
|
return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
|
||||||
|
});
|
||||||
|
const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length;
|
||||||
|
|
||||||
|
if (joinColumnsToShow.length === 0 && addedCount === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details key={`join-${tableIndex}`} className="group">
|
||||||
|
<summary className="border-border/60 my-2 flex cursor-pointer list-none items-center gap-2 border-t pt-2 select-none">
|
||||||
|
<ChevronRight className="h-3 w-3 shrink-0 text-blue-500 transition-transform group-open:rotate-90" />
|
||||||
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
||||||
|
<span className="text-[10px] font-medium text-blue-600">{joinTable.tableName}</span>
|
||||||
|
{addedCount > 0 && (
|
||||||
|
<span className="rounded-full bg-blue-100 px-1.5 text-[9px] font-medium text-blue-600">{addedCount}개 선택</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[9px] text-gray-400">{joinColumnsToShow.length}개 남음</span>
|
||||||
|
</summary>
|
||||||
|
<div className="space-y-0.5 pt-1">
|
||||||
|
{joinColumnsToShow.map((column, colIndex) => {
|
||||||
|
const matchingJoinColumn = joinData.availableColumns.find(
|
||||||
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||||
|
);
|
||||||
|
if (!matchingJoinColumn) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={colIndex}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-blue-50/60"
|
||||||
|
onClick={() => {
|
||||||
|
updateRightPanel({
|
||||||
|
columns: [...selectedColumns, {
|
||||||
|
name: matchingJoinColumn.joinAlias,
|
||||||
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
|
width: 100,
|
||||||
|
isEntityJoin: true,
|
||||||
|
joinInfo: {
|
||||||
|
sourceTable: rightTable!,
|
||||||
|
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
|
||||||
|
referenceTable: matchingJoinColumn.tableName,
|
||||||
|
joinAlias: matchingJoinColumn.joinAlias,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||||
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
||||||
|
<span className="truncate text-xs text-blue-700">{column.columnLabel || column.columnName}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{joinColumnsToShow.length === 0 && (
|
||||||
|
<p className="px-2 py-1 text-[10px] text-gray-400">모든 컬럼이 이미 추가되었습니다</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2507,75 +2532,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 패널 Entity 조인 컬럼 */}
|
{/* 우측 패널 Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */}
|
||||||
{(() => {
|
|
||||||
const rightTable = config.rightPanel?.tableName;
|
|
||||||
const joinData = rightTable ? entityJoinColumns[rightTable] : null;
|
|
||||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
|
||||||
const selectedColumns = config.rightPanel?.columns || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
||||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">Entity 조인 컬럼</h3>
|
|
||||||
<p className="text-muted-foreground text-[10px]">연관 테이블의 컬럼을 표시 컬럼에 추가합니다</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{joinData.joinTables.map((joinTable, tableIndex) => (
|
|
||||||
<div key={tableIndex} className="space-y-1">
|
|
||||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-blue-600">
|
|
||||||
<Link2 className="h-3 w-3" />
|
|
||||||
<span>{joinTable.tableName}</span>
|
|
||||||
<Badge variant="outline" className="text-[10px]">{joinTable.currentDisplayColumn}</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-32 space-y-0.5 overflow-y-auto rounded-md border p-2">
|
|
||||||
{joinTable.availableColumns.map((column, colIndex) => {
|
|
||||||
const matchingJoinColumn = joinData.availableColumns.find(
|
|
||||||
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
|
||||||
);
|
|
||||||
if (!matchingJoinColumn) return null;
|
|
||||||
const isAdded = selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={colIndex}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-blue-50",
|
|
||||||
isAdded && "bg-blue-50",
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (isAdded) {
|
|
||||||
updateRightPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) });
|
|
||||||
} else {
|
|
||||||
updateRightPanel({
|
|
||||||
columns: [...selectedColumns, {
|
|
||||||
name: matchingJoinColumn.joinAlias,
|
|
||||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
|
||||||
width: 100,
|
|
||||||
isEntityJoin: true,
|
|
||||||
joinInfo: {
|
|
||||||
sourceTable: rightTable!,
|
|
||||||
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
|
|
||||||
referenceTable: matchingJoinColumn.tableName,
|
|
||||||
joinAlias: matchingJoinColumn.joinAlias,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox checked={isAdded} className="pointer-events-none h-3.5 w-3.5" />
|
|
||||||
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
|
||||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
|
||||||
<span className="ml-auto text-[10px] text-blue-400">{column.dataType}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* 우측 패널 데이터 필터링 */}
|
{/* 우측 패널 데이터 필터링 */}
|
||||||
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue