feat: Add Numbering Rule APIs and Frontend Integration
- Implemented a new API endpoint to retrieve numbering rules based on table and column names, enhancing the flexibility of numbering rule management. - Added a new service method to handle the retrieval of numbering columns specific to a company, ensuring proper company code filtering. - Updated the frontend to load and display numbering columns, allowing users to select and manage numbering rules effectively. - Refactored existing logic to improve the handling of numbering rules, including fallback mechanisms for legacy data. These changes enhance the functionality and user experience in managing numbering rules within the application.
This commit is contained in:
parent
f6a02b5182
commit
2b4b7819c5
|
|
@ -405,6 +405,30 @@ router.post(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 테이블+컬럼 기반 채번 규칙 조회 (메인 API)
|
||||||
|
router.get(
|
||||||
|
"/by-column/:tableName/:columnName",
|
||||||
|
authenticateToken,
|
||||||
|
async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||||||
|
companyCode,
|
||||||
|
tableName,
|
||||||
|
columnName
|
||||||
|
);
|
||||||
|
return res.json({ success: true, data: rule });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== 테스트 테이블용 API ====================
|
// ==================== 테스트 테이블용 API ====================
|
||||||
|
|
||||||
// [테스트] 테스트 테이블에서 채번 규칙 목록 조회
|
// [테스트] 테스트 테이블에서 채번 규칙 목록 조회
|
||||||
|
|
|
||||||
|
|
@ -3019,3 +3019,72 @@ export async function toggleColumnUnique(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사별 채번 타입 컬럼 조회 (카테고리 패턴과 동일)
|
||||||
|
*
|
||||||
|
* @route GET /api/table-management/numbering-columns
|
||||||
|
*/
|
||||||
|
export async function getNumberingColumnsByCompany(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
|
||||||
|
logger.info("회사별 채번 컬럼 조회 요청", { companyCode });
|
||||||
|
|
||||||
|
if (!companyCode) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "회사 코드를 확인할 수 없습니다.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getPool } = await import("../database/db");
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const targetCompanyCode = companyCode === "*" ? "*" : 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(
|
||||||
|
ttc.column_label,
|
||||||
|
initcap(replace(ttc.column_name, '_', ' '))
|
||||||
|
) AS "columnLabel",
|
||||||
|
ttc.input_type AS "inputType"
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
LEFT JOIN table_labels tl
|
||||||
|
ON ttc.table_name = tl.table_name
|
||||||
|
WHERE ttc.input_type = 'numbering'
|
||||||
|
AND ttc.company_code = $1
|
||||||
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const columnsResult = await pool.query(columnsQuery, [targetCompanyCode]);
|
||||||
|
|
||||||
|
logger.info("채번 컬럼 조회 완료", {
|
||||||
|
companyCode,
|
||||||
|
rowCount: columnsResult.rows.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: columnsResult.rows,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("채번 컬럼 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "채번 컬럼 조회 중 오류가 발생했습니다.",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
toggleLogTable,
|
toggleLogTable,
|
||||||
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||||
getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회
|
getCategoryColumnsByCompany, // 🆕 회사별 카테고리 컬럼 조회
|
||||||
|
getNumberingColumnsByCompany, // 채번 타입 컬럼 조회
|
||||||
multiTableSave, // 🆕 범용 다중 테이블 저장
|
multiTableSave, // 🆕 범용 다중 테이블 저장
|
||||||
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
||||||
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
||||||
|
|
@ -254,6 +255,12 @@ router.post("/tables/:tableName/log/toggle", toggleLogTable);
|
||||||
*/
|
*/
|
||||||
router.get("/category-columns", getCategoryColumnsByCompany);
|
router.get("/category-columns", getCategoryColumnsByCompany);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사 기준 모든 채번 타입 컬럼 조회
|
||||||
|
* GET /api/table-management/numbering-columns
|
||||||
|
*/
|
||||||
|
router.get("/numbering-columns", getNumberingColumnsByCompany);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||||
* GET /api/table-management/menu/:menuObjid/category-columns
|
* GET /api/table-management/menu/:menuObjid/category-columns
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,7 @@ class MasterDetailExcelService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 테이블의 특정 컬럼이 채번 타입인지 확인하고, 채번 규칙 ID를 반환
|
* 특정 테이블의 특정 컬럼이 채번 타입인지 확인하고, 채번 규칙 ID를 반환
|
||||||
* 회사별 설정을 우선 조회하고, 없으면 공통(*) 설정으로 fallback
|
* numbering_rules 테이블에서 table_name + column_name + company_code로 직접 조회
|
||||||
*/
|
*/
|
||||||
private async detectNumberingRuleForColumn(
|
private async detectNumberingRuleForColumn(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
|
|
@ -502,32 +502,58 @@ class MasterDetailExcelService {
|
||||||
companyCode?: string
|
companyCode?: string
|
||||||
): Promise<{ numberingRuleId: string } | null> {
|
): Promise<{ numberingRuleId: string } | null> {
|
||||||
try {
|
try {
|
||||||
// 회사별 설정 우선, 공통 설정 fallback (company_code DESC로 회사별이 먼저)
|
// 1. table_type_columns에서 numbering 타입인지 확인
|
||||||
const companyCondition = companyCode && companyCode !== "*"
|
const companyCondition = companyCode && companyCode !== "*"
|
||||||
? `AND company_code IN ($3, '*')`
|
? `AND company_code IN ($3, '*')`
|
||||||
: `AND company_code = '*'`;
|
: `AND company_code = '*'`;
|
||||||
const params = companyCode && companyCode !== "*"
|
const ttcParams = companyCode && companyCode !== "*"
|
||||||
? [tableName, columnName, companyCode]
|
? [tableName, columnName, companyCode]
|
||||||
: [tableName, columnName];
|
: [tableName, columnName];
|
||||||
|
|
||||||
const result = await query<any>(
|
const ttcResult = await query<any>(
|
||||||
`SELECT input_type, detail_settings, company_code
|
`SELECT input_type FROM table_type_columns
|
||||||
FROM table_type_columns
|
|
||||||
WHERE table_name = $1 AND column_name = $2 ${companyCondition}
|
WHERE table_name = $1 AND column_name = $2 ${companyCondition}
|
||||||
ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
AND input_type = 'numbering' LIMIT 1`,
|
||||||
params
|
ttcParams
|
||||||
);
|
);
|
||||||
|
|
||||||
// 채번 타입인 행 찾기 (회사별 우선)
|
if (ttcResult.length === 0) return null;
|
||||||
for (const row of result) {
|
|
||||||
if (row.input_type === "numbering") {
|
|
||||||
const settings = typeof row.detail_settings === "string"
|
|
||||||
? JSON.parse(row.detail_settings || "{}")
|
|
||||||
: row.detail_settings;
|
|
||||||
|
|
||||||
if (settings?.numberingRuleId) {
|
// 2. numbering_rules에서 table_name + column_name으로 규칙 조회
|
||||||
return { numberingRuleId: settings.numberingRuleId };
|
const ruleCompanyCondition = companyCode && companyCode !== "*"
|
||||||
}
|
? `AND company_code IN ($3, '*')`
|
||||||
|
: `AND company_code = '*'`;
|
||||||
|
const ruleParams = companyCode && companyCode !== "*"
|
||||||
|
? [tableName, columnName, companyCode]
|
||||||
|
: [tableName, columnName];
|
||||||
|
|
||||||
|
const ruleResult = await query<any>(
|
||||||
|
`SELECT rule_id FROM numbering_rules
|
||||||
|
WHERE table_name = $1 AND column_name = $2 ${ruleCompanyCondition}
|
||||||
|
ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END
|
||||||
|
LIMIT 1`,
|
||||||
|
ruleParams
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ruleResult.length > 0) {
|
||||||
|
return { numberingRuleId: ruleResult[0].rule_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. fallback: detail_settings.numberingRuleId (하위 호환)
|
||||||
|
const fallbackResult = await query<any>(
|
||||||
|
`SELECT detail_settings FROM table_type_columns
|
||||||
|
WHERE table_name = $1 AND column_name = $2 ${companyCondition}
|
||||||
|
AND input_type = 'numbering'
|
||||||
|
ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||||
|
ttcParams
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const row of fallbackResult) {
|
||||||
|
const settings = typeof row.detail_settings === "string"
|
||||||
|
? JSON.parse(row.detail_settings || "{}")
|
||||||
|
: row.detail_settings;
|
||||||
|
if (settings?.numberingRuleId) {
|
||||||
|
return { numberingRuleId: settings.numberingRuleId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,7 +566,7 @@ class MasterDetailExcelService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 테이블의 모든 채번 컬럼을 한 번에 조회
|
* 특정 테이블의 모든 채번 컬럼을 한 번에 조회
|
||||||
* 회사별 설정 우선, 공통(*) 설정 fallback
|
* numbering_rules 테이블에서 table_name + column_name으로 직접 조회
|
||||||
* @returns Map<columnName, numberingRuleId>
|
* @returns Map<columnName, numberingRuleId>
|
||||||
*/
|
*/
|
||||||
private async detectAllNumberingColumns(
|
private async detectAllNumberingColumns(
|
||||||
|
|
@ -549,6 +575,7 @@ class MasterDetailExcelService {
|
||||||
): Promise<Map<string, string>> {
|
): Promise<Map<string, string>> {
|
||||||
const numberingCols = new Map<string, string>();
|
const numberingCols = new Map<string, string>();
|
||||||
try {
|
try {
|
||||||
|
// 1. table_type_columns에서 numbering 타입 컬럼 목록 조회
|
||||||
const companyCondition = companyCode && companyCode !== "*"
|
const companyCondition = companyCode && companyCode !== "*"
|
||||||
? `AND company_code IN ($2, '*')`
|
? `AND company_code IN ($2, '*')`
|
||||||
: `AND company_code = '*'`;
|
: `AND company_code = '*'`;
|
||||||
|
|
@ -556,22 +583,26 @@ class MasterDetailExcelService {
|
||||||
? [tableName, companyCode]
|
? [tableName, companyCode]
|
||||||
: [tableName];
|
: [tableName];
|
||||||
|
|
||||||
const result = await query<any>(
|
const ttcResult = await query<any>(
|
||||||
`SELECT column_name, detail_settings, company_code
|
`SELECT DISTINCT column_name FROM table_type_columns
|
||||||
FROM table_type_columns
|
WHERE table_name = $1 AND input_type = 'numbering' ${companyCondition}`,
|
||||||
WHERE table_name = $1 AND input_type = 'numbering' ${companyCondition}
|
|
||||||
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컬럼별로 회사 설정 우선 적용
|
// 2. 각 컬럼에 대해 numbering_rules에서 규칙 조회
|
||||||
for (const row of result) {
|
for (const row of ttcResult) {
|
||||||
if (numberingCols.has(row.column_name)) continue; // 이미 회사별 설정이 있으면 스킵
|
const ruleResult = await query<any>(
|
||||||
const settings = typeof row.detail_settings === "string"
|
`SELECT rule_id FROM numbering_rules
|
||||||
? JSON.parse(row.detail_settings || "{}")
|
WHERE table_name = $1 AND column_name = $2 ${companyCondition}
|
||||||
: row.detail_settings;
|
ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END
|
||||||
if (settings?.numberingRuleId) {
|
LIMIT 1`,
|
||||||
numberingCols.set(row.column_name, settings.numberingRuleId);
|
companyCode && companyCode !== "*"
|
||||||
|
? [tableName, row.column_name, companyCode]
|
||||||
|
: [tableName, row.column_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ruleResult.length > 0) {
|
||||||
|
numberingCols.set(row.column_name, ruleResult[0].rule_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1747,7 +1747,53 @@ class NumberingRuleService {
|
||||||
`;
|
`;
|
||||||
const params = [companyCode, tableName, columnName];
|
const params = [companyCode, tableName, columnName];
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
let result = await pool.query(query, params);
|
||||||
|
|
||||||
|
// fallback: column_name이 비어있는 레거시 규칙 검색
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
const fallbackQuery = `
|
||||||
|
SELECT
|
||||||
|
r.rule_id AS "ruleId",
|
||||||
|
r.rule_name AS "ruleName",
|
||||||
|
r.description,
|
||||||
|
r.separator,
|
||||||
|
r.reset_period AS "resetPeriod",
|
||||||
|
r.current_sequence AS "currentSequence",
|
||||||
|
r.table_name AS "tableName",
|
||||||
|
r.column_name AS "columnName",
|
||||||
|
r.company_code AS "companyCode",
|
||||||
|
r.category_column AS "categoryColumn",
|
||||||
|
r.category_value_id AS "categoryValueId",
|
||||||
|
cv.value_label AS "categoryValueLabel",
|
||||||
|
r.created_at AS "createdAt",
|
||||||
|
r.updated_at AS "updatedAt",
|
||||||
|
r.created_by AS "createdBy"
|
||||||
|
FROM numbering_rules r
|
||||||
|
LEFT JOIN category_values cv ON r.category_value_id = cv.value_id
|
||||||
|
WHERE r.company_code = $1
|
||||||
|
AND r.table_name = $2
|
||||||
|
AND (r.column_name IS NULL OR r.column_name = '')
|
||||||
|
AND r.category_value_id IS NULL
|
||||||
|
ORDER BY r.updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
result = await pool.query(fallbackQuery, [companyCode, tableName]);
|
||||||
|
|
||||||
|
// 찾으면 column_name 자동 업데이트 (레거시 데이터 마이그레이션)
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
const foundRule = result.rows[0];
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE numbering_rules SET column_name = $1 WHERE rule_id = $2 AND company_code = $3`,
|
||||||
|
[columnName, foundRule.ruleId, companyCode]
|
||||||
|
);
|
||||||
|
result.rows[0].columnName = columnName;
|
||||||
|
logger.info("레거시 채번 규칙 자동 매핑 완료", {
|
||||||
|
ruleId: foundRule.ruleId,
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
logger.info("테이블+컬럼 기반 채번 규칙을 찾을 수 없음", {
|
logger.info("테이블+컬럼 기반 채번 규칙을 찾을 수 없음", {
|
||||||
|
|
@ -1760,7 +1806,6 @@ class NumberingRuleService {
|
||||||
|
|
||||||
const rule = result.rows[0];
|
const rule = result.rows[0];
|
||||||
|
|
||||||
// 파트 정보 조회 (테스트 테이블)
|
|
||||||
const partsQuery = `
|
const partsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -1779,7 +1824,7 @@ class NumberingRuleService {
|
||||||
]);
|
]);
|
||||||
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
|
logger.info("테이블+컬럼 기반 채번 규칙 조회 성공", {
|
||||||
ruleId: rule.ruleId,
|
ruleId: rule.ruleId,
|
||||||
ruleName: rule.ruleName,
|
ruleName: rule.ruleName,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -669,38 +669,6 @@ export default function TableManagementPage() {
|
||||||
console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings);
|
console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
|
|
||||||
console.log("🔍 Numbering 저장 체크:", {
|
|
||||||
inputType: column.inputType,
|
|
||||||
numberingRuleId: column.numberingRuleId,
|
|
||||||
hasNumberingRuleId: !!column.numberingRuleId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (column.inputType === "numbering") {
|
|
||||||
let existingSettings: Record<string, unknown> = {};
|
|
||||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
|
||||||
try {
|
|
||||||
existingSettings = JSON.parse(finalDetailSettings);
|
|
||||||
} catch {
|
|
||||||
existingSettings = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// numberingRuleId가 있으면 저장, 없으면 제거
|
|
||||||
if (column.numberingRuleId) {
|
|
||||||
const numberingSettings = {
|
|
||||||
...existingSettings,
|
|
||||||
numberingRuleId: column.numberingRuleId,
|
|
||||||
};
|
|
||||||
finalDetailSettings = JSON.stringify(numberingSettings);
|
|
||||||
console.log("🔧 Numbering 설정 JSON 생성:", numberingSettings);
|
|
||||||
} else {
|
|
||||||
// numberingRuleId가 없으면 빈 객체
|
|
||||||
finalDetailSettings = JSON.stringify(existingSettings);
|
|
||||||
console.log("🔧 Numbering 규칙 없이 저장:", existingSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnSetting = {
|
const columnSetting = {
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
columnLabel: column.displayName,
|
columnLabel: column.displayName,
|
||||||
|
|
@ -844,28 +812,6 @@ export default function TableManagementPage() {
|
||||||
// detailSettings 계산
|
// detailSettings 계산
|
||||||
let finalDetailSettings = column.detailSettings || "";
|
let finalDetailSettings = column.detailSettings || "";
|
||||||
|
|
||||||
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
|
|
||||||
if (column.inputType === "numbering" && column.numberingRuleId) {
|
|
||||||
let existingSettings: Record<string, unknown> = {};
|
|
||||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
|
||||||
try {
|
|
||||||
existingSettings = JSON.parse(finalDetailSettings);
|
|
||||||
} catch {
|
|
||||||
existingSettings = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const numberingSettings = {
|
|
||||||
...existingSettings,
|
|
||||||
numberingRuleId: column.numberingRuleId,
|
|
||||||
};
|
|
||||||
finalDetailSettings = JSON.stringify(numberingSettings);
|
|
||||||
console.log("🔧 전체저장 - Numbering 설정 JSON 생성:", {
|
|
||||||
columnName: column.columnName,
|
|
||||||
numberingRuleId: column.numberingRuleId,
|
|
||||||
finalDetailSettings,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 Entity 타입인 경우 detailSettings에 엔티티 설정 포함
|
// 🆕 Entity 타입인 경우 detailSettings에 엔티티 설정 포함
|
||||||
if (column.inputType === "entity" && column.referenceTable) {
|
if (column.inputType === "entity" && column.referenceTable) {
|
||||||
let existingSettings: Record<string, unknown> = {};
|
let existingSettings: Record<string, unknown> = {};
|
||||||
|
|
@ -1987,118 +1933,7 @@ export default function TableManagementPage() {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* 입력 타입이 'numbering'인 경우 채번규칙 선택 */}
|
{/* 채번 타입은 옵션설정 > 채번설정에서 관리 (별도 선택 불필요) */}
|
||||||
{column.inputType === "numbering" && (
|
|
||||||
<div className="w-64">
|
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">채번규칙</label>
|
|
||||||
<Popover
|
|
||||||
open={numberingComboboxOpen[column.columnName] || false}
|
|
||||||
onOpenChange={(open) =>
|
|
||||||
setNumberingComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: open,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={numberingComboboxOpen[column.columnName] || false}
|
|
||||||
disabled={numberingRulesLoading}
|
|
||||||
className="bg-background h-8 w-full justify-between text-xs"
|
|
||||||
>
|
|
||||||
<span className="truncate">
|
|
||||||
{numberingRulesLoading
|
|
||||||
? "로딩 중..."
|
|
||||||
: column.numberingRuleId
|
|
||||||
? numberingRules.find((r) => r.ruleId === column.numberingRuleId)
|
|
||||||
?.ruleName || column.numberingRuleId
|
|
||||||
: "채번규칙 선택..."}
|
|
||||||
</span>
|
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[280px] p-0" align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="규칙 검색..." className="h-8 text-xs" />
|
|
||||||
<CommandList className="max-h-[200px]">
|
|
||||||
<CommandEmpty className="py-2 text-center text-xs">
|
|
||||||
채번규칙을 찾을 수 없습니다.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
<CommandItem
|
|
||||||
value="none"
|
|
||||||
onSelect={() => {
|
|
||||||
const columnIndex = columns.findIndex(
|
|
||||||
(c) => c.columnName === column.columnName,
|
|
||||||
);
|
|
||||||
handleColumnChange(columnIndex, "numberingRuleId", undefined);
|
|
||||||
setNumberingComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: false,
|
|
||||||
}));
|
|
||||||
// 자동 저장 제거 - 전체 저장 버튼으로 저장
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
!column.numberingRuleId ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
-- 선택 안함 --
|
|
||||||
</CommandItem>
|
|
||||||
{numberingRules.map((rule) => (
|
|
||||||
<CommandItem
|
|
||||||
key={rule.ruleId}
|
|
||||||
value={`${rule.ruleName} ${rule.ruleId}`}
|
|
||||||
onSelect={() => {
|
|
||||||
const columnIndex = columns.findIndex(
|
|
||||||
(c) => c.columnName === column.columnName,
|
|
||||||
);
|
|
||||||
// 상태 업데이트만 (자동 저장 제거)
|
|
||||||
handleColumnChange(columnIndex, "numberingRuleId", rule.ruleId);
|
|
||||||
setNumberingComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: false,
|
|
||||||
}));
|
|
||||||
// 전체 저장 버튼으로 저장
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
column.numberingRuleId === rule.ruleId
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{rule.ruleName}</span>
|
|
||||||
{rule.tableName && (
|
|
||||||
<span className="text-muted-foreground text-[10px]">
|
|
||||||
{rule.tableName}.{rule.columnName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{column.numberingRuleId && (
|
|
||||||
<div className="bg-primary/10 text-primary mt-1 flex items-center gap-1 rounded px-2 py-0.5 text-[10px]">
|
|
||||||
<Check className="h-2.5 w-2.5" />
|
|
||||||
<span>규칙 설정됨</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-4">
|
<div className="pl-4">
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Plus, Save, Edit2, Trash2, FolderTree, Check, ChevronsUpDown } from "lucide-react";
|
import { Plus, Save, Edit2, FolderTree } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||||
import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule";
|
import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule";
|
||||||
import { NumberingRuleCard } from "./NumberingRuleCard";
|
import { NumberingRuleCard } from "./NumberingRuleCard";
|
||||||
import { NumberingRulePreview } from "./NumberingRulePreview";
|
import { NumberingRulePreview } from "./NumberingRulePreview";
|
||||||
import {
|
import { saveNumberingRuleToTest } from "@/lib/api/numberingRule";
|
||||||
saveNumberingRuleToTest,
|
import { apiClient } from "@/lib/api/client";
|
||||||
deleteNumberingRuleFromTest,
|
|
||||||
getNumberingRulesFromTest,
|
|
||||||
} from "@/lib/api/numberingRule";
|
|
||||||
import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// 카테고리 값 트리 노드 타입
|
interface NumberingColumn {
|
||||||
interface CategoryValueNode {
|
tableName: string;
|
||||||
valueId: number;
|
tableLabel: string;
|
||||||
valueCode: string;
|
columnName: string;
|
||||||
valueLabel: string;
|
columnLabel: string;
|
||||||
depth: number;
|
}
|
||||||
path: string;
|
|
||||||
parentValueId: number | null;
|
interface GroupedColumns {
|
||||||
children?: CategoryValueNode[];
|
tableLabel: string;
|
||||||
|
columns: NumberingColumn[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NumberingRuleDesignerProps {
|
interface NumberingRuleDesignerProps {
|
||||||
|
|
@ -54,138 +48,100 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
currentTableName,
|
currentTableName,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
}) => {
|
}) => {
|
||||||
const [savedRules, setSavedRules] = useState<NumberingRuleConfig[]>([]);
|
const [numberingColumns, setNumberingColumns] = useState<NumberingColumn[]>([]);
|
||||||
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null);
|
||||||
const [currentRule, setCurrentRule] = useState<NumberingRuleConfig | null>(null);
|
const [currentRule, setCurrentRule] = useState<NumberingRuleConfig | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [leftTitle, setLeftTitle] = useState("저장된 규칙 목록");
|
const [columnSearch, setColumnSearch] = useState("");
|
||||||
const [rightTitle, setRightTitle] = useState("규칙 편집");
|
const [rightTitle, setRightTitle] = useState("규칙 편집");
|
||||||
const [editingLeftTitle, setEditingLeftTitle] = useState(false);
|
|
||||||
const [editingRightTitle, setEditingRightTitle] = useState(false);
|
const [editingRightTitle, setEditingRightTitle] = useState(false);
|
||||||
|
|
||||||
// 구분자 관련 상태 (개별 파트 사이 구분자)
|
// 구분자 관련 상태 (개별 파트 사이 구분자)
|
||||||
const [separatorTypes, setSeparatorTypes] = useState<Record<number, SeparatorType>>({});
|
const [separatorTypes, setSeparatorTypes] = useState<Record<number, SeparatorType>>({});
|
||||||
const [customSeparators, setCustomSeparators] = useState<Record<number, string>>({});
|
const [customSeparators, setCustomSeparators] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
// 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회
|
// 좌측: 채번 타입 컬럼 목록 로드
|
||||||
interface CategoryOption {
|
|
||||||
tableName: string;
|
|
||||||
columnName: string;
|
|
||||||
displayName: string; // "테이블명.컬럼명" 형식
|
|
||||||
}
|
|
||||||
const [allCategoryOptions, setAllCategoryOptions] = useState<CategoryOption[]>([]);
|
|
||||||
const [selectedCategoryKey, setSelectedCategoryKey] = useState<string>(""); // "tableName.columnName"
|
|
||||||
const [categoryValues, setCategoryValues] = useState<CategoryValueNode[]>([]);
|
|
||||||
const [categoryKeyOpen, setCategoryKeyOpen] = useState(false);
|
|
||||||
const [categoryValueOpen, setCategoryValueOpen] = useState(false);
|
|
||||||
const [loadingCategories, setLoadingCategories] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRules();
|
loadNumberingColumns();
|
||||||
loadAllCategoryOptions(); // 전체 카테고리 옵션 로드
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// currentRule의 categoryColumn이 변경되면 selectedCategoryKey 동기화
|
const loadNumberingColumns = async () => {
|
||||||
useEffect(() => {
|
|
||||||
if (currentRule?.categoryColumn) {
|
|
||||||
setSelectedCategoryKey(currentRule.categoryColumn);
|
|
||||||
} else {
|
|
||||||
setSelectedCategoryKey("");
|
|
||||||
}
|
|
||||||
}, [currentRule?.categoryColumn]);
|
|
||||||
|
|
||||||
// 카테고리 키 선택 시 해당 카테고리 값 로드
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedCategoryKey) {
|
|
||||||
const [tableName, columnName] = selectedCategoryKey.split(".");
|
|
||||||
if (tableName && columnName) {
|
|
||||||
loadCategoryValues(tableName, columnName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setCategoryValues([]);
|
|
||||||
}
|
|
||||||
}, [selectedCategoryKey]);
|
|
||||||
|
|
||||||
// 전체 카테고리 옵션 로드 (모든 테이블의 category 타입 컬럼)
|
|
||||||
const loadAllCategoryOptions = async () => {
|
|
||||||
try {
|
|
||||||
// category_values 테이블에서 고유한 테이블.컬럼 조합 조회
|
|
||||||
const response = await getAllCategoryKeys();
|
|
||||||
if (response.success && response.data) {
|
|
||||||
const options: CategoryOption[] = response.data.map((item) => ({
|
|
||||||
tableName: item.tableName,
|
|
||||||
columnName: item.columnName,
|
|
||||||
displayName: `${item.tableName}.${item.columnName}`,
|
|
||||||
}));
|
|
||||||
setAllCategoryOptions(options);
|
|
||||||
console.log("전체 카테고리 옵션 로드:", options);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("카테고리 옵션 목록 조회 실패:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 특정 카테고리 컬럼의 값 트리 조회
|
|
||||||
const loadCategoryValues = async (tableName: string, columnName: string) => {
|
|
||||||
setLoadingCategories(true);
|
|
||||||
try {
|
|
||||||
const response = await getCategoryTree(tableName, columnName);
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setCategoryValues(response.data);
|
|
||||||
console.log("카테고리 값 로드:", { tableName, columnName, count: response.data.length });
|
|
||||||
} else {
|
|
||||||
setCategoryValues([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("카테고리 값 트리 조회 실패:", error);
|
|
||||||
setCategoryValues([]);
|
|
||||||
} finally {
|
|
||||||
setLoadingCategories(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 카테고리 값을 플랫 리스트로 변환 (UI에서 선택용)
|
|
||||||
const flattenCategoryValues = (nodes: CategoryValueNode[], result: CategoryValueNode[] = []): CategoryValueNode[] => {
|
|
||||||
for (const node of nodes) {
|
|
||||||
result.push(node);
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
flattenCategoryValues(node.children, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const flatCategoryValues = flattenCategoryValues(categoryValues);
|
|
||||||
|
|
||||||
const loadRules = useCallback(async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", {
|
const response = await apiClient.get("/table-management/numbering-columns");
|
||||||
menuObjid,
|
if (response.data.success && response.data.data) {
|
||||||
hasMenuObjid: !!menuObjid,
|
setNumberingColumns(response.data.data);
|
||||||
});
|
|
||||||
|
|
||||||
// test 테이블에서 조회
|
|
||||||
const response = await getNumberingRulesFromTest(menuObjid);
|
|
||||||
|
|
||||||
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", {
|
|
||||||
menuObjid,
|
|
||||||
success: response.success,
|
|
||||||
rulesCount: response.data?.length || 0,
|
|
||||||
rules: response.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setSavedRules(response.data);
|
|
||||||
} else {
|
|
||||||
toast.error(response.error || "규칙 목록을 불러올 수 없습니다");
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(`로딩 실패: ${error.message}`);
|
console.error("채번 컬럼 목록 로드 실패:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [menuObjid]);
|
};
|
||||||
|
|
||||||
|
// 컬럼 선택 시 해당 컬럼의 채번 규칙 로드
|
||||||
|
const handleSelectColumn = async (tableName: string, columnName: string) => {
|
||||||
|
setSelectedColumn({ tableName, columnName });
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/numbering-rules/by-column/${tableName}/${columnName}`);
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
const rule = response.data.data as NumberingRuleConfig;
|
||||||
|
setCurrentRule(JSON.parse(JSON.stringify(rule)));
|
||||||
|
} else {
|
||||||
|
// 규칙 없으면 신규 생성 모드
|
||||||
|
const newRule: NumberingRuleConfig = {
|
||||||
|
ruleId: `rule-${Date.now()}`,
|
||||||
|
ruleName: `${columnName} 채번`,
|
||||||
|
parts: [],
|
||||||
|
separator: "-",
|
||||||
|
resetPeriod: "none",
|
||||||
|
currentSequence: 1,
|
||||||
|
scopeType: "table",
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
};
|
||||||
|
setCurrentRule(newRule);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
const newRule: NumberingRuleConfig = {
|
||||||
|
ruleId: `rule-${Date.now()}`,
|
||||||
|
ruleName: `${columnName} 채번`,
|
||||||
|
parts: [],
|
||||||
|
separator: "-",
|
||||||
|
resetPeriod: "none",
|
||||||
|
currentSequence: 1,
|
||||||
|
scopeType: "table",
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
};
|
||||||
|
setCurrentRule(newRule);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 테이블별로 그룹화
|
||||||
|
const groupedColumns = numberingColumns.reduce<Record<string, GroupedColumns>>((acc, col) => {
|
||||||
|
if (!acc[col.tableName]) {
|
||||||
|
acc[col.tableName] = { tableLabel: col.tableLabel, columns: [] };
|
||||||
|
}
|
||||||
|
acc[col.tableName].columns.push(col);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 검색 필터 적용
|
||||||
|
const filteredGroups = Object.entries(groupedColumns).filter(([tableName, group]) => {
|
||||||
|
if (!columnSearch) return true;
|
||||||
|
const search = columnSearch.toLowerCase();
|
||||||
|
return (
|
||||||
|
tableName.toLowerCase().includes(search) ||
|
||||||
|
group.tableLabel.toLowerCase().includes(search) ||
|
||||||
|
group.columns.some(
|
||||||
|
(c) => c.columnName.toLowerCase().includes(search) || c.columnLabel.toLowerCase().includes(search)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentRule) {
|
if (currentRule) {
|
||||||
|
|
@ -343,60 +299,20 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
return part;
|
return part;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
|
||||||
// menuObjid가 있으면 menu 스코프, 없으면 기존 scopeType 유지
|
|
||||||
const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null;
|
|
||||||
const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global");
|
|
||||||
|
|
||||||
const ruleToSave = {
|
const ruleToSave = {
|
||||||
...currentRule,
|
...currentRule,
|
||||||
parts: partsWithDefaults,
|
parts: partsWithDefaults,
|
||||||
scopeType: effectiveScopeType as "menu" | "global", // menuObjid 유무에 따라 결정
|
scopeType: "table" as const,
|
||||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
tableName: selectedColumn?.tableName || currentRule.tableName || "",
|
||||||
menuObjid: effectiveMenuObjid, // 메뉴 OBJID (필터링 기준)
|
columnName: selectedColumn?.columnName || currentRule.columnName || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("💾 채번 규칙 저장:", {
|
|
||||||
currentTableName,
|
|
||||||
menuObjid,
|
|
||||||
"currentRule.tableName": currentRule.tableName,
|
|
||||||
"currentRule.menuObjid": currentRule.menuObjid,
|
|
||||||
"ruleToSave.tableName": ruleToSave.tableName,
|
|
||||||
"ruleToSave.menuObjid": ruleToSave.menuObjid,
|
|
||||||
"ruleToSave.scopeType": ruleToSave.scopeType,
|
|
||||||
ruleToSave,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 테스트 테이블에 저장 (numbering_rules)
|
// 테스트 테이블에 저장 (numbering_rules)
|
||||||
const response = await saveNumberingRuleToTest(ruleToSave);
|
const response = await saveNumberingRuleToTest(ruleToSave);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// 깊은 복사하여 savedRules와 currentRule이 다른 객체를 참조하도록 함
|
|
||||||
const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
|
const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
|
||||||
|
|
||||||
// setSavedRules 내부에서 prev를 사용해서 existing 확인 (클로저 문제 방지)
|
|
||||||
setSavedRules((prev) => {
|
|
||||||
const savedData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
|
|
||||||
const existsInPrev = prev.some((r) => r.ruleId === ruleToSave.ruleId);
|
|
||||||
|
|
||||||
console.log("🔍 [handleSave] setSavedRules:", {
|
|
||||||
ruleId: ruleToSave.ruleId,
|
|
||||||
existsInPrev,
|
|
||||||
prevCount: prev.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existsInPrev) {
|
|
||||||
// 기존 규칙 업데이트
|
|
||||||
return prev.map((r) => (r.ruleId === ruleToSave.ruleId ? savedData : r));
|
|
||||||
} else {
|
|
||||||
// 새 규칙 추가
|
|
||||||
return [...prev, savedData];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setCurrentRule(currentData);
|
setCurrentRule(currentData);
|
||||||
setSelectedRuleId(response.data.ruleId);
|
|
||||||
|
|
||||||
await onSave?.(response.data);
|
await onSave?.(response.data);
|
||||||
toast.success("채번 규칙이 저장되었습니다");
|
toast.success("채번 규칙이 저장되었습니다");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -407,143 +323,62 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentRule, onSave, currentTableName, menuObjid]);
|
}, [currentRule, onSave, selectedColumn]);
|
||||||
|
|
||||||
const handleSelectRule = useCallback((rule: NumberingRuleConfig) => {
|
|
||||||
console.log("🔍 [handleSelectRule] 규칙 선택:", {
|
|
||||||
ruleId: rule.ruleId,
|
|
||||||
ruleName: rule.ruleName,
|
|
||||||
partsCount: rule.parts?.length || 0,
|
|
||||||
parts: rule.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })),
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedRuleId(rule.ruleId);
|
|
||||||
// 깊은 복사하여 객체 참조 분리 (좌측 목록과 편집 영역의 객체가 공유되지 않도록)
|
|
||||||
const ruleCopy = JSON.parse(JSON.stringify(rule)) as NumberingRuleConfig;
|
|
||||||
|
|
||||||
console.log("🔍 [handleSelectRule] 깊은 복사 후:", {
|
|
||||||
ruleId: ruleCopy.ruleId,
|
|
||||||
partsCount: ruleCopy.parts?.length || 0,
|
|
||||||
parts: ruleCopy.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })),
|
|
||||||
});
|
|
||||||
|
|
||||||
setCurrentRule(ruleCopy);
|
|
||||||
toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteSavedRule = useCallback(
|
|
||||||
async (ruleId: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await deleteNumberingRuleFromTest(ruleId);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
|
|
||||||
|
|
||||||
if (selectedRuleId === ruleId) {
|
|
||||||
setSelectedRuleId(null);
|
|
||||||
setCurrentRule(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("규칙이 삭제되었습니다");
|
|
||||||
} else {
|
|
||||||
showErrorToast("채번 규칙 삭제에 실패했습니다", response.error, { guidance: "잠시 후 다시 시도해 주세요." });
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
showErrorToast("채번 규칙 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[selectedRuleId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNewRule = useCallback(() => {
|
|
||||||
console.log("📋 새 규칙 생성:", { currentTableName, menuObjid });
|
|
||||||
|
|
||||||
const newRule: NumberingRuleConfig = {
|
|
||||||
ruleId: `rule-${Date.now()}`,
|
|
||||||
ruleName: "새 채번 규칙",
|
|
||||||
parts: [],
|
|
||||||
separator: "-",
|
|
||||||
resetPeriod: "none",
|
|
||||||
currentSequence: 1,
|
|
||||||
scopeType: "table", // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
|
||||||
tableName: currentTableName || "", // 현재 화면의 테이블명 자동 설정
|
|
||||||
menuObjid: menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("📋 생성된 규칙 정보:", newRule);
|
|
||||||
|
|
||||||
setSelectedRuleId(newRule.ruleId);
|
|
||||||
setCurrentRule(newRule);
|
|
||||||
|
|
||||||
toast.success("새 규칙이 생성되었습니다");
|
|
||||||
}, [currentTableName, menuObjid]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex h-full gap-4 ${className}`}>
|
<div className={`flex h-full gap-4 ${className}`}>
|
||||||
{/* 좌측: 저장된 규칙 목록 */}
|
{/* 좌측: 채번 컬럼 목록 (카테고리 패턴) */}
|
||||||
<div className="flex w-80 flex-shrink-0 flex-col gap-4">
|
<div className="flex w-72 flex-shrink-0 flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<h2 className="text-sm font-semibold sm:text-base">채번 컬럼</h2>
|
||||||
{editingLeftTitle ? (
|
|
||||||
<Input
|
|
||||||
value={leftTitle}
|
|
||||||
onChange={(e) => setLeftTitle(e.target.value)}
|
|
||||||
onBlur={() => setEditingLeftTitle(false)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && setEditingLeftTitle(false)}
|
|
||||||
className="h-8 text-sm font-semibold"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<h2 className="text-sm font-semibold sm:text-base">{leftTitle}</h2>
|
|
||||||
)}
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingLeftTitle(true)}>
|
|
||||||
<Edit2 className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={handleNewRule} variant="outline" className="h-9 w-full text-sm">
|
<Input
|
||||||
<Plus className="mr-2 h-4 w-4" />새 규칙 생성
|
value={columnSearch}
|
||||||
</Button>
|
onChange={(e) => setColumnSearch(e.target.value)}
|
||||||
|
placeholder="검색..."
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
<div className="flex-1 space-y-1 overflow-y-auto">
|
||||||
{loading ? (
|
{loading && numberingColumns.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center">
|
<div className="flex h-32 items-center justify-center">
|
||||||
<p className="text-muted-foreground text-xs">로딩 중...</p>
|
<p className="text-muted-foreground text-xs">로딩 중...</p>
|
||||||
</div>
|
</div>
|
||||||
) : savedRules.length === 0 ? (
|
) : filteredGroups.length === 0 ? (
|
||||||
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
|
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||||
<p className="text-muted-foreground text-xs">저장된 규칙이 없습니다</p>
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{numberingColumns.length === 0
|
||||||
|
? "채번 타입 컬럼이 없습니다"
|
||||||
|
: "검색 결과가 없습니다"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
savedRules.map((rule) => (
|
filteredGroups.map(([tableName, group]) => (
|
||||||
<Card
|
<div key={tableName} className="mb-2">
|
||||||
key={rule.ruleId}
|
<div className="text-muted-foreground mb-1 flex items-center gap-1 px-1 text-[11px] font-medium">
|
||||||
className={`border-border hover:bg-accent cursor-pointer py-2 transition-colors ${
|
<FolderTree className="h-3 w-3" />
|
||||||
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
<span>{group.tableLabel}</span>
|
||||||
}`}
|
<span className="text-muted-foreground/60">({group.columns.length})</span>
|
||||||
onClick={() => handleSelectRule(rule)}
|
</div>
|
||||||
>
|
{group.columns.map((col) => {
|
||||||
<CardHeader className="px-3 py-0">
|
const isSelected =
|
||||||
<div className="flex items-start justify-between">
|
selectedColumn?.tableName === col.tableName &&
|
||||||
<div className="flex-1">
|
selectedColumn?.columnName === col.columnName;
|
||||||
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
return (
|
||||||
</div>
|
<div
|
||||||
<Button
|
key={`${col.tableName}.${col.columnName}`}
|
||||||
variant="ghost"
|
className={cn(
|
||||||
size="icon"
|
"cursor-pointer rounded-md px-3 py-1.5 text-xs transition-colors",
|
||||||
className="h-6 w-6"
|
isSelected
|
||||||
onClick={(e) => {
|
? "bg-primary/10 text-primary border-primary border font-medium"
|
||||||
e.stopPropagation();
|
: "hover:bg-accent"
|
||||||
handleDeleteSavedRule(rule.ruleId);
|
)}
|
||||||
}}
|
onClick={() => handleSelectColumn(col.tableName, col.columnName)}
|
||||||
>
|
>
|
||||||
<Trash2 className="text-destructive h-3 w-3" />
|
{col.columnLabel}
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
);
|
||||||
</CardHeader>
|
})}
|
||||||
</Card>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -557,8 +392,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
{!currentRule ? (
|
{!currentRule ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-muted-foreground mb-2 text-lg font-medium">규칙을 선택해주세요</p>
|
<FolderTree className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
|
||||||
<p className="text-muted-foreground text-sm">좌측에서 규칙을 선택하거나 새로 생성하세요</p>
|
<p className="text-muted-foreground mb-2 text-lg font-medium">컬럼을 선택해주세요</p>
|
||||||
|
<p className="text-muted-foreground text-sm">좌측에서 채번 컬럼을 선택하면 규칙을 편집할 수 있습니다</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue