다국어 자동생성
This commit is contained in:
@@ -1293,4 +1293,291 @@ export class MultiLangService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 회사/메뉴 기반 카테고리 자동 생성 메서드
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* 화면(screen) 루트 카테고리 확인 또는 생성
|
||||
*/
|
||||
async ensureScreenRootCategory(): Promise<number> {
|
||||
try {
|
||||
// 기존 screen 카테고리 확인
|
||||
const existing = await queryOne<{ category_id: number }>(
|
||||
`SELECT category_id FROM multi_lang_category
|
||||
WHERE category_code = 'screen' AND parent_id IS NULL`,
|
||||
[]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return existing.category_id;
|
||||
}
|
||||
|
||||
// 없으면 생성
|
||||
const result = await queryOne<{ category_id: number }>(
|
||||
`INSERT INTO multi_lang_category
|
||||
(category_code, category_name, parent_id, level, key_prefix, description, sort_order, is_active, created_date)
|
||||
VALUES ('screen', '화면', NULL, 1, 'screen', '화면 디자이너에서 자동 생성된 다국어 키', 100, 'Y', NOW())
|
||||
RETURNING category_id`,
|
||||
[]
|
||||
);
|
||||
|
||||
logger.info("화면 루트 카테고리 생성", { categoryId: result?.category_id });
|
||||
return result!.category_id;
|
||||
} catch (error) {
|
||||
logger.error("화면 루트 카테고리 생성 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 카테고리 확인 또는 생성
|
||||
*/
|
||||
async ensureCompanyCategory(companyCode: string, companyName: string): Promise<number> {
|
||||
try {
|
||||
const screenRootId = await this.ensureScreenRootCategory();
|
||||
|
||||
// 기존 회사 카테고리 확인
|
||||
const existing = await queryOne<{ category_id: number }>(
|
||||
`SELECT category_id FROM multi_lang_category
|
||||
WHERE category_code = $1 AND parent_id = $2`,
|
||||
[companyCode, screenRootId]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return existing.category_id;
|
||||
}
|
||||
|
||||
// 회사 카테고리 생성
|
||||
const displayName = companyCode === "*" ? "공통" : companyName;
|
||||
const keyPrefix = companyCode === "*" ? "common" : companyCode.toLowerCase();
|
||||
|
||||
const result = await queryOne<{ category_id: number }>(
|
||||
`INSERT INTO multi_lang_category
|
||||
(category_code, category_name, parent_id, level, key_prefix, description, sort_order, is_active, created_date)
|
||||
VALUES ($1, $2, $3, 2, $4, $5, $6, 'Y', NOW())
|
||||
RETURNING category_id`,
|
||||
[
|
||||
companyCode,
|
||||
displayName,
|
||||
screenRootId,
|
||||
keyPrefix,
|
||||
`${displayName} 회사의 화면 다국어`,
|
||||
companyCode === "*" ? 0 : 10,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info("회사 카테고리 생성", { companyCode, categoryId: result?.category_id });
|
||||
return result!.category_id;
|
||||
} catch (error) {
|
||||
logger.error("회사 카테고리 생성 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 카테고리 확인 또는 생성 (메뉴 경로 전체)
|
||||
*/
|
||||
async ensureMenuCategory(
|
||||
companyCode: string,
|
||||
companyName: string,
|
||||
menuPath: string[] // ["영업관리", "수주관리"]
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (menuPath.length === 0) {
|
||||
return await this.ensureCompanyCategory(companyCode, companyName);
|
||||
}
|
||||
|
||||
let parentId = await this.ensureCompanyCategory(companyCode, companyName);
|
||||
let currentLevel = 3;
|
||||
|
||||
for (const menuName of menuPath) {
|
||||
// 현재 메뉴 카테고리 확인
|
||||
const existing = await queryOne<{ category_id: number }>(
|
||||
`SELECT category_id FROM multi_lang_category
|
||||
WHERE category_name = $1 AND parent_id = $2`,
|
||||
[menuName, parentId]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
parentId = existing.category_id;
|
||||
} else {
|
||||
// 메뉴 카테고리 생성
|
||||
const menuCode = `${companyCode}_${menuName}`.replace(/\s+/g, "_");
|
||||
const keyPrefix = menuName.toLowerCase().replace(/\s+/g, "_");
|
||||
|
||||
const result = await queryOne<{ category_id: number }>(
|
||||
`INSERT INTO multi_lang_category
|
||||
(category_code, category_name, parent_id, level, key_prefix, description, sort_order, is_active, created_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 0, 'Y', NOW())
|
||||
RETURNING category_id`,
|
||||
[menuCode, menuName, parentId, currentLevel, keyPrefix, `${menuName} 메뉴의 다국어`]
|
||||
);
|
||||
|
||||
logger.info("메뉴 카테고리 생성", { menuName, categoryId: result?.category_id });
|
||||
parentId = result!.category_id;
|
||||
}
|
||||
|
||||
currentLevel++;
|
||||
}
|
||||
|
||||
return parentId;
|
||||
} catch (error) {
|
||||
logger.error("메뉴 카테고리 생성 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 경로 조회 (menu_info에서 부모 메뉴까지)
|
||||
*/
|
||||
async getMenuPath(menuObjId: string): Promise<string[]> {
|
||||
try {
|
||||
const menus = await query<{ menu_name_kor: string; level: number }>(
|
||||
`WITH RECURSIVE menu_path AS (
|
||||
SELECT objid, parent_obj_id, menu_name_kor, 1 as level
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
UNION ALL
|
||||
SELECT m.objid, m.parent_obj_id, m.menu_name_kor, mp.level + 1
|
||||
FROM menu_info m
|
||||
INNER JOIN menu_path mp ON m.objid = mp.parent_obj_id
|
||||
WHERE m.parent_obj_id IS NOT NULL AND m.parent_obj_id != 0
|
||||
)
|
||||
SELECT menu_name_kor, level FROM menu_path
|
||||
WHERE menu_name_kor IS NOT NULL
|
||||
ORDER BY level DESC`,
|
||||
[menuObjId]
|
||||
);
|
||||
|
||||
return menus.map((m) => m.menu_name_kor);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 경로 조회 실패:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 라벨 다국어 키 자동 생성
|
||||
*/
|
||||
async generateScreenLabelKeys(params: {
|
||||
screenId: number;
|
||||
companyCode: string;
|
||||
companyName: string;
|
||||
menuObjId?: string;
|
||||
labels: Array<{ componentId: string; label: string; type?: string }>;
|
||||
}): Promise<Array<{ componentId: string; keyId: number; langKey: string }>> {
|
||||
try {
|
||||
logger.info("화면 라벨 다국어 키 자동 생성 시작", {
|
||||
screenId: params.screenId,
|
||||
companyCode: params.companyCode,
|
||||
labelCount: params.labels.length,
|
||||
});
|
||||
|
||||
// 메뉴 경로 조회
|
||||
const menuPath = params.menuObjId
|
||||
? await this.getMenuPath(params.menuObjId)
|
||||
: [];
|
||||
|
||||
// 메뉴 카테고리 확보
|
||||
const categoryId = await this.ensureMenuCategory(
|
||||
params.companyCode,
|
||||
params.companyName,
|
||||
menuPath
|
||||
);
|
||||
|
||||
// 카테고리 경로 조회 (키 생성용)
|
||||
const categoryPath = await this.getCategoryPath(categoryId);
|
||||
const keyPrefixParts = categoryPath.map((c) => c.keyPrefix);
|
||||
|
||||
const results: Array<{ componentId: string; keyId: number; langKey: string }> = [];
|
||||
|
||||
for (const labelInfo of params.labels) {
|
||||
// 라벨을 키 형태로 변환 (한글 → 스네이크케이스)
|
||||
const keyMeaning = this.labelToKeyMeaning(labelInfo.label);
|
||||
const langKey = [...keyPrefixParts, keyMeaning].join(".");
|
||||
|
||||
// 기존 키 확인
|
||||
const existingKey = await queryOne<{ key_id: number }>(
|
||||
`SELECT key_id FROM multi_lang_key_master
|
||||
WHERE lang_key = $1 AND company_code = $2`,
|
||||
[langKey, params.companyCode]
|
||||
);
|
||||
|
||||
let keyId: number;
|
||||
|
||||
if (existingKey) {
|
||||
keyId = existingKey.key_id;
|
||||
logger.info("기존 키 사용", { langKey, keyId });
|
||||
} else {
|
||||
// 새 키 생성
|
||||
const keyResult = await queryOne<{ key_id: number }>(
|
||||
`INSERT INTO multi_lang_key_master
|
||||
(company_code, lang_key, description, is_active, category_id, key_meaning, created_date, created_by)
|
||||
VALUES ($1, $2, $3, 'Y', $4, $5, NOW(), 'system')
|
||||
RETURNING key_id`,
|
||||
[
|
||||
params.companyCode,
|
||||
langKey,
|
||||
`화면 ${params.screenId}의 ${labelInfo.type || "라벨"}: ${labelInfo.label}`,
|
||||
categoryId,
|
||||
keyMeaning,
|
||||
]
|
||||
);
|
||||
|
||||
keyId = keyResult!.key_id;
|
||||
|
||||
// 한국어 텍스트 저장 (원문)
|
||||
await query(
|
||||
`INSERT INTO multi_lang_text (key_id, lang_code, lang_text, is_active, created_date, created_by)
|
||||
VALUES ($1, 'KR', $2, 'Y', NOW(), 'system')
|
||||
ON CONFLICT (key_id, lang_code) DO UPDATE SET lang_text = $2, updated_date = NOW()`,
|
||||
[keyId, labelInfo.label]
|
||||
);
|
||||
|
||||
logger.info("새 키 생성", { langKey, keyId });
|
||||
}
|
||||
|
||||
results.push({
|
||||
componentId: labelInfo.componentId,
|
||||
keyId,
|
||||
langKey,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("화면 라벨 다국어 키 생성 완료", {
|
||||
screenId: params.screenId,
|
||||
generatedCount: results.length,
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
logger.error("화면 라벨 다국어 키 생성 실패:", error);
|
||||
throw new Error(
|
||||
`화면 라벨 다국어 키 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라벨을 키 의미로 변환 (한글 → 스네이크케이스 또는 영문 유지)
|
||||
*/
|
||||
private labelToKeyMeaning(label: string): string {
|
||||
// 이미 영문 스네이크케이스면 그대로 사용
|
||||
if (/^[a-z][a-z0-9_]*$/.test(label)) {
|
||||
return label;
|
||||
}
|
||||
|
||||
// 영문 일반이면 스네이크케이스로 변환
|
||||
if (/^[A-Za-z][A-Za-z0-9 ]*$/.test(label)) {
|
||||
return label.toLowerCase().replace(/\s+/g, "_");
|
||||
}
|
||||
|
||||
// 한글이면 간단한 변환 (특수문자 제거, 공백을 _로)
|
||||
return label
|
||||
.replace(/[^\w가-힣\s]/g, "")
|
||||
.replace(/\s+/g, "_")
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user