feat: 테스트 테이블에서 채번 규칙 목록 조회 API 추가 및 회사별 카테고리 컬럼 조회 기능 구현
- 테스트 테이블에서 채번 규칙 목록을 조회하는 API를 추가하였습니다. 이 API는 회사 코드와 선택적 메뉴 OBJID를 기반으로 규칙을 반환합니다. - 회사별 카테고리 컬럼을 조회하는 API를 추가하여, 회사 코드에 따라 카테고리 컬럼을 필터링하여 반환하도록 개선하였습니다. - 관련된 서비스 및 라우터를 업데이트하여 새로운 기능을 통합하였습니다.
This commit is contained in:
parent
9b8546ebef
commit
1753822211
|
|
@ -269,6 +269,23 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques
|
|||
|
||||
// ==================== 테스트 테이블용 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) => {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!companyCode) {
|
||||
logger.error("❌ 회사 코드가 없습니다", { menuObjid, user: req.user });
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "회사 코드를 확인할 수 없습니다. 다시 로그인해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { getPool } = await import("../database/db");
|
||||
const pool = getPool();
|
||||
|
||||
// 1. category_column_mapping 테이블 존재 여부 확인
|
||||
const tableExistsResult = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'category_column_mapping'
|
||||
) as table_exists
|
||||
`);
|
||||
const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true;
|
||||
// 🆕 table_type_columns에서 직접 input_type = 'category'인 컬럼들을 조회
|
||||
// category_column_mapping 대신 table_type_columns 기준으로 조회
|
||||
logger.info("🔍 table_type_columns 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// 상위 메뉴들에 설정된 모든 카테고리 컬럼 조회 (테이블 필터링 제거)
|
||||
|
||||
// 최고 관리자인 경우 모든 회사의 카테고리 컬럼 조회
|
||||
if (companyCode === "*") {
|
||||
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;
|
||||
}
|
||||
|
||||
const columnsQuery = `
|
||||
SELECT
|
||||
ttc.table_name AS "tableName",
|
||||
COALESCE(
|
||||
tl.table_label,
|
||||
|
|
@ -1806,14 +1816,46 @@ export async function getCategoryColumnsByMenu(
|
|||
AND ttc.column_name = cl.column_name
|
||||
LEFT JOIN table_labels tl
|
||||
ON ttc.table_name = tl.table_name
|
||||
WHERE ttc.table_name = ANY($1)
|
||||
AND ttc.company_code = $2
|
||||
AND ttc.input_type = 'category'
|
||||
WHERE ttc.input_type = 'category'
|
||||
AND ttc.company_code = '*'
|
||||
ORDER BY ttc.table_name, ttc.column_name
|
||||
`;
|
||||
|
||||
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
||||
logger.info("✅ 레거시 방식 조회 완료", { rowCount: columnsResult.rows.length });
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
getLogData,
|
||||
toggleLogTable,
|
||||
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||
getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회
|
||||
multiTableSave, // 🆕 범용 다중 테이블 저장
|
||||
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
||||
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
||||
|
|
@ -212,6 +213,12 @@ router.post("/tables/:tableName/log/toggle", toggleLogTable);
|
|||
// 메뉴 기반 카테고리 관리 API
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 회사 기준 모든 카테고리 타입 컬럼 조회 (메뉴 종속 없음)
|
||||
* GET /api/table-management/category-columns
|
||||
*/
|
||||
router.get("/category-columns", getCategoryColumnsByCompany);
|
||||
|
||||
/**
|
||||
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||
* GET /api/table-management/menu/:menuObjid/category-columns
|
||||
|
|
|
|||
|
|
@ -1196,6 +1196,109 @@ class NumberingRuleService {
|
|||
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 없이)
|
||||
* numbering_rules_test 테이블 사용
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorTyp
|
|||
import { NumberingRuleCard } from "./NumberingRuleCard";
|
||||
import { NumberingRulePreview } from "./NumberingRulePreview";
|
||||
import {
|
||||
getAvailableNumberingRules,
|
||||
saveNumberingRuleToTest,
|
||||
deleteNumberingRuleFromTest,
|
||||
getNumberingRulesFromTest,
|
||||
} from "@/lib/api/numberingRule";
|
||||
import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
|
@ -159,14 +159,15 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
|||
const loadRules = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작:", {
|
||||
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", {
|
||||
menuObjid,
|
||||
hasMenuObjid: !!menuObjid,
|
||||
});
|
||||
|
||||
const response = await getAvailableNumberingRules(menuObjid);
|
||||
// test 테이블에서 조회
|
||||
const response = await getNumberingRulesFromTest(menuObjid);
|
||||
|
||||
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답:", {
|
||||
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", {
|
||||
menuObjid,
|
||||
success: response.success,
|
||||
rulesCount: response.data?.length || 0,
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface GridSettings {
|
||||
columns: number;
|
||||
|
|
@ -173,112 +175,118 @@ export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
|||
{screenResolution && (
|
||||
<>
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
<Popover open={showCustomInput} onOpenChange={setShowCustomInput}>
|
||||
<DropdownMenu>
|
||||
<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">
|
||||
{getCategoryIcon(screenResolution.category || "desktop")}
|
||||
<span className="text-sm font-medium text-blue-900">{screenResolution.name}</span>
|
||||
<span className="text-xs text-blue-600">
|
||||
({screenResolution.width} × {screenResolution.height})
|
||||
</span>
|
||||
{onResolutionChange && <ChevronDown className="h-3 w-3 text-blue-600" />}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
{onResolutionChange && (
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">데스크톱</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Monitor className="h-4 w-4 text-blue-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">태블릿</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Tablet className="h-4 w-4 text-green-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">모바일</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Smartphone className="h-4 w-4 text-purple-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">사용자 정의</DropdownMenuLabel>
|
||||
<DropdownMenu>
|
||||
<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">
|
||||
{getCategoryIcon(screenResolution.category || "desktop")}
|
||||
<span className="text-sm font-medium text-blue-900">{screenResolution.name}</span>
|
||||
<span className="text-xs text-blue-600">
|
||||
({screenResolution.width} × {screenResolution.height})
|
||||
</span>
|
||||
{onResolutionChange && <ChevronDown className="h-3 w-3 text-blue-600" />}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
{onResolutionChange && (
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">데스크톱</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCustomWidth(screenResolution.width.toString());
|
||||
setCustomHeight(screenResolution.height.toString());
|
||||
setShowCustomInput(true);
|
||||
}}
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-gray-600" />
|
||||
<span className="flex-1">사용자 정의...</span>
|
||||
<Monitor className="h-4 w-4 text-blue-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
<PopoverContent align="start" className="w-64 p-3">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium">사용자 정의 해상도</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-500">너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={customWidth}
|
||||
onChange={(e) => setCustomWidth(e.target.value)}
|
||||
placeholder="1920"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-500">높이 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={customHeight}
|
||||
onChange={(e) => setCustomHeight(e.target.value)}
|
||||
placeholder="1080"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">태블릿</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Tablet className="h-4 w-4 text-green-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">모바일</DropdownMenuLabel>
|
||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => (
|
||||
<DropdownMenuItem
|
||||
key={resolution.name}
|
||||
onClick={() => onResolutionChange(resolution)}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Smartphone className="h-4 w-4 text-purple-600" />
|
||||
<span className="flex-1">{resolution.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{resolution.width}×{resolution.height}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-gray-500">사용자 정의</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCustomWidth(screenResolution.width.toString());
|
||||
setCustomHeight(screenResolution.height.toString());
|
||||
setShowCustomInput(true);
|
||||
}}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-gray-600" />
|
||||
<span className="flex-1">사용자 정의...</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 사용자 정의 해상도 다이얼로그 */}
|
||||
<Dialog open={showCustomInput} onOpenChange={setShowCustomInput}>
|
||||
<DialogContent className="sm:max-w-[320px]">
|
||||
<DialogHeader>
|
||||
<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
|
||||
id="customWidth"
|
||||
type="number"
|
||||
value={customWidth}
|
||||
onChange={(e) => setCustomWidth(e.target.value)}
|
||||
placeholder="1920"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleCustomResolution} size="sm" className="w-full">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="customHeight" className="text-sm">높이 (px)</Label>
|
||||
<Input
|
||||
id="customHeight"
|
||||
type="number"
|
||||
value={customHeight}
|
||||
onChange={(e) => setCustomHeight(e.target.value)}
|
||||
placeholder="1080"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCustomInput(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleCustomResolution}>
|
||||
적용
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
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 {
|
||||
tableName: string;
|
||||
|
|
@ -28,17 +29,29 @@ interface CategoryColumnListProps {
|
|||
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) {
|
||||
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
||||
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(() => {
|
||||
if (menuObjid) {
|
||||
loadCategoryColumnsByMenu();
|
||||
} else if (tableName) {
|
||||
// menuObjid가 없으면 tableName 기반으로 조회
|
||||
loadCategoryColumnsByTable();
|
||||
} else {
|
||||
console.warn("⚠️ menuObjid와 tableName 모두 없어서 카테고리 컬럼을 로드할 수 없습니다");
|
||||
setColumns([]);
|
||||
}
|
||||
// 메뉴 종속 없이 항상 회사 기준으로 카테고리 컬럼 조회
|
||||
loadCategoryColumnsByMenu();
|
||||
}, [menuObjid, tableName]);
|
||||
|
||||
// tableName 기반으로 카테고리 컬럼 조회
|
||||
|
|
@ -126,10 +139,13 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
|||
const loadCategoryColumnsByMenu = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log("🔍 형제 메뉴의 카테고리 컬럼 조회 시작", { menuObjid });
|
||||
console.log("🔍 회사 기준 카테고리 컬럼 조회 시작", { menuObjid });
|
||||
|
||||
// 새 API: 형제 메뉴들의 카테고리 컬럼 조회
|
||||
const response = await apiClient.get(`/table-management/menu/${menuObjid}/category-columns`);
|
||||
// 회사 기준 카테고리 컬럼 조회 (menuObjid는 선택사항)
|
||||
const url = menuObjid
|
||||
? `/table-management/menu/${menuObjid}/category-columns`
|
||||
: `/table-management/category-columns`;
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
console.log("✅ 메뉴별 카테고리 컬럼 API 응답:", {
|
||||
menuObjid,
|
||||
|
|
@ -242,8 +258,33 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
|
|||
<p className="text-muted-foreground text-xs">관리할 카테고리 컬럼을 선택하세요</p>
|
||||
</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">
|
||||
{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 isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -251,29 +251,57 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
loadTables();
|
||||
}, []);
|
||||
|
||||
// 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반)
|
||||
// 연관 테이블 목록 로드 (엔티티 관계 기반)
|
||||
// 1. 화면 메인 테이블이 있으면: 그 테이블을 참조하는 테이블 (자식 테이블)
|
||||
// 2. 저장 테이블이 선택되었으면: 그 테이블을 참조하는 테이블도 추가
|
||||
useEffect(() => {
|
||||
const loadRelatedTables = async () => {
|
||||
if (!currentTableName) {
|
||||
// 화면 메인 테이블 또는 저장 테이블 중 하나라도 있어야 함
|
||||
const baseTable = currentTableName || config.mainTableName;
|
||||
if (!baseTable) {
|
||||
setRelatedTables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingRelations(true);
|
||||
try {
|
||||
// column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
||||
const allRelations: TableRelation[] = [];
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const relations: TableRelation[] = response.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, // FK 컬럼
|
||||
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼
|
||||
}));
|
||||
setRelatedTables(relations);
|
||||
// 1. 화면 메인 테이블을 참조하는 테이블 조회 (자식 테이블)
|
||||
if (currentTableName) {
|
||||
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
||||
if (response.data.success && response.data.data) {
|
||||
const relations: TableRelation[] = response.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, // FK 컬럼 (자식 테이블의)
|
||||
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼 (부모 테이블의)
|
||||
}));
|
||||
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) {
|
||||
console.error("연관 테이블 로드 실패:", error);
|
||||
setRelatedTables([]);
|
||||
|
|
@ -282,7 +310,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
}
|
||||
};
|
||||
loadRelatedTables();
|
||||
}, [currentTableName]);
|
||||
}, [currentTableName, config.mainTableName]);
|
||||
|
||||
// 설정 업데이트 헬퍼
|
||||
const updateConfig = useCallback(
|
||||
|
|
@ -806,16 +834,17 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* FK 직접 입력 (연관 테이블이 아닌 경우만) */}
|
||||
{config.useCustomTable && config.mainTableName &&
|
||||
{/* FK 직접 입력 - 화면 메인 테이블이 있고 연관 테이블이 아닌 경우만 표시 */}
|
||||
{/* 화면 메인 테이블이 없으면 FK 설정 불필요 (독립 저장) */}
|
||||
{config.useCustomTable && config.mainTableName && currentTableName &&
|
||||
!relatedTables.some(r => r.tableName === config.mainTableName) && (
|
||||
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
|
||||
<p className="text-[10px] text-amber-700">
|
||||
엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요.
|
||||
화면 테이블({currentTableName})과의 엔티티 관계가 없습니다. FK 컬럼을 직접 입력하세요.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">FK 컬럼</Label>
|
||||
<Label className="text-[10px]">FK 컬럼 (저장 테이블)</Label>
|
||||
<Input
|
||||
value={config.foreignKeyColumn || ""}
|
||||
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
||||
|
|
@ -824,7 +853,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">PK 컬럼</Label>
|
||||
<Label className="text-[10px]">PK 컬럼 (화면 테이블)</Label>
|
||||
<Input
|
||||
value={config.foreignKeySourceColumn || "id"}
|
||||
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
||||
|
|
@ -835,26 +864,31 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
</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>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 현재 화면 정보 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">현재 화면</Label>
|
||||
{currentTableName ? (
|
||||
<div className="rounded border border-blue-200 bg-blue-50 p-2">
|
||||
<p className="text-xs text-blue-700 font-medium">{currentTableName}</p>
|
||||
<p className="text-[10px] text-blue-500">
|
||||
{/* 현재 화면 정보 (메인 테이블이 설정된 경우에만 표시) */}
|
||||
{currentTableName && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">메인 화면 테이블 (참고)</Label>
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
||||
<p className="text-xs text-gray-700 font-medium">{currentTableName}</p>
|
||||
<p className="text-[10px] text-gray-500">
|
||||
컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개
|
||||
</p>
|
||||
</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 && (
|
||||
|
|
@ -870,7 +904,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
<Select
|
||||
value={config.dataSource?.foreignKey || ""}
|
||||
onValueChange={handleEntityColumnSelect}
|
||||
disabled={!currentTableName}
|
||||
disabled={!targetTableForColumns}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="엔티티 컬럼 선택" />
|
||||
|
|
@ -891,7 +925,11 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
) : (
|
||||
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
||||
<p className="text-[10px] text-gray-500">
|
||||
{loadingColumns ? "로딩 중..." : "엔티티 타입 컬럼이 없습니다"}
|
||||
{loadingColumns
|
||||
? "로딩 중..."
|
||||
: !targetTableForColumns
|
||||
? "저장 테이블을 먼저 선택하세요"
|
||||
: "엔티티 타입 컬럼이 없습니다"}
|
||||
</p>
|
||||
</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