Merge pull request '분할패널 설정변경' (#342) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/342
This commit is contained in:
commit
0f57309d74
|
|
@ -2185,3 +2185,67 @@ export async function multiTableSave(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 테이블 간의 엔티티 관계 자동 감지
|
||||||
|
* GET /api/table-management/tables/entity-relations?leftTable=xxx&rightTable=yyy
|
||||||
|
*
|
||||||
|
* column_labels에서 정의된 엔티티/카테고리 타입 설정을 기반으로
|
||||||
|
* 두 테이블 간의 외래키 관계를 자동으로 감지합니다.
|
||||||
|
*/
|
||||||
|
export async function getTableEntityRelations(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { leftTable, rightTable } = req.query;
|
||||||
|
|
||||||
|
logger.info(`=== 테이블 엔티티 관계 조회 시작: ${leftTable} <-> ${rightTable} ===`);
|
||||||
|
|
||||||
|
if (!leftTable || !rightTable) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "leftTable과 rightTable 파라미터가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "leftTable과 rightTable 쿼리 파라미터가 필요합니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableManagementService = new TableManagementService();
|
||||||
|
const relations = await tableManagementService.detectTableEntityRelations(
|
||||||
|
String(leftTable),
|
||||||
|
String(rightTable)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`테이블 엔티티 관계 조회 완료: ${relations.length}개 발견`);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: `${relations.length}개의 엔티티 관계를 발견했습니다.`,
|
||||||
|
data: {
|
||||||
|
leftTable: String(leftTable),
|
||||||
|
rightTable: String(rightTable),
|
||||||
|
relations,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 엔티티 관계 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 엔티티 관계 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "ENTITY_RELATIONS_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
toggleLogTable,
|
toggleLogTable,
|
||||||
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||||
multiTableSave, // 🆕 범용 다중 테이블 저장
|
multiTableSave, // 🆕 범용 다중 테이블 저장
|
||||||
|
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
||||||
} from "../controllers/tableManagementController";
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -38,6 +39,15 @@ router.use(authenticateToken);
|
||||||
*/
|
*/
|
||||||
router.get("/tables", getTableList);
|
router.get("/tables", getTableList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 테이블 간 엔티티 관계 조회
|
||||||
|
* GET /api/table-management/tables/entity-relations?leftTable=xxx&rightTable=yyy
|
||||||
|
*
|
||||||
|
* column_labels에서 정의된 엔티티/카테고리 타입 설정을 기반으로
|
||||||
|
* 두 테이블 간의 외래키 관계를 자동으로 감지합니다.
|
||||||
|
*/
|
||||||
|
router.get("/tables/entity-relations", getTableEntityRelations);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 컬럼 정보 조회
|
* 테이블 컬럼 정보 조회
|
||||||
* GET /api/table-management/tables/:tableName/columns
|
* GET /api/table-management/tables/:tableName/columns
|
||||||
|
|
|
||||||
|
|
@ -1306,6 +1306,41 @@ export class TableManagementService {
|
||||||
paramCount: number;
|
paramCount: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
|
// 🆕 배열 값 처리 (다중 값 검색 - 분할패널 엔티티 타입에서 "2,3" 형태 지원)
|
||||||
|
// 좌측에서 "2"를 선택해도, 우측에서 "2,3"을 가진 행이 표시되도록 함
|
||||||
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
|
// 배열의 각 값에 대해 OR 조건으로 검색
|
||||||
|
// 우측 컬럼에 "2,3" 같은 다중 값이 있을 수 있으므로
|
||||||
|
// 각 값을 LIKE 또는 = 조건으로 처리
|
||||||
|
const conditions: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
|
||||||
|
value.forEach((v: any, idx: number) => {
|
||||||
|
const safeValue = String(v).trim();
|
||||||
|
// 정확히 일치하거나, 콤마로 구분된 값 중 하나로 포함
|
||||||
|
// 예: "2,3" 컬럼에서 "2"를 찾으려면:
|
||||||
|
// - 정확히 "2"
|
||||||
|
// - "2," 로 시작
|
||||||
|
// - ",2" 로 끝남
|
||||||
|
// - ",2," 중간에 포함
|
||||||
|
const paramBase = paramIndex + (idx * 4);
|
||||||
|
conditions.push(`(
|
||||||
|
${columnName}::text = $${paramBase} OR
|
||||||
|
${columnName}::text LIKE $${paramBase + 1} OR
|
||||||
|
${columnName}::text LIKE $${paramBase + 2} OR
|
||||||
|
${columnName}::text LIKE $${paramBase + 3}
|
||||||
|
)`);
|
||||||
|
values.push(safeValue, `${safeValue},%`, `%,${safeValue}`, `%,${safeValue},%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`🔍 다중 값 배열 검색: ${columnName} IN [${value.join(", ")}]`);
|
||||||
|
return {
|
||||||
|
whereClause: `(${conditions.join(" OR ")})`,
|
||||||
|
values,
|
||||||
|
paramCount: values.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
|
// 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
|
||||||
if (typeof value === "string" && value.includes("|")) {
|
if (typeof value === "string" && value.includes("|")) {
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(
|
const columnInfo = await this.getColumnWebTypeInfo(
|
||||||
|
|
@ -4630,4 +4665,101 @@ export class TableManagementService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 테이블 간의 엔티티 관계 자동 감지
|
||||||
|
* column_labels에서 엔티티 타입 설정을 기반으로 테이블 간 관계를 찾습니다.
|
||||||
|
*
|
||||||
|
* @param leftTable 좌측 테이블명
|
||||||
|
* @param rightTable 우측 테이블명
|
||||||
|
* @returns 감지된 엔티티 관계 배열
|
||||||
|
*/
|
||||||
|
async detectTableEntityRelations(
|
||||||
|
leftTable: string,
|
||||||
|
rightTable: string
|
||||||
|
): Promise<Array<{
|
||||||
|
leftColumn: string;
|
||||||
|
rightColumn: string;
|
||||||
|
direction: "left_to_right" | "right_to_left";
|
||||||
|
inputType: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}>> {
|
||||||
|
try {
|
||||||
|
logger.info(`두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}`);
|
||||||
|
|
||||||
|
const relations: Array<{
|
||||||
|
leftColumn: string;
|
||||||
|
rightColumn: string;
|
||||||
|
direction: "left_to_right" | "right_to_left";
|
||||||
|
inputType: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// 1. 우측 테이블에서 좌측 테이블을 참조하는 엔티티 컬럼 찾기
|
||||||
|
// 예: right_table의 customer_id -> left_table(customer_mng)의 customer_code
|
||||||
|
const rightToLeftRels = await query<{
|
||||||
|
column_name: string;
|
||||||
|
reference_column: string;
|
||||||
|
input_type: string;
|
||||||
|
display_column: string | null;
|
||||||
|
}>(
|
||||||
|
`SELECT column_name, reference_column, input_type, display_column
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND input_type IN ('entity', 'category')
|
||||||
|
AND reference_table = $2
|
||||||
|
AND reference_column IS NOT NULL
|
||||||
|
AND reference_column != ''`,
|
||||||
|
[rightTable, leftTable]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const rel of rightToLeftRels) {
|
||||||
|
relations.push({
|
||||||
|
leftColumn: rel.reference_column,
|
||||||
|
rightColumn: rel.column_name,
|
||||||
|
direction: "right_to_left",
|
||||||
|
inputType: rel.input_type,
|
||||||
|
displayColumn: rel.display_column || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 좌측 테이블에서 우측 테이블을 참조하는 엔티티 컬럼 찾기
|
||||||
|
// 예: left_table의 item_id -> right_table(item_info)의 item_number
|
||||||
|
const leftToRightRels = await query<{
|
||||||
|
column_name: string;
|
||||||
|
reference_column: string;
|
||||||
|
input_type: string;
|
||||||
|
display_column: string | null;
|
||||||
|
}>(
|
||||||
|
`SELECT column_name, reference_column, input_type, display_column
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND input_type IN ('entity', 'category')
|
||||||
|
AND reference_table = $2
|
||||||
|
AND reference_column IS NOT NULL
|
||||||
|
AND reference_column != ''`,
|
||||||
|
[leftTable, rightTable]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const rel of leftToRightRels) {
|
||||||
|
relations.push({
|
||||||
|
leftColumn: rel.column_name,
|
||||||
|
rightColumn: rel.reference_column,
|
||||||
|
direction: "left_to_right",
|
||||||
|
inputType: rel.input_type,
|
||||||
|
displayColumn: rel.display_column || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`엔티티 관계 감지 완료: ${relations.length}개 발견`);
|
||||||
|
relations.forEach((rel, idx) => {
|
||||||
|
logger.info(` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return relations;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,40 @@ class TableManagementApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 테이블 간의 엔티티 관계 자동 감지
|
||||||
|
* column_labels에서 정의된 엔티티/카테고리 타입 설정을 기반으로
|
||||||
|
* 두 테이블 간의 외래키 관계를 자동으로 감지합니다.
|
||||||
|
*/
|
||||||
|
async getTableEntityRelations(
|
||||||
|
leftTable: string,
|
||||||
|
rightTable: string
|
||||||
|
): Promise<ApiResponse<{
|
||||||
|
leftTable: string;
|
||||||
|
rightTable: string;
|
||||||
|
relations: Array<{
|
||||||
|
leftColumn: string;
|
||||||
|
rightColumn: string;
|
||||||
|
direction: "left_to_right" | "right_to_left";
|
||||||
|
inputType: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}>;
|
||||||
|
}>> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`${this.basePath}/tables/entity-relations?leftTable=${encodeURIComponent(leftTable)}&rightTable=${encodeURIComponent(rightTable)}`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ 테이블 엔티티 관계 조회 실패: ${leftTable} <-> ${rightTable}`, error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.response?.data?.message || error.message || "테이블 엔티티 관계를 조회할 수 없습니다.",
|
||||||
|
errorCode: error.response?.data?.errorCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 싱글톤 인스턴스 생성
|
// 싱글톤 인스턴스 생성
|
||||||
|
|
|
||||||
|
|
@ -830,7 +830,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
size: 1,
|
size: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
|
// result.data가 EntityJoinResponse의 실제 배열 필드
|
||||||
|
const detail = result.data && result.data.length > 0 ? result.data[0] : null;
|
||||||
setRightData(detail);
|
setRightData(detail);
|
||||||
} else if (relationshipType === "join") {
|
} else if (relationshipType === "join") {
|
||||||
// 조인 모드: 다른 테이블의 관련 데이터 (여러 개)
|
// 조인 모드: 다른 테이블의 관련 데이터 (여러 개)
|
||||||
|
|
@ -899,16 +900,54 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 복합키 지원
|
// 🆕 엔티티 관계 자동 감지 로직 개선
|
||||||
if (keys && keys.length > 0 && leftTable) {
|
// 1. 설정된 keys가 있으면 사용
|
||||||
|
// 2. 없으면 테이블 타입관리에서 정의된 엔티티 관계를 자동으로 조회
|
||||||
|
let effectiveKeys = keys || [];
|
||||||
|
|
||||||
|
if (effectiveKeys.length === 0 && leftTable && rightTableName) {
|
||||||
|
// 엔티티 관계 자동 감지
|
||||||
|
console.log("🔍 [분할패널] 엔티티 관계 자동 감지 시작:", leftTable, "->", rightTableName);
|
||||||
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||||
|
const relResponse = await tableManagementApi.getTableEntityRelations(leftTable, rightTableName);
|
||||||
|
|
||||||
|
if (relResponse.success && relResponse.data?.relations && relResponse.data.relations.length > 0) {
|
||||||
|
effectiveKeys = relResponse.data.relations.map((rel) => ({
|
||||||
|
leftColumn: rel.leftColumn,
|
||||||
|
rightColumn: rel.rightColumn,
|
||||||
|
}));
|
||||||
|
console.log("✅ [분할패널] 자동 감지된 관계:", effectiveKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveKeys.length > 0 && leftTable) {
|
||||||
// 복합키: 여러 조건으로 필터링
|
// 복합키: 여러 조건으로 필터링
|
||||||
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
|
|
||||||
// 복합키 조건 생성
|
// 복합키 조건 생성 (다중 값 지원)
|
||||||
|
// 🆕 항상 배열로 전달하여 백엔드에서 다중 값 컬럼 검색을 지원하도록 함
|
||||||
|
// 예: 좌측에서 "2"를 선택해도, 우측에서 "2,3"을 가진 행이 표시되도록
|
||||||
const searchConditions: Record<string, any> = {};
|
const searchConditions: Record<string, any> = {};
|
||||||
keys.forEach((key) => {
|
effectiveKeys.forEach((key) => {
|
||||||
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
||||||
searchConditions[key.rightColumn] = leftItem[key.leftColumn];
|
const leftValue = leftItem[key.leftColumn];
|
||||||
|
// 다중 값 지원: 모든 값을 배열로 변환하여 다중 값 컬럼 검색 활성화
|
||||||
|
if (typeof leftValue === "string") {
|
||||||
|
if (leftValue.includes(",")) {
|
||||||
|
// "2,3" 형태면 분리해서 배열로
|
||||||
|
const values = leftValue.split(",").map((v: string) => v.trim()).filter((v: string) => v);
|
||||||
|
searchConditions[key.rightColumn] = values;
|
||||||
|
console.log("🔗 [분할패널] 다중 값 검색 (분리):", key.rightColumn, "=", values);
|
||||||
|
} else {
|
||||||
|
// 단일 값도 배열로 변환 (우측에 "2,3" 같은 다중 값이 있을 수 있으므로)
|
||||||
|
searchConditions[key.rightColumn] = [leftValue.trim()];
|
||||||
|
console.log("🔗 [분할패널] 다중 값 검색 (단일):", key.rightColumn, "=", [leftValue.trim()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 숫자나 다른 타입은 배열로 감싸기
|
||||||
|
searchConditions[key.rightColumn] = [leftValue];
|
||||||
|
console.log("🔗 [분할패널] 다중 값 검색 (기타):", key.rightColumn, "=", [leftValue]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -947,7 +986,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
|
|
||||||
setRightData(filteredData);
|
setRightData(filteredData);
|
||||||
} else {
|
} else {
|
||||||
// 단일키 (하위 호환성)
|
// 단일키 (하위 호환성) 또는 관계를 찾지 못한 경우
|
||||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||||
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
|
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
|
||||||
|
|
||||||
|
|
@ -965,6 +1004,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
|
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
|
||||||
);
|
);
|
||||||
setRightData(joinedData || []); // 모든 관련 레코드 (배열)
|
setRightData(joinedData || []); // 모든 관련 레코드 (배열)
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ [분할패널] 테이블 관계를 찾을 수 없습니다:", leftTable, "->", rightTableName);
|
||||||
|
setRightData([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,71 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
}
|
}
|
||||||
}, [config.rightPanel?.tableName]);
|
}, [config.rightPanel?.tableName]);
|
||||||
|
|
||||||
|
// 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지
|
||||||
|
const [autoDetectedRelations, setAutoDetectedRelations] = useState<
|
||||||
|
Array<{
|
||||||
|
leftColumn: string;
|
||||||
|
rightColumn: string;
|
||||||
|
direction: "left_to_right" | "right_to_left";
|
||||||
|
inputType: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
|
const [isDetectingRelations, setIsDetectingRelations] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const detectRelations = async () => {
|
||||||
|
const leftTable = config.leftPanel?.tableName || screenTableName;
|
||||||
|
const rightTable = config.rightPanel?.tableName;
|
||||||
|
|
||||||
|
// 조인 모드이고 양쪽 테이블이 모두 있을 때만 감지
|
||||||
|
if (relationshipType !== "join" || !leftTable || !rightTable) {
|
||||||
|
setAutoDetectedRelations([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDetectingRelations(true);
|
||||||
|
try {
|
||||||
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||||
|
const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
|
||||||
|
|
||||||
|
if (response.success && response.data?.relations) {
|
||||||
|
console.log("🔍 엔티티 관계 자동 감지:", response.data.relations);
|
||||||
|
setAutoDetectedRelations(response.data.relations);
|
||||||
|
|
||||||
|
// 감지된 관계가 있고, 현재 설정된 키가 없으면 자동으로 첫 번째 관계를 설정
|
||||||
|
const currentKeys = config.rightPanel?.relation?.keys || [];
|
||||||
|
if (response.data.relations.length > 0 && currentKeys.length === 0) {
|
||||||
|
// 첫 번째 관계만 자동 설정 (사용자가 추가로 설정 가능)
|
||||||
|
const firstRel = response.data.relations[0];
|
||||||
|
console.log("✅ 첫 번째 엔티티 관계 자동 설정:", firstRel);
|
||||||
|
updateRightPanel({
|
||||||
|
relation: {
|
||||||
|
...config.rightPanel?.relation,
|
||||||
|
type: "join",
|
||||||
|
useMultipleKeys: true,
|
||||||
|
keys: [
|
||||||
|
{
|
||||||
|
leftColumn: firstRel.leftColumn,
|
||||||
|
rightColumn: firstRel.rightColumn,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 엔티티 관계 감지 실패:", error);
|
||||||
|
setAutoDetectedRelations([]);
|
||||||
|
} finally {
|
||||||
|
setIsDetectingRelations(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
detectRelations();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]);
|
||||||
|
|
||||||
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
|
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
|
||||||
console.log(" - config:", config);
|
console.log(" - config:", config);
|
||||||
console.log(" - tables:", tables);
|
console.log(" - tables:", tables);
|
||||||
|
|
@ -1633,234 +1698,50 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 컬럼 매핑 - 조인 모드에서만 표시 */}
|
{/* 엔티티 관계 자동 감지 (읽기 전용) - 조인 모드에서만 표시 */}
|
||||||
{relationshipType !== "detail" && (
|
{relationshipType !== "detail" && (
|
||||||
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<Label className="text-sm font-semibold">테이블 관계 (자동 감지)</Label>
|
||||||
<Label className="text-sm font-semibold">컬럼 매핑 (외래키 관계)</Label>
|
<p className="text-xs text-gray-600">테이블 타입관리에서 정의된 엔티티 관계입니다</p>
|
||||||
<p className="text-xs text-gray-600">좌측 테이블의 컬럼을 우측 테이블의 컬럼과 연결합니다</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
const currentKeys = config.rightPanel?.relation?.keys || [];
|
|
||||||
// 단일키에서 복합키로 전환 시 기존 값 유지
|
|
||||||
if (
|
|
||||||
currentKeys.length === 0 &&
|
|
||||||
config.rightPanel?.relation?.leftColumn &&
|
|
||||||
config.rightPanel?.relation?.foreignKey
|
|
||||||
) {
|
|
||||||
updateRightPanel({
|
|
||||||
relation: {
|
|
||||||
...config.rightPanel?.relation,
|
|
||||||
keys: [
|
|
||||||
{
|
|
||||||
leftColumn: config.rightPanel.relation.leftColumn,
|
|
||||||
rightColumn: config.rightPanel.relation.foreignKey,
|
|
||||||
},
|
|
||||||
{ leftColumn: "", rightColumn: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateRightPanel({
|
|
||||||
relation: {
|
|
||||||
...config.rightPanel?.relation,
|
|
||||||
keys: [...currentKeys, { leftColumn: "", rightColumn: "" }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
|
||||||
조인 키 추가
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[10px] text-blue-600">복합키: 여러 컬럼으로 조인 (예: item_code + lot_number)</p>
|
{isDetectingRelations ? (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||||
{/* 복합키가 설정된 경우 */}
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||||
{(config.rightPanel?.relation?.keys || []).length > 0 ? (
|
관계 감지 중...
|
||||||
<>
|
</div>
|
||||||
{(config.rightPanel?.relation?.keys || []).map((key, index) => (
|
) : autoDetectedRelations.length > 0 ? (
|
||||||
<div key={index} className="space-y-2 rounded-md border bg-white p-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
{autoDetectedRelations.map((rel, index) => (
|
||||||
<span className="text-xs font-medium">조인 키 {index + 1}</span>
|
<div key={index} className="flex items-center gap-2 rounded-md border border-blue-300 bg-white p-2">
|
||||||
<Button
|
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
||||||
size="sm"
|
{leftTableName}.{rel.leftColumn}
|
||||||
variant="ghost"
|
</span>
|
||||||
className="text-destructive h-6 w-6 p-0"
|
<ArrowRight className="h-3 w-3 text-blue-400" />
|
||||||
onClick={() => {
|
<span className="rounded bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
|
||||||
const newKeys = (config.rightPanel?.relation?.keys || []).filter((_, i) => i !== index);
|
{rightTableName}.{rel.rightColumn}
|
||||||
updateRightPanel({
|
</span>
|
||||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
<span className="ml-auto text-[10px] text-gray-500">
|
||||||
});
|
{rel.inputType === "entity" ? "엔티티" : "카테고리"}
|
||||||
}}
|
</span>
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">좌측 컬럼</Label>
|
|
||||||
<Select
|
|
||||||
value={key.leftColumn || ""}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
|
|
||||||
newKeys[index] = { ...newKeys[index], leftColumn: value };
|
|
||||||
updateRightPanel({
|
|
||||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
|
||||||
<SelectValue placeholder="좌측 컬럼" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{leftTableColumns.map((column) => (
|
|
||||||
<SelectItem key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">우측 컬럼</Label>
|
|
||||||
<Select
|
|
||||||
value={key.rightColumn || ""}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
|
|
||||||
newKeys[index] = { ...newKeys[index], rightColumn: value };
|
|
||||||
updateRightPanel({
|
|
||||||
relation: { ...config.rightPanel?.relation, keys: newKeys },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
|
||||||
<SelectValue placeholder="우측 컬럼" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{rightTableColumns.map((column) => (
|
|
||||||
<SelectItem key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
<p className="text-[10px] text-blue-600">
|
||||||
|
테이블 타입관리에서 엔티티/카테고리 설정을 변경하면 자동으로 적용됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : config.rightPanel?.tableName ? (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500">감지된 엔티티 관계가 없습니다</p>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-400">
|
||||||
|
테이블 타입관리에서 엔티티 타입과 참조 테이블을 설정하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* 단일키 (하위 호환성) */
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
||||||
<>
|
<p className="text-xs text-gray-500">우측 테이블을 선택하면 관계를 자동 감지합니다</p>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label className="text-xs">좌측 컬럼</Label>
|
|
||||||
<Popover open={leftColumnOpen} onOpenChange={setLeftColumnOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={leftColumnOpen}
|
|
||||||
className="w-full justify-between"
|
|
||||||
disabled={!config.leftPanel?.tableName}
|
|
||||||
>
|
|
||||||
{config.rightPanel?.relation?.leftColumn || "좌측 컬럼 선택"}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-full p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="컬럼 검색..." />
|
|
||||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
||||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
||||||
{leftTableColumns.map((column) => (
|
|
||||||
<CommandItem
|
|
||||||
key={column.columnName}
|
|
||||||
value={column.columnName}
|
|
||||||
onSelect={(value) => {
|
|
||||||
updateRightPanel({
|
|
||||||
relation: { ...config.rightPanel?.relation, leftColumn: value },
|
|
||||||
});
|
|
||||||
setLeftColumnOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
config.rightPanel?.relation?.leftColumn === column.columnName
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{column.columnName}
|
|
||||||
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">우측 컬럼 (외래키)</Label>
|
|
||||||
<Popover open={rightColumnOpen} onOpenChange={setRightColumnOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={rightColumnOpen}
|
|
||||||
className="w-full justify-between"
|
|
||||||
disabled={!config.rightPanel?.tableName}
|
|
||||||
>
|
|
||||||
{config.rightPanel?.relation?.foreignKey || "우측 컬럼 선택"}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-full p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="컬럼 검색..." />
|
|
||||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
||||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
||||||
{rightTableColumns.map((column) => (
|
|
||||||
<CommandItem
|
|
||||||
key={column.columnName}
|
|
||||||
value={column.columnName}
|
|
||||||
onSelect={(value) => {
|
|
||||||
updateRightPanel({
|
|
||||||
relation: { ...config.rightPanel?.relation, foreignKey: value },
|
|
||||||
});
|
|
||||||
setRightColumnOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
config.rightPanel?.relation?.foreignKey === column.columnName
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{column.columnName}
|
|
||||||
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue