Compare commits
3 Commits
351d53cd0c
...
589f5b9222
| Author | SHA1 | Date |
|---|---|---|
|
|
589f5b9222 | |
|
|
1753822211 | |
|
|
9b8546ebef |
|
|
@ -269,6 +269,23 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques
|
||||||
|
|
||||||
// ==================== 테스트 테이블용 API ====================
|
// ==================== 테스트 테이블용 API ====================
|
||||||
|
|
||||||
|
// [테스트] 테스트 테이블에서 채번 규칙 목록 조회
|
||||||
|
router.get("/test/list/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
|
||||||
|
|
||||||
|
logger.info("[테스트] 채번 규칙 목록 조회 요청", { companyCode, menuObjid });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rules = await numberingRuleService.getRulesFromTest(companyCode, menuObjid);
|
||||||
|
logger.info("[테스트] 채번 규칙 목록 조회 성공", { companyCode, menuObjid, count: rules.length });
|
||||||
|
return res.json({ success: true, data: rules });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[테스트] 채번 규칙 목록 조회 실패", { error: error.message });
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// [테스트] 테이블+컬럼 기반 채번 규칙 조회
|
// [테스트] 테이블+컬럼 기반 채번 규칙 조회
|
||||||
router.get("/test/by-column/:tableName/:columnName", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
router.get("/test/by-column/:tableName/:columnName", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
|
|
|
||||||
|
|
@ -1642,6 +1642,113 @@ export async function toggleLogTable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사별 카테고리 컬럼 조회 (메뉴 종속 없음)
|
||||||
|
*
|
||||||
|
* @route GET /api/table-management/category-columns
|
||||||
|
* @description table_type_columns에서 회사 코드 기준으로 input_type = 'category'인 컬럼을 조회
|
||||||
|
*/
|
||||||
|
export async function getCategoryColumnsByCompany(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
|
||||||
|
logger.info("📥 회사별 카테고리 컬럼 조회 요청", { companyCode });
|
||||||
|
|
||||||
|
if (!companyCode) {
|
||||||
|
logger.error("❌ 회사 코드가 없습니다", { user: req.user });
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getPool } = await import("../database/db");
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
let columnsResult;
|
||||||
|
|
||||||
|
// 최고 관리자인 경우 company_code = '*'인 카테고리 컬럼 조회
|
||||||
|
if (companyCode === "*") {
|
||||||
|
const columnsQuery = `
|
||||||
|
SELECT DISTINCT
|
||||||
|
ttc.table_name AS "tableName",
|
||||||
|
COALESCE(
|
||||||
|
tl.table_label,
|
||||||
|
initcap(replace(ttc.table_name, '_', ' '))
|
||||||
|
) AS "tableLabel",
|
||||||
|
ttc.column_name AS "columnName",
|
||||||
|
COALESCE(
|
||||||
|
cl.column_label,
|
||||||
|
initcap(replace(ttc.column_name, '_', ' '))
|
||||||
|
) AS "columnLabel",
|
||||||
|
ttc.input_type AS "inputType"
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
LEFT JOIN column_labels cl
|
||||||
|
ON ttc.table_name = cl.table_name
|
||||||
|
AND ttc.column_name = cl.column_name
|
||||||
|
LEFT JOIN table_labels tl
|
||||||
|
ON ttc.table_name = tl.table_name
|
||||||
|
WHERE ttc.input_type = 'category'
|
||||||
|
AND ttc.company_code = '*'
|
||||||
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
columnsResult = await pool.query(columnsQuery);
|
||||||
|
logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", {
|
||||||
|
rowCount: columnsResult.rows.length
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
|
||||||
|
const columnsQuery = `
|
||||||
|
SELECT DISTINCT
|
||||||
|
ttc.table_name AS "tableName",
|
||||||
|
COALESCE(
|
||||||
|
tl.table_label,
|
||||||
|
initcap(replace(ttc.table_name, '_', ' '))
|
||||||
|
) AS "tableLabel",
|
||||||
|
ttc.column_name AS "columnName",
|
||||||
|
COALESCE(
|
||||||
|
cl.column_label,
|
||||||
|
initcap(replace(ttc.column_name, '_', ' '))
|
||||||
|
) AS "columnLabel",
|
||||||
|
ttc.input_type AS "inputType"
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
LEFT JOIN column_labels cl
|
||||||
|
ON ttc.table_name = cl.table_name
|
||||||
|
AND ttc.column_name = cl.column_name
|
||||||
|
LEFT JOIN table_labels tl
|
||||||
|
ON ttc.table_name = tl.table_name
|
||||||
|
WHERE ttc.input_type = 'category'
|
||||||
|
AND ttc.company_code = $1
|
||||||
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
columnsResult = await pool.query(columnsQuery, [companyCode]);
|
||||||
|
logger.info("✅ 회사별 카테고리 컬럼 조회 완료", {
|
||||||
|
companyCode,
|
||||||
|
rowCount: columnsResult.rows.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: columnsResult.rows,
|
||||||
|
message: "카테고리 컬럼 조회 성공",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("❌ 회사별 카테고리 컬럼 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "카테고리 컬럼 조회 중 오류가 발생했습니다.",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴의 상위 메뉴들이 설정한 모든 카테고리 타입 컬럼 조회 (계층 구조 상속)
|
* 메뉴의 상위 메뉴들이 설정한 모든 카테고리 타입 컬럼 조회 (계층 구조 상속)
|
||||||
*
|
*
|
||||||
|
|
@ -1670,125 +1777,28 @@ export async function getCategoryColumnsByMenu(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getPool } = await import("../database/db");
|
if (!companyCode) {
|
||||||
const pool = getPool();
|
logger.error("❌ 회사 코드가 없습니다", { menuObjid, user: req.user });
|
||||||
|
res.status(400).json({
|
||||||
// 1. category_column_mapping 테이블 존재 여부 확인
|
success: false,
|
||||||
const tableExistsResult = await pool.query(`
|
message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.",
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'category_column_mapping'
|
|
||||||
) as table_exists
|
|
||||||
`);
|
|
||||||
const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true;
|
|
||||||
|
|
||||||
let columnsResult;
|
|
||||||
|
|
||||||
if (mappingTableExists) {
|
|
||||||
// 🆕 category_column_mapping을 사용한 계층 구조 기반 조회
|
|
||||||
logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회 (계층 구조 상속)", { menuObjid, companyCode });
|
|
||||||
|
|
||||||
// 현재 메뉴와 모든 상위 메뉴의 objid 조회 (재귀)
|
|
||||||
const ancestorMenuQuery = `
|
|
||||||
WITH RECURSIVE menu_hierarchy AS (
|
|
||||||
-- 현재 메뉴
|
|
||||||
SELECT objid, parent_obj_id, menu_type, menu_name_kor
|
|
||||||
FROM menu_info
|
|
||||||
WHERE objid = $1
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- 부모 메뉴 재귀 조회
|
|
||||||
SELECT m.objid, m.parent_obj_id, m.menu_type, m.menu_name_kor
|
|
||||||
FROM menu_info m
|
|
||||||
INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id
|
|
||||||
WHERE m.parent_obj_id != 0 -- 최상위 메뉴(parent_obj_id=0) 제외
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
ARRAY_AGG(objid) as menu_objids,
|
|
||||||
ARRAY_AGG(menu_name_kor) as menu_names
|
|
||||||
FROM menu_hierarchy
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]);
|
|
||||||
const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)];
|
|
||||||
const ancestorMenuNames = ancestorMenuResult.rows[0]?.menu_names || [];
|
|
||||||
|
|
||||||
logger.info("✅ 상위 메뉴 계층 조회 완료", {
|
|
||||||
ancestorMenuObjids,
|
|
||||||
ancestorMenuNames,
|
|
||||||
hierarchyDepth: ancestorMenuObjids.length
|
|
||||||
});
|
|
||||||
|
|
||||||
// 상위 메뉴들에 설정된 모든 카테고리 컬럼 조회 (테이블 필터링 제거)
|
|
||||||
const columnsQuery = `
|
|
||||||
SELECT DISTINCT
|
|
||||||
ttc.table_name AS "tableName",
|
|
||||||
COALESCE(
|
|
||||||
tl.table_label,
|
|
||||||
initcap(replace(ttc.table_name, '_', ' '))
|
|
||||||
) AS "tableLabel",
|
|
||||||
ccm.logical_column_name AS "columnName",
|
|
||||||
COALESCE(
|
|
||||||
cl.column_label,
|
|
||||||
initcap(replace(ccm.logical_column_name, '_', ' '))
|
|
||||||
) AS "columnLabel",
|
|
||||||
ttc.input_type AS "inputType",
|
|
||||||
ccm.menu_objid AS "definedAtMenuObjid"
|
|
||||||
FROM category_column_mapping ccm
|
|
||||||
INNER JOIN table_type_columns ttc
|
|
||||||
ON ccm.table_name = ttc.table_name
|
|
||||||
AND ccm.physical_column_name = ttc.column_name
|
|
||||||
LEFT JOIN column_labels cl
|
|
||||||
ON ttc.table_name = cl.table_name
|
|
||||||
AND ttc.column_name = cl.column_name
|
|
||||||
LEFT JOIN table_labels tl
|
|
||||||
ON ttc.table_name = tl.table_name
|
|
||||||
WHERE ccm.company_code = $1
|
|
||||||
AND ccm.menu_objid = ANY($2)
|
|
||||||
AND ttc.input_type = 'category'
|
|
||||||
ORDER BY ttc.table_name, ccm.logical_column_name
|
|
||||||
`;
|
|
||||||
|
|
||||||
columnsResult = await pool.query(columnsQuery, [companyCode, ancestorMenuObjids]);
|
|
||||||
logger.info("✅ category_column_mapping 기반 조회 완료 (계층 구조 상속)", {
|
|
||||||
rowCount: columnsResult.rows.length,
|
|
||||||
columns: columnsResult.rows.map((r: any) => `${r.tableName}.${r.columnName}`)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 🔄 레거시 방식: 형제 메뉴들의 테이블에서 모든 카테고리 컬럼 조회
|
|
||||||
logger.info("🔍 레거시 방식: 형제 메뉴 테이블 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
|
||||||
|
|
||||||
// 형제 메뉴 조회
|
|
||||||
const { getSiblingMenuObjids } = await import("../services/menuService");
|
|
||||||
const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid));
|
|
||||||
|
|
||||||
// 형제 메뉴들이 사용하는 테이블 조회
|
|
||||||
const tablesQuery = `
|
|
||||||
SELECT DISTINCT sd.table_name
|
|
||||||
FROM screen_menu_assignments sma
|
|
||||||
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
|
||||||
WHERE sma.menu_objid = ANY($1)
|
|
||||||
AND sma.company_code = $2
|
|
||||||
AND sd.table_name IS NOT NULL
|
|
||||||
`;
|
|
||||||
|
|
||||||
const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]);
|
|
||||||
const tableNames = tablesResult.rows.map((row: any) => row.table_name);
|
|
||||||
|
|
||||||
logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length });
|
|
||||||
|
|
||||||
if (tableNames.length === 0) {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: [],
|
|
||||||
message: "형제 메뉴에 연결된 테이블이 없습니다.",
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { getPool } = await import("../database/db");
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
// 🆕 table_type_columns에서 직접 input_type = 'category'인 컬럼들을 조회
|
||||||
|
// category_column_mapping 대신 table_type_columns 기준으로 조회
|
||||||
|
logger.info("🔍 table_type_columns 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
||||||
|
|
||||||
|
let columnsResult;
|
||||||
|
|
||||||
|
// 최고 관리자인 경우 모든 회사의 카테고리 컬럼 조회
|
||||||
|
if (companyCode === "*") {
|
||||||
const columnsQuery = `
|
const columnsQuery = `
|
||||||
SELECT
|
SELECT DISTINCT
|
||||||
ttc.table_name AS "tableName",
|
ttc.table_name AS "tableName",
|
||||||
COALESCE(
|
COALESCE(
|
||||||
tl.table_label,
|
tl.table_label,
|
||||||
|
|
@ -1806,14 +1816,46 @@ export async function getCategoryColumnsByMenu(
|
||||||
AND ttc.column_name = cl.column_name
|
AND ttc.column_name = cl.column_name
|
||||||
LEFT JOIN table_labels tl
|
LEFT JOIN table_labels tl
|
||||||
ON ttc.table_name = tl.table_name
|
ON ttc.table_name = tl.table_name
|
||||||
WHERE ttc.table_name = ANY($1)
|
WHERE ttc.input_type = 'category'
|
||||||
AND ttc.company_code = $2
|
AND ttc.company_code = '*'
|
||||||
AND ttc.input_type = 'category'
|
|
||||||
ORDER BY ttc.table_name, ttc.column_name
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
columnsResult = await pool.query(columnsQuery);
|
||||||
logger.info("✅ 레거시 방식 조회 완료", { rowCount: columnsResult.rows.length });
|
logger.info("✅ 최고 관리자: 전체 카테고리 컬럼 조회 완료", {
|
||||||
|
rowCount: columnsResult.rows.length
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 일반 회사: 해당 회사의 카테고리 컬럼만 조회
|
||||||
|
const columnsQuery = `
|
||||||
|
SELECT DISTINCT
|
||||||
|
ttc.table_name AS "tableName",
|
||||||
|
COALESCE(
|
||||||
|
tl.table_label,
|
||||||
|
initcap(replace(ttc.table_name, '_', ' '))
|
||||||
|
) AS "tableLabel",
|
||||||
|
ttc.column_name AS "columnName",
|
||||||
|
COALESCE(
|
||||||
|
cl.column_label,
|
||||||
|
initcap(replace(ttc.column_name, '_', ' '))
|
||||||
|
) AS "columnLabel",
|
||||||
|
ttc.input_type AS "inputType"
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
LEFT JOIN column_labels cl
|
||||||
|
ON ttc.table_name = cl.table_name
|
||||||
|
AND ttc.column_name = cl.column_name
|
||||||
|
LEFT JOIN table_labels tl
|
||||||
|
ON ttc.table_name = tl.table_name
|
||||||
|
WHERE ttc.input_type = 'category'
|
||||||
|
AND ttc.company_code = $1
|
||||||
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
columnsResult = await pool.query(columnsQuery, [companyCode]);
|
||||||
|
logger.info("✅ 회사별 카테고리 컬럼 조회 완료", {
|
||||||
|
companyCode,
|
||||||
|
rowCount: columnsResult.rows.length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
getLogData,
|
getLogData,
|
||||||
toggleLogTable,
|
toggleLogTable,
|
||||||
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||||
|
getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회
|
||||||
multiTableSave, // 🆕 범용 다중 테이블 저장
|
multiTableSave, // 🆕 범용 다중 테이블 저장
|
||||||
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
||||||
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
||||||
|
|
@ -212,6 +213,12 @@ router.post("/tables/:tableName/log/toggle", toggleLogTable);
|
||||||
// 메뉴 기반 카테고리 관리 API
|
// 메뉴 기반 카테고리 관리 API
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사 기준 모든 카테고리 타입 컬럼 조회 (메뉴 종속 없음)
|
||||||
|
* GET /api/table-management/category-columns
|
||||||
|
*/
|
||||||
|
router.get("/category-columns", getCategoryColumnsByCompany);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||||
* GET /api/table-management/menu/:menuObjid/category-columns
|
* GET /api/table-management/menu/:menuObjid/category-columns
|
||||||
|
|
|
||||||
|
|
@ -1196,6 +1196,109 @@ class NumberingRuleService {
|
||||||
logger.info("시퀀스 초기화 완료", { ruleId, companyCode });
|
logger.info("시퀀스 초기화 완료", { ruleId, companyCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [테스트] 테스트 테이블에서 채번 규칙 목록 조회
|
||||||
|
* numbering_rules_test 테이블 사용
|
||||||
|
*/
|
||||||
|
async getRulesFromTest(
|
||||||
|
companyCode: string,
|
||||||
|
menuObjid?: number
|
||||||
|
): Promise<NumberingRuleConfig[]> {
|
||||||
|
try {
|
||||||
|
logger.info("[테스트] 채번 규칙 목록 조회 시작", { companyCode, menuObjid });
|
||||||
|
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
// 멀티테넌시: 최고 관리자 vs 일반 회사
|
||||||
|
let query: string;
|
||||||
|
let params: any[];
|
||||||
|
|
||||||
|
if (companyCode === "*") {
|
||||||
|
// 최고 관리자: 모든 규칙 조회
|
||||||
|
query = `
|
||||||
|
SELECT
|
||||||
|
rule_id AS "ruleId",
|
||||||
|
rule_name AS "ruleName",
|
||||||
|
description,
|
||||||
|
separator,
|
||||||
|
reset_period AS "resetPeriod",
|
||||||
|
current_sequence AS "currentSequence",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
category_column AS "categoryColumn",
|
||||||
|
category_value_id AS "categoryValueId",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy"
|
||||||
|
FROM numbering_rules_test
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
params = [];
|
||||||
|
} else {
|
||||||
|
// 일반 회사: 자신의 규칙만 조회
|
||||||
|
query = `
|
||||||
|
SELECT
|
||||||
|
rule_id AS "ruleId",
|
||||||
|
rule_name AS "ruleName",
|
||||||
|
description,
|
||||||
|
separator,
|
||||||
|
reset_period AS "resetPeriod",
|
||||||
|
current_sequence AS "currentSequence",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
category_column AS "categoryColumn",
|
||||||
|
category_value_id AS "categoryValueId",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy"
|
||||||
|
FROM numbering_rules_test
|
||||||
|
WHERE company_code = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
params = [companyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(query, params);
|
||||||
|
|
||||||
|
// 각 규칙의 파트 정보 조회
|
||||||
|
for (const rule of result.rows) {
|
||||||
|
const partsQuery = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
part_order AS "order",
|
||||||
|
part_type AS "partType",
|
||||||
|
generation_method AS "generationMethod",
|
||||||
|
auto_config AS "autoConfig",
|
||||||
|
manual_config AS "manualConfig"
|
||||||
|
FROM numbering_rule_parts_test
|
||||||
|
WHERE rule_id = $1 AND company_code = $2
|
||||||
|
ORDER BY part_order
|
||||||
|
`;
|
||||||
|
const partsResult = await pool.query(partsQuery, [
|
||||||
|
rule.ruleId,
|
||||||
|
companyCode === "*" ? rule.companyCode : companyCode,
|
||||||
|
]);
|
||||||
|
rule.parts = partsResult.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("[테스트] 채번 규칙 목록 조회 완료", {
|
||||||
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
count: result.rows.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[테스트] 채번 규칙 목록 조회 실패", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [테스트] 테이블명 + 컬럼명 기반으로 채번규칙 조회 (menu_objid 없이)
|
* [테스트] 테이블명 + 컬럼명 기반으로 채번규칙 조회 (menu_objid 없이)
|
||||||
* numbering_rules_test 테이블 사용
|
* numbering_rules_test 테이블 사용
|
||||||
|
|
|
||||||
|
|
@ -1469,6 +1469,7 @@ export class ScreenManagementService {
|
||||||
console.log(`컴포넌트 수: ${layoutData.components.length}`);
|
console.log(`컴포넌트 수: ${layoutData.components.length}`);
|
||||||
console.log(`격자 설정:`, layoutData.gridSettings);
|
console.log(`격자 설정:`, layoutData.gridSettings);
|
||||||
console.log(`해상도 설정:`, layoutData.screenResolution);
|
console.log(`해상도 설정:`, layoutData.screenResolution);
|
||||||
|
console.log(`기본 테이블:`, (layoutData as any).mainTableName);
|
||||||
|
|
||||||
// 권한 확인
|
// 권한 확인
|
||||||
const screens = await query<{ company_code: string | null }>(
|
const screens = await query<{ company_code: string | null }>(
|
||||||
|
|
@ -1486,6 +1487,16 @@ export class ScreenManagementService {
|
||||||
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
|
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 화면의 기본 테이블 업데이트 (테이블이 선택된 경우)
|
||||||
|
const mainTableName = (layoutData as any).mainTableName;
|
||||||
|
if (mainTableName) {
|
||||||
|
await query(
|
||||||
|
`UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`,
|
||||||
|
[mainTableName, screenId]
|
||||||
|
);
|
||||||
|
console.log(`✅ 화면 기본 테이블 업데이트: ${mainTableName}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
|
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
|
||||||
await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
|
await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,392 @@
|
||||||
|
# column_labels 테이블 제거 영향 분석
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
현재 시스템은 컬럼 메타데이터를 **두 개의 테이블**에서 관리하고 있습니다:
|
||||||
|
- `column_labels`: 레거시 테이블 (회사코드 없음, 공통 데이터)
|
||||||
|
- `table_type_columns`: 새 테이블 (회사코드 있음, 멀티테넌시 지원)
|
||||||
|
|
||||||
|
이 문서는 `column_labels` 테이블을 제거하고 `table_type_columns`로 통합할 때의 영향을 분석합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 두 테이블 스키마 비교
|
||||||
|
|
||||||
|
### column_labels (레거시)
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | INTEGER | PK |
|
||||||
|
| table_name | VARCHAR | 테이블명 |
|
||||||
|
| column_name | VARCHAR | 컬럼명 |
|
||||||
|
| column_label | VARCHAR | 한글 라벨 |
|
||||||
|
| web_type | VARCHAR | 레거시 (사용 안 함) |
|
||||||
|
| input_type | VARCHAR | 입력 타입 |
|
||||||
|
| detail_settings | TEXT | 상세 설정 (JSON) |
|
||||||
|
| description | TEXT | 설명 |
|
||||||
|
| display_order | INTEGER | 표시 순서 |
|
||||||
|
| is_visible | BOOLEAN | 표시 여부 |
|
||||||
|
| code_category | VARCHAR | 코드 카테고리 |
|
||||||
|
| code_value | VARCHAR | 코드 값 |
|
||||||
|
| reference_table | VARCHAR | 참조 테이블 |
|
||||||
|
| reference_column | VARCHAR | 참조 컬럼 |
|
||||||
|
| display_column | VARCHAR | 표시 컬럼 |
|
||||||
|
| created_date | TIMESTAMP | 생성일 |
|
||||||
|
| updated_date | TIMESTAMP | 수정일 |
|
||||||
|
|
||||||
|
**특징**: `company_code` 없음 → 멀티테넌시 불가
|
||||||
|
|
||||||
|
### table_type_columns (신규)
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | INTEGER | PK |
|
||||||
|
| table_name | VARCHAR | 테이블명 |
|
||||||
|
| column_name | VARCHAR | 컬럼명 |
|
||||||
|
| input_type | VARCHAR | 입력 타입 |
|
||||||
|
| detail_settings | TEXT | 상세 설정 (JSON) |
|
||||||
|
| is_nullable | VARCHAR | NULL 허용 |
|
||||||
|
| display_order | INTEGER | 표시 순서 |
|
||||||
|
| company_code | VARCHAR | **회사 코드** |
|
||||||
|
| created_date | TIMESTAMP | 생성일 |
|
||||||
|
| updated_date | TIMESTAMP | 수정일 |
|
||||||
|
|
||||||
|
**특징**: `company_code` 있음 → 멀티테넌시 지원
|
||||||
|
|
||||||
|
### 누락된 컬럼 (table_type_columns에 추가 필요)
|
||||||
|
|
||||||
|
| 컬럼 | 용도 |
|
||||||
|
|------|------|
|
||||||
|
| column_label | 한글 라벨 |
|
||||||
|
| description | 설명 |
|
||||||
|
| is_visible | 표시 여부 |
|
||||||
|
| code_category | 코드 카테고리 |
|
||||||
|
| code_value | 코드 값 |
|
||||||
|
| reference_table | 참조 테이블 (엔티티) |
|
||||||
|
| reference_column | 참조 컬럼 |
|
||||||
|
| display_column | 표시 컬럼 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 영향 받는 파일 목록
|
||||||
|
|
||||||
|
### 백엔드 파일 (16개, 87회 참조)
|
||||||
|
|
||||||
|
| 파일 | 참조 횟수 | 영향도 | 용도 |
|
||||||
|
|------|----------|--------|------|
|
||||||
|
| `tableManagementService.ts` | 21회 | 🔴 매우 높음 | 컬럼 조회/저장/업데이트 핵심 로직 |
|
||||||
|
| `screenGroupController.ts` | 13회 | 🟡 중간 | 화면 그룹/메뉴 동기화 시 라벨 조회 |
|
||||||
|
| `masterDetailExcelService.ts` | 7회 | 🟡 중간 | 엑셀 다운로드 시 엔티티 관계 조회 |
|
||||||
|
| `ddlExecutionService.ts` | 7회 | 🟡 중간 | 테이블 생성/삭제 시 메타데이터 등록 |
|
||||||
|
| `tableManagementController.ts` | 7회 | 🟡 중간 | API 엔드포인트 |
|
||||||
|
| `screenManagementService.ts` | 5회 | 🔴 높음 | 화면에서 컬럼 라벨 조회 |
|
||||||
|
| `entityJoinService.ts` | 4회 | 🔴 높음 | 엔티티 조인 관계 감지 |
|
||||||
|
| `entityReferenceController.ts` | 4회 | 🟡 중간 | 엔티티 참조 데이터 조회 |
|
||||||
|
| `adminController.ts` | 3회 | 🟢 낮음 | 엑셀 업로드 컬럼 매핑 |
|
||||||
|
| `dataService.ts` | 3회 | 🟢 낮음 | 컬럼 라벨 조회 |
|
||||||
|
| `flowController.ts` | 3회 | 🟢 낮음 | 플로우 컬럼 라벨 조회 |
|
||||||
|
| `categoryTreeService.ts` | 1회 | 🟢 낮음 | 카테고리 라벨 조회 |
|
||||||
|
| `tableManagementRoutes.ts` | 1회 | 🟢 낮음 | 라우트 주석 |
|
||||||
|
| `multiConnectionQueryService.ts` | 1회 | 🟢 낮음 | 멀티 연결 쿼리 |
|
||||||
|
| `migrate-input-type-to-web-type.ts` | 6회 | 🟢 낮음 | 마이그레이션 스크립트 |
|
||||||
|
| `types/ddl.ts` | 1회 | 🟢 낮음 | 타입 정의 |
|
||||||
|
|
||||||
|
### 프론트엔드 파일 (15개, 20회 참조)
|
||||||
|
|
||||||
|
| 파일 | 참조 횟수 | 영향도 | 용도 |
|
||||||
|
|------|----------|--------|------|
|
||||||
|
| `UnifiedRepeater.tsx` | 3회 | 🟢 낮음 | 타입 주석 |
|
||||||
|
| `ScreenDesigner.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
|
||||||
|
| `ButtonConfigPanel.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
|
||||||
|
| `ScreenRelationFlow.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
|
||||||
|
| `buttonActions.ts` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `webTypeMapping.ts` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `screenGroup.ts` | 1회 | 🟢 낮음 | API 타입 |
|
||||||
|
| `tableManagement.ts` | 1회 | 🟢 낮음 | API 타입 |
|
||||||
|
| `TableSettingModal.tsx` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `tableSchema.ts` | 1회 | 🟢 낮음 | 타입 |
|
||||||
|
| `types/ddl.ts` | 1회 | 🟢 낮음 | 타입 정의 |
|
||||||
|
| `ControlConditionStep.tsx` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `ActionConditionBuilder.tsx` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `WebTypeInput.tsx` | 1회 | 🟢 낮음 | 주석 |
|
||||||
|
| `types/multiConnection.ts` | 1회 | 🟢 낮음 | 타입 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 주요 사용 패턴 분석
|
||||||
|
|
||||||
|
### 패턴 1: 컬럼 조회 (가장 많음)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 현재 방식: column_labels + table_type_columns 조인
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
|
COALESCE(ttc.input_type, cl.input_type, 'text') as "inputType",
|
||||||
|
COALESCE(ttc.detail_settings, cl.detail_settings) as "detailSettings",
|
||||||
|
cl.reference_table as "referenceTable" -- ❌ 문제: ttc의 detailSettings 무시
|
||||||
|
FROM information_schema.columns c
|
||||||
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||||
|
LEFT JOIN table_type_columns ttc ON c.table_name = ttc.table_name
|
||||||
|
AND c.column_name = ttc.column_name
|
||||||
|
AND ttc.company_code = $company_code
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: `referenceTable`이 `column_labels`에서만 조회됨 → 회사별 엔티티 설정 무시
|
||||||
|
|
||||||
|
### 패턴 2: 컬럼 저장 (이중 저장)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 현재: 두 테이블에 모두 저장
|
||||||
|
await query(`INSERT INTO column_labels (...) VALUES (...) ON CONFLICT ... DO UPDATE ...`);
|
||||||
|
await this.updateColumnInputType(...); // table_type_columns에도 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: 데이터 불일치 가능성, 유지보수 어려움
|
||||||
|
|
||||||
|
### 패턴 3: 엔티티 관계 조회
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT column_name, reference_table, reference_column
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1 AND input_type = 'entity'
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: 회사별 엔티티 설정 무시 (column_labels에 company_code 없음)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 마이그레이션 계획
|
||||||
|
|
||||||
|
### Phase 1: 스키마 확장 (table_type_columns)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 마이그레이션 파일: 044_extend_table_type_columns.sql
|
||||||
|
|
||||||
|
-- 1. 누락된 컬럼 추가
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS column_label VARCHAR(200);
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS description TEXT;
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS is_visible BOOLEAN DEFAULT true;
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS code_category VARCHAR(100);
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS code_value VARCHAR(100);
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS reference_table VARCHAR(100);
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS reference_column VARCHAR(100);
|
||||||
|
ALTER TABLE table_type_columns ADD COLUMN IF NOT EXISTS display_column VARCHAR(100);
|
||||||
|
|
||||||
|
-- 2. 인덱스 추가
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ttc_reference_table ON table_type_columns(reference_table);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ttc_input_type ON table_type_columns(input_type);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 데이터 마이그레이션
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- column_labels 데이터를 table_type_columns로 이관 (company_code = '*' 로 공통 데이터)
|
||||||
|
INSERT INTO table_type_columns (
|
||||||
|
table_name, column_name, input_type, detail_settings,
|
||||||
|
column_label, description, is_visible, code_category, code_value,
|
||||||
|
reference_table, reference_column, display_column, display_order,
|
||||||
|
company_code, created_date, updated_date
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
table_name, column_name,
|
||||||
|
COALESCE(input_type, 'text'),
|
||||||
|
detail_settings,
|
||||||
|
column_label,
|
||||||
|
description,
|
||||||
|
COALESCE(is_visible, true),
|
||||||
|
code_category,
|
||||||
|
code_value,
|
||||||
|
reference_table,
|
||||||
|
reference_column,
|
||||||
|
display_column,
|
||||||
|
display_order,
|
||||||
|
'*', -- 공통 데이터 (회사별 설정 없으면 폴백)
|
||||||
|
COALESCE(created_date, NOW()),
|
||||||
|
COALESCE(updated_date, NOW())
|
||||||
|
FROM column_labels
|
||||||
|
ON CONFLICT (table_name, column_name, company_code)
|
||||||
|
DO UPDATE SET
|
||||||
|
column_label = COALESCE(EXCLUDED.column_label, table_type_columns.column_label),
|
||||||
|
description = COALESCE(EXCLUDED.description, table_type_columns.description),
|
||||||
|
reference_table = COALESCE(EXCLUDED.reference_table, table_type_columns.reference_table),
|
||||||
|
reference_column = COALESCE(EXCLUDED.reference_column, table_type_columns.reference_column),
|
||||||
|
display_column = COALESCE(EXCLUDED.display_column, table_type_columns.display_column),
|
||||||
|
updated_date = NOW();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: 코드 수정
|
||||||
|
|
||||||
|
#### 3.1 조회 쿼리 변경 패턴
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 변경 전
|
||||||
|
SELECT ... FROM column_labels WHERE table_name = $1
|
||||||
|
|
||||||
|
-- 변경 후 (회사코드 폴백 포함)
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT *,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY table_name, column_name
|
||||||
|
ORDER BY CASE WHEN company_code = $company_code THEN 0 ELSE 1 END
|
||||||
|
) as rn
|
||||||
|
FROM table_type_columns
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND company_code IN ($company_code, '*')
|
||||||
|
) ranked
|
||||||
|
WHERE rn = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 저장 쿼리 변경 패턴
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 변경 전: 두 테이블에 저장
|
||||||
|
INSERT INTO column_labels (...) ...;
|
||||||
|
INSERT INTO table_type_columns (...) ...;
|
||||||
|
|
||||||
|
-- 변경 후: 하나의 테이블만
|
||||||
|
INSERT INTO table_type_columns (
|
||||||
|
table_name, column_name, input_type, detail_settings,
|
||||||
|
column_label, description, is_visible, code_category, code_value,
|
||||||
|
reference_table, reference_column, display_column, display_order,
|
||||||
|
company_code, created_date, updated_date
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), NOW())
|
||||||
|
ON CONFLICT (table_name, column_name, company_code)
|
||||||
|
DO UPDATE SET ...;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: 레거시 코드 정리
|
||||||
|
|
||||||
|
1. `column_labels` 관련 INSERT/UPDATE 제거
|
||||||
|
2. `column_labels` LEFT JOIN을 `table_type_columns`로 변경
|
||||||
|
3. 프론트엔드 주석/타입 업데이트
|
||||||
|
|
||||||
|
### Phase 5: 테이블 삭제 (최종)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 모든 코드 마이그레이션 완료 후
|
||||||
|
DROP TABLE IF EXISTS column_labels;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 수정해야 할 파일 상세
|
||||||
|
|
||||||
|
### 🔴 우선순위 1: 핵심 서비스 (3개)
|
||||||
|
|
||||||
|
#### tableManagementService.ts (21개 쿼리)
|
||||||
|
|
||||||
|
| 함수 | 라인 | 수정 내용 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `checkCodeTypeColumn` | 30-36 | column_labels → table_type_columns |
|
||||||
|
| `getColumnList` | 215-218, 257-258 | JOIN 변경 + referenceTable 추출 |
|
||||||
|
| `updateColumnSettings` | 460-494 | column_labels INSERT 제거 |
|
||||||
|
| `getColumnLabels` | 670-673 | table_type_columns로 변경 |
|
||||||
|
| `setColumnInputType` | 735-740 | column_labels INSERT 제거 |
|
||||||
|
| `getFileColumns` | 1288 | table_type_columns로 변경 |
|
||||||
|
| `getColumnMetaInfo` | 1956 | table_type_columns로 변경 |
|
||||||
|
| `findEntityRelation` | 3579-3590 | table_type_columns로 변경 |
|
||||||
|
| `upsertColumnLabel` | 3723 | table_type_columns로 변경 |
|
||||||
|
| `getColumnInputTypes` | 4129 | 이미 table_type_columns 사용 중 ✅ |
|
||||||
|
| `detectEntityRelation` | 4810, 4838 | table_type_columns로 변경 |
|
||||||
|
|
||||||
|
#### screenManagementService.ts (5개 쿼리)
|
||||||
|
|
||||||
|
| 함수 | 라인 | 수정 내용 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `getTableColumns` | 1279 | table_type_columns로 변경 |
|
||||||
|
| `getTableColumns` | 1334 | 라벨 추가 로직 수정 |
|
||||||
|
| `getColumnInfo` | 2083 | table_type_columns로 변경 |
|
||||||
|
| `saveColumnSettings` | 2104 | table_type_columns로 변경 |
|
||||||
|
|
||||||
|
#### entityJoinService.ts (4개 쿼리)
|
||||||
|
|
||||||
|
| 함수 | 라인 | 수정 내용 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `detectEntityColumns` | 36 | table_type_columns로 변경 |
|
||||||
|
| `getTableColumns` | 755 | table_type_columns로 변경 |
|
||||||
|
|
||||||
|
### 🟡 우선순위 2: 보조 서비스 (5개)
|
||||||
|
|
||||||
|
| 파일 | 수정 쿼리 수 | 수정 내용 |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| `screenGroupController.ts` | 13개 | 라벨 조회, FK 조회 쿼리 변경 |
|
||||||
|
| `masterDetailExcelService.ts` | 7개 | 엔티티 관계 조회 변경 |
|
||||||
|
| `ddlExecutionService.ts` | 7개 | 테이블 생성 시 메타데이터 등록 변경 |
|
||||||
|
| `entityReferenceController.ts` | 4개 | 참조 데이터 조회 변경 |
|
||||||
|
| `adminController.ts` | 3개 | 스키마 조회 변경 |
|
||||||
|
|
||||||
|
### 🟢 우선순위 3: 기타 (8개)
|
||||||
|
|
||||||
|
| 파일 | 수정 내용 |
|
||||||
|
|------|----------|
|
||||||
|
| `dataService.ts` | 라벨 조회 변경 |
|
||||||
|
| `flowController.ts` | 라벨 조회 변경 |
|
||||||
|
| `categoryTreeService.ts` | JOIN 변경 |
|
||||||
|
| `tableManagementRoutes.ts` | 주석 수정 |
|
||||||
|
| `multiConnectionQueryService.ts` | 주석/타입 수정 |
|
||||||
|
| `migrate-input-type-to-web-type.ts` | 마이그레이션 스크립트 (이미 실행됨, 삭제 가능) |
|
||||||
|
| `types/ddl.ts` | 타입 정의 수정 |
|
||||||
|
| 프론트엔드 15개 파일 | 주석/타입 수정 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 예상 작업 시간
|
||||||
|
|
||||||
|
| 단계 | 작업 | 예상 시간 |
|
||||||
|
|------|------|----------|
|
||||||
|
| Phase 1 | 스키마 확장 (마이그레이션 SQL) | 30분 |
|
||||||
|
| Phase 2 | 데이터 마이그레이션 (SQL) | 30분 |
|
||||||
|
| Phase 3.1 | tableManagementService.ts 수정 | 2시간 |
|
||||||
|
| Phase 3.2 | screenManagementService.ts 수정 | 1시간 |
|
||||||
|
| Phase 3.3 | entityJoinService.ts 수정 | 30분 |
|
||||||
|
| Phase 3.4 | 보조 서비스 5개 수정 | 2시간 |
|
||||||
|
| Phase 3.5 | 기타 파일 8개 수정 | 1시간 |
|
||||||
|
| Phase 4 | 테스트 및 검증 | 2시간 |
|
||||||
|
| Phase 5 | column_labels 삭제 | 10분 |
|
||||||
|
| **합계** | | **약 10시간** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 리스크 및 주의사항
|
||||||
|
|
||||||
|
### 높은 리스크
|
||||||
|
|
||||||
|
1. **데이터 불일치**: 마이그레이션 중 column_labels와 table_type_columns 데이터 충돌 가능
|
||||||
|
2. **회사코드 폴백 로직 복잡성**: 모든 조회에 `company_code IN ($code, '*')` + 우선순위 필요
|
||||||
|
3. **기존 운영 데이터 손실**: 마이그레이션 실수 시 column_labels 데이터 유실 가능
|
||||||
|
|
||||||
|
### 완화 방안
|
||||||
|
|
||||||
|
1. **단계적 마이그레이션**: column_labels는 당분간 유지, 조회만 table_type_columns 우선으로 변경
|
||||||
|
2. **폴백 헬퍼 함수**: 회사코드 폴백 로직을 공통 함수로 추출
|
||||||
|
3. **백업 필수**: 마이그레이션 전 column_labels 전체 백업
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 결론
|
||||||
|
|
||||||
|
### 현재 문제점
|
||||||
|
|
||||||
|
1. **이중 관리**: 같은 데이터가 두 테이블에 저장됨
|
||||||
|
2. **멀티테넌시 불완전**: `referenceTable` 등이 `column_labels`에서만 조회되어 회사별 설정 무시
|
||||||
|
3. **유지보수 어려움**: 변경 시 두 곳 모두 수정 필요
|
||||||
|
|
||||||
|
### 권장 방향
|
||||||
|
|
||||||
|
**장기적으로 `table_type_columns`로 통합 권장**
|
||||||
|
|
||||||
|
하지만 작업량이 상당하므로:
|
||||||
|
|
||||||
|
1. **단기 (즉시)**: 조회 시 `detailSettings`에서 `referenceTable` 우선 추출하도록 수정
|
||||||
|
2. **중기 (1-2주)**: `table_type_columns` 스키마 확장 + 데이터 마이그레이션
|
||||||
|
3. **장기 (한 달)**: 모든 코드 수정 후 `column_labels` 제거
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- `table_type_columns` 관련 마이그레이션: `db/migrations/030_create_table_type_columns.sql`
|
||||||
|
- 테이블 타입 관리 UI: `frontend/app/(main)/admin/systemMng/tableMngList/page.tsx`
|
||||||
|
- 컬럼 조회 핵심 로직: `backend-node/src/services/tableManagementService.ts:getColumnList()`
|
||||||
|
|
||||||
|
|
@ -12,9 +12,9 @@ import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorTyp
|
||||||
import { NumberingRuleCard } from "./NumberingRuleCard";
|
import { NumberingRuleCard } from "./NumberingRuleCard";
|
||||||
import { NumberingRulePreview } from "./NumberingRulePreview";
|
import { NumberingRulePreview } from "./NumberingRulePreview";
|
||||||
import {
|
import {
|
||||||
getAvailableNumberingRules,
|
|
||||||
saveNumberingRuleToTest,
|
saveNumberingRuleToTest,
|
||||||
deleteNumberingRuleFromTest,
|
deleteNumberingRuleFromTest,
|
||||||
|
getNumberingRulesFromTest,
|
||||||
} from "@/lib/api/numberingRule";
|
} from "@/lib/api/numberingRule";
|
||||||
import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree";
|
import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
|
@ -159,14 +159,15 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
const loadRules = useCallback(async () => {
|
const loadRules = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작:", {
|
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", {
|
||||||
menuObjid,
|
menuObjid,
|
||||||
hasMenuObjid: !!menuObjid,
|
hasMenuObjid: !!menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await getAvailableNumberingRules(menuObjid);
|
// test 테이블에서 조회
|
||||||
|
const response = await getNumberingRulesFromTest(menuObjid);
|
||||||
|
|
||||||
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답:", {
|
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", {
|
||||||
menuObjid,
|
menuObjid,
|
||||||
success: response.success,
|
success: response.success,
|
||||||
rulesCount: response.data?.length || 0,
|
rulesCount: response.data?.length || 0,
|
||||||
|
|
|
||||||
|
|
@ -265,8 +265,10 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||||
setSelectedRestApiId(null);
|
setSelectedRestApiId(null);
|
||||||
setRestApiEndpoint("");
|
setRestApiEndpoint("");
|
||||||
setRestApiJsonPath("data");
|
setRestApiJsonPath("data");
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
// 필요 시 토스트 추가 가능
|
console.error("화면 생성 실패:", e);
|
||||||
|
const errorMessage = e?.response?.data?.message || e?.message || "화면 생성에 실패했습니다.";
|
||||||
|
alert(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1650,10 +1650,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
});
|
});
|
||||||
|
|
||||||
// 해상도 정보를 포함한 레이아웃 데이터 생성
|
// 해상도 정보를 포함한 레이아웃 데이터 생성
|
||||||
|
// 현재 선택된 테이블을 화면의 기본 테이블로 저장
|
||||||
|
const currentMainTableName = tables.length > 0 ? tables[0].tableName : null;
|
||||||
|
|
||||||
const layoutWithResolution = {
|
const layoutWithResolution = {
|
||||||
...layout,
|
...layout,
|
||||||
components: updatedComponents,
|
components: updatedComponents,
|
||||||
screenResolution: screenResolution,
|
screenResolution: screenResolution,
|
||||||
|
mainTableName: currentMainTableName, // 화면의 기본 테이블
|
||||||
};
|
};
|
||||||
// 🔍 버튼 컴포넌트들의 action.type 확인
|
// 🔍 버튼 컴포넌트들의 action.type 확인
|
||||||
const buttonComponents = layoutWithResolution.components.filter(
|
const buttonComponents = layoutWithResolution.components.filter(
|
||||||
|
|
@ -1687,7 +1691,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [selectedScreen, layout, screenResolution]);
|
}, [selectedScreen, layout, screenResolution, tables]);
|
||||||
|
|
||||||
// 다국어 자동 생성 핸들러
|
// 다국어 자동 생성 핸들러
|
||||||
const handleGenerateMultilang = useCallback(async () => {
|
const handleGenerateMultilang = useCallback(async () => {
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,12 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Dialog,
|
||||||
PopoverContent,
|
DialogContent,
|
||||||
PopoverTrigger,
|
DialogHeader,
|
||||||
} from "@/components/ui/popover";
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface GridSettings {
|
interface GridSettings {
|
||||||
columns: number;
|
columns: number;
|
||||||
|
|
@ -173,7 +175,6 @@ export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
||||||
{screenResolution && (
|
{screenResolution && (
|
||||||
<>
|
<>
|
||||||
<div className="h-6 w-px bg-gray-300" />
|
<div className="h-6 w-px bg-gray-300" />
|
||||||
<Popover open={showCustomInput} onOpenChange={setShowCustomInput}>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="flex items-center space-x-2 rounded-md bg-blue-50 px-3 py-1.5 transition-colors hover:bg-blue-100">
|
<button className="flex items-center space-x-2 rounded-md bg-blue-50 px-3 py-1.5 transition-colors hover:bg-blue-100">
|
||||||
|
|
@ -234,8 +235,7 @@ export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuLabel className="text-xs text-gray-500">사용자 정의</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-xs text-gray-500">사용자 정의</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
e.preventDefault();
|
|
||||||
setCustomWidth(screenResolution.width.toString());
|
setCustomWidth(screenResolution.width.toString());
|
||||||
setCustomHeight(screenResolution.height.toString());
|
setCustomHeight(screenResolution.height.toString());
|
||||||
setShowCustomInput(true);
|
setShowCustomInput(true);
|
||||||
|
|
@ -248,37 +248,45 @@ export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<PopoverContent align="start" className="w-64 p-3">
|
|
||||||
<div className="space-y-3">
|
{/* 사용자 정의 해상도 다이얼로그 */}
|
||||||
<div className="text-sm font-medium">사용자 정의 해상도</div>
|
<Dialog open={showCustomInput} onOpenChange={setShowCustomInput}>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<DialogContent className="sm:max-w-[320px]">
|
||||||
<div className="space-y-1">
|
<DialogHeader>
|
||||||
<Label className="text-xs text-gray-500">너비 (px)</Label>
|
<DialogTitle>사용자 정의 해상도</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid grid-cols-2 gap-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="customWidth" className="text-sm">너비 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="customWidth"
|
||||||
type="number"
|
type="number"
|
||||||
value={customWidth}
|
value={customWidth}
|
||||||
onChange={(e) => setCustomWidth(e.target.value)}
|
onChange={(e) => setCustomWidth(e.target.value)}
|
||||||
placeholder="1920"
|
placeholder="1920"
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs text-gray-500">높이 (px)</Label>
|
<Label htmlFor="customHeight" className="text-sm">높이 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="customHeight"
|
||||||
type="number"
|
type="number"
|
||||||
value={customHeight}
|
value={customHeight}
|
||||||
onChange={(e) => setCustomHeight(e.target.value)}
|
onChange={(e) => setCustomHeight(e.target.value)}
|
||||||
placeholder="1080"
|
placeholder="1080"
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCustomResolution} size="sm" className="w-full">
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowCustomInput(false)}>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCustomResolution}>
|
||||||
적용
|
적용
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</PopoverContent>
|
</DialogContent>
|
||||||
</Popover>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||||
import { FolderTree, Loader2 } from "lucide-react";
|
import { FolderTree, Loader2, Search, X } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
interface CategoryColumn {
|
interface CategoryColumn {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
|
@ -28,17 +29,29 @@ interface CategoryColumnListProps {
|
||||||
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) {
|
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) {
|
||||||
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
// 검색어로 필터링된 컬럼 목록
|
||||||
|
const filteredColumns = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return columns;
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return columns.filter((col) => {
|
||||||
|
const columnName = (col.columnName || "").toLowerCase();
|
||||||
|
const columnLabel = (col.columnLabel || "").toLowerCase();
|
||||||
|
const tableName = (col.tableName || "").toLowerCase();
|
||||||
|
const tableLabel = (col.tableLabel || "").toLowerCase();
|
||||||
|
|
||||||
|
return columnName.includes(query) ||
|
||||||
|
columnLabel.includes(query) ||
|
||||||
|
tableName.includes(query) ||
|
||||||
|
tableLabel.includes(query);
|
||||||
|
});
|
||||||
|
}, [columns, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuObjid) {
|
// 메뉴 종속 없이 항상 회사 기준으로 카테고리 컬럼 조회
|
||||||
loadCategoryColumnsByMenu();
|
loadCategoryColumnsByMenu();
|
||||||
} else if (tableName) {
|
|
||||||
// menuObjid가 없으면 tableName 기반으로 조회
|
|
||||||
loadCategoryColumnsByTable();
|
|
||||||
} else {
|
|
||||||
console.warn("⚠️ menuObjid와 tableName 모두 없어서 카테고리 컬럼을 로드할 수 없습니다");
|
|
||||||
setColumns([]);
|
|
||||||
}
|
|
||||||
}, [menuObjid, tableName]);
|
}, [menuObjid, tableName]);
|
||||||
|
|
||||||
// tableName 기반으로 카테고리 컬럼 조회
|
// tableName 기반으로 카테고리 컬럼 조회
|
||||||
|
|
@ -126,10 +139,13 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
||||||
const loadCategoryColumnsByMenu = async () => {
|
const loadCategoryColumnsByMenu = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
console.log("🔍 형제 메뉴의 카테고리 컬럼 조회 시작", { menuObjid });
|
console.log("🔍 회사 기준 카테고리 컬럼 조회 시작", { menuObjid });
|
||||||
|
|
||||||
// 새 API: 형제 메뉴들의 카테고리 컬럼 조회
|
// 회사 기준 카테고리 컬럼 조회 (menuObjid는 선택사항)
|
||||||
const response = await apiClient.get(`/table-management/menu/${menuObjid}/category-columns`);
|
const url = menuObjid
|
||||||
|
? `/table-management/menu/${menuObjid}/category-columns`
|
||||||
|
: `/table-management/category-columns`;
|
||||||
|
const response = await apiClient.get(url);
|
||||||
|
|
||||||
console.log("✅ 메뉴별 카테고리 컬럼 API 응답:", {
|
console.log("✅ 메뉴별 카테고리 컬럼 API 응답:", {
|
||||||
menuObjid,
|
menuObjid,
|
||||||
|
|
@ -242,8 +258,33 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
||||||
<p className="text-muted-foreground text-xs">관리할 카테고리 컬럼을 선택하세요</p>
|
<p className="text-muted-foreground text-xs">관리할 카테고리 컬럼을 선택하세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 검색 입력 필드 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="text-muted-foreground absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="컬럼 검색..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="h-8 pl-8 pr-8 text-xs"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
className="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{columns.map((column) => {
|
{filteredColumns.length === 0 && searchQuery ? (
|
||||||
|
<div className="text-muted-foreground py-4 text-center text-xs">
|
||||||
|
'{searchQuery}'에 대한 검색 결과가 없습니다
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{filteredColumns.map((column) => {
|
||||||
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
||||||
const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
|
const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -251,29 +251,57 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
loadTables();
|
loadTables();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반)
|
// 연관 테이블 목록 로드 (엔티티 관계 기반)
|
||||||
|
// 1. 화면 메인 테이블이 있으면: 그 테이블을 참조하는 테이블 (자식 테이블)
|
||||||
|
// 2. 저장 테이블이 선택되었으면: 그 테이블을 참조하는 테이블도 추가
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRelatedTables = async () => {
|
const loadRelatedTables = async () => {
|
||||||
if (!currentTableName) {
|
// 화면 메인 테이블 또는 저장 테이블 중 하나라도 있어야 함
|
||||||
|
const baseTable = currentTableName || config.mainTableName;
|
||||||
|
if (!baseTable) {
|
||||||
setRelatedTables([]);
|
setRelatedTables([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingRelations(true);
|
setLoadingRelations(true);
|
||||||
try {
|
try {
|
||||||
// column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기
|
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
const allRelations: TableRelation[] = [];
|
||||||
|
|
||||||
|
// 1. 화면 메인 테이블을 참조하는 테이블 조회 (자식 테이블)
|
||||||
|
if (currentTableName) {
|
||||||
|
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const relations: TableRelation[] = response.data.data.map((rel: any) => ({
|
const relations: TableRelation[] = response.data.data.map((rel: any) => ({
|
||||||
tableName: rel.tableName || rel.table_name,
|
tableName: rel.tableName || rel.table_name,
|
||||||
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
||||||
foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼
|
foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼 (자식 테이블의)
|
||||||
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼
|
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼 (부모 테이블의)
|
||||||
}));
|
}));
|
||||||
setRelatedTables(relations);
|
allRelations.push(...relations);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 저장 테이블이 화면 메인 테이블과 다르면, 저장 테이블을 참조하는 테이블도 조회
|
||||||
|
if (config.mainTableName && config.mainTableName !== currentTableName) {
|
||||||
|
const response2 = await apiClient.get(`/table-management/columns/${config.mainTableName}/referenced-by`);
|
||||||
|
if (response2.data.success && response2.data.data) {
|
||||||
|
const relations2: TableRelation[] = response2.data.data.map((rel: any) => ({
|
||||||
|
tableName: rel.tableName || rel.table_name,
|
||||||
|
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
||||||
|
foreignKeyColumn: rel.columnName || rel.column_name,
|
||||||
|
referenceColumn: rel.referenceColumn || rel.reference_column || "id",
|
||||||
|
}));
|
||||||
|
// 중복 제거 후 추가
|
||||||
|
relations2.forEach(rel => {
|
||||||
|
if (!allRelations.some(r => r.tableName === rel.tableName)) {
|
||||||
|
allRelations.push(rel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelatedTables(allRelations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("연관 테이블 로드 실패:", error);
|
console.error("연관 테이블 로드 실패:", error);
|
||||||
setRelatedTables([]);
|
setRelatedTables([]);
|
||||||
|
|
@ -282,7 +310,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadRelatedTables();
|
loadRelatedTables();
|
||||||
}, [currentTableName]);
|
}, [currentTableName, config.mainTableName]);
|
||||||
|
|
||||||
// 설정 업데이트 헬퍼
|
// 설정 업데이트 헬퍼
|
||||||
const updateConfig = useCallback(
|
const updateConfig = useCallback(
|
||||||
|
|
@ -806,16 +834,17 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{/* FK 직접 입력 (연관 테이블이 아닌 경우만) */}
|
{/* FK 직접 입력 - 화면 메인 테이블이 있고 연관 테이블이 아닌 경우만 표시 */}
|
||||||
{config.useCustomTable && config.mainTableName &&
|
{/* 화면 메인 테이블이 없으면 FK 설정 불필요 (독립 저장) */}
|
||||||
|
{config.useCustomTable && config.mainTableName && currentTableName &&
|
||||||
!relatedTables.some(r => r.tableName === config.mainTableName) && (
|
!relatedTables.some(r => r.tableName === config.mainTableName) && (
|
||||||
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
|
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
|
||||||
<p className="text-[10px] text-amber-700">
|
<p className="text-[10px] text-amber-700">
|
||||||
엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요.
|
화면 테이블({currentTableName})과의 엔티티 관계가 없습니다. FK 컬럼을 직접 입력하세요.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">FK 컬럼</Label>
|
<Label className="text-[10px]">FK 컬럼 (저장 테이블)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={config.foreignKeyColumn || ""}
|
value={config.foreignKeyColumn || ""}
|
||||||
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
||||||
|
|
@ -824,7 +853,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">PK 컬럼</Label>
|
<Label className="text-[10px]">PK 컬럼 (화면 테이블)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={config.foreignKeySourceColumn || "id"}
|
value={config.foreignKeySourceColumn || "id"}
|
||||||
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
||||||
|
|
@ -835,26 +864,31 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 화면 메인 테이블이 없을 때 안내 */}
|
||||||
|
{config.useCustomTable && config.mainTableName && !currentTableName && (
|
||||||
|
<div className="rounded border border-blue-200 bg-blue-50 p-2">
|
||||||
|
<p className="text-[10px] text-blue-700">
|
||||||
|
독립 저장 모드: 화면 테이블 없이 직접 저장합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* 현재 화면 정보 */}
|
{/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */}
|
||||||
|
{currentTableName && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">현재 화면</Label>
|
<Label className="text-xs font-medium">메인 화면 테이블 (참고)</Label>
|
||||||
{currentTableName ? (
|
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
||||||
<div className="rounded border border-blue-200 bg-blue-50 p-2">
|
<p className="text-xs text-gray-700 font-medium">{currentTableName}</p>
|
||||||
<p className="text-xs text-blue-700 font-medium">{currentTableName}</p>
|
<p className="text-[10px] text-gray-500">
|
||||||
<p className="text-[10px] text-blue-500">
|
|
||||||
컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개
|
컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="rounded border border-amber-200 bg-amber-50 p-2">
|
|
||||||
<p className="text-[10px] text-amber-600">화면에 테이블을 먼저 설정해주세요</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 모달 모드: 엔티티 컬럼 선택 */}
|
{/* 모달 모드: 엔티티 컬럼 선택 */}
|
||||||
{isModalMode && (
|
{isModalMode && (
|
||||||
|
|
@ -870,7 +904,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
<Select
|
<Select
|
||||||
value={config.dataSource?.foreignKey || ""}
|
value={config.dataSource?.foreignKey || ""}
|
||||||
onValueChange={handleEntityColumnSelect}
|
onValueChange={handleEntityColumnSelect}
|
||||||
disabled={!currentTableName}
|
disabled={!targetTableForColumns}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="엔티티 컬럼 선택" />
|
<SelectValue placeholder="엔티티 컬럼 선택" />
|
||||||
|
|
@ -891,7 +925,11 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
||||||
<p className="text-[10px] text-gray-500">
|
<p className="text-[10px] text-gray-500">
|
||||||
{loadingColumns ? "로딩 중..." : "엔티티 타입 컬럼이 없습니다"}
|
{loadingColumns
|
||||||
|
? "로딩 중..."
|
||||||
|
: !targetTableForColumns
|
||||||
|
? "저장 테이블을 먼저 선택하세요"
|
||||||
|
: "엔티티 타입 컬럼이 없습니다"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,29 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 테스트용 API (menu_objid 없는 방식) ======
|
// ====== 테스트용 API (numbering_rules_test 테이블 사용) ======
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [테스트] 테스트 테이블에서 채번규칙 목록 조회
|
||||||
|
* numbering_rules_test 테이블 사용
|
||||||
|
* @param menuObjid 메뉴 OBJID (선택) - 필터링용
|
||||||
|
*/
|
||||||
|
export async function getNumberingRulesFromTest(
|
||||||
|
menuObjid?: number
|
||||||
|
): Promise<ApiResponse<NumberingRuleConfig[]>> {
|
||||||
|
try {
|
||||||
|
const url = menuObjid
|
||||||
|
? `/numbering-rules/test/list/${menuObjid}`
|
||||||
|
: "/numbering-rules/test/list";
|
||||||
|
const response = await apiClient.get(url);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.response?.data?.error || error.message || "테스트 규칙 목록 조회 실패",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [테스트] 테이블+컬럼 기반 채번규칙 조회
|
* [테스트] 테이블+컬럼 기반 채번규칙 조회
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue