다국어 생성후 매핑 자동저장
This commit is contained in:
parent
f9575d7b5f
commit
26bb93ab6e
|
|
@ -1137,16 +1137,22 @@ export const generateScreenLabelKeys = async (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용자 회사 정보
|
// 화면의 회사 정보 조회 (사용자 회사가 아닌 화면 소속 회사 기준)
|
||||||
const companyCode = req.user?.companyCode || "*";
|
const { queryOne } = await import("../database/db");
|
||||||
|
const screenInfo = await queryOne<{ company_code: string }>(
|
||||||
|
`SELECT company_code FROM screen_definitions WHERE screen_id = $1`,
|
||||||
|
[screenId]
|
||||||
|
);
|
||||||
|
const companyCode = screenInfo?.company_code || req.user?.companyCode || "*";
|
||||||
|
|
||||||
// 회사명 조회
|
// 회사명 조회
|
||||||
const { queryOne } = await import("../database/db");
|
|
||||||
const companyInfo = await queryOne<{ company_name: string }>(
|
const companyInfo = await queryOne<{ company_name: string }>(
|
||||||
`SELECT company_name FROM company_mng WHERE company_code = $1`,
|
`SELECT company_name FROM company_mng WHERE company_code = $1`,
|
||||||
[companyCode]
|
[companyCode]
|
||||||
);
|
);
|
||||||
const companyName = companyCode === "*" ? "공통" : (companyInfo?.company_name || companyCode);
|
const companyName = companyCode === "*" ? "공통" : (companyInfo?.company_name || companyCode);
|
||||||
|
|
||||||
|
logger.info("화면 소속 회사 정보", { screenId, companyCode, companyName });
|
||||||
|
|
||||||
const multiLangService = new MultiLangService();
|
const multiLangService = new MultiLangService();
|
||||||
const results = await multiLangService.generateScreenLabelKeys({
|
const results = await multiLangService.generateScreenLabelKeys({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { getPool } from "../database/db";
|
import { getPool } from "../database/db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { MultiLangService } from "../services/multilangService";
|
||||||
|
|
||||||
// pool 인스턴스 가져오기
|
// pool 인스턴스 가져오기
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
|
// 다국어 서비스 인스턴스
|
||||||
|
const multiLangService = new MultiLangService();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 화면 그룹 (screen_groups) CRUD
|
// 화면 그룹 (screen_groups) CRUD
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -191,6 +195,47 @@ export const createScreenGroup = async (req: Request, res: Response) => {
|
||||||
// 업데이트된 데이터 반환
|
// 업데이트된 데이터 반환
|
||||||
const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]);
|
const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]);
|
||||||
|
|
||||||
|
// 다국어 카테고리 자동 생성 (그룹 경로 기반)
|
||||||
|
try {
|
||||||
|
// 그룹 경로 조회 (상위 그룹 → 현재 그룹)
|
||||||
|
const groupPathResult = await pool.query(
|
||||||
|
`WITH RECURSIVE group_path AS (
|
||||||
|
SELECT id, parent_group_id, group_name, group_level, 1 as depth
|
||||||
|
FROM screen_groups
|
||||||
|
WHERE id = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT g.id, g.parent_group_id, g.group_name, g.group_level, gp.depth + 1
|
||||||
|
FROM screen_groups g
|
||||||
|
INNER JOIN group_path gp ON g.id = gp.parent_group_id
|
||||||
|
WHERE g.parent_group_id IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT group_name FROM group_path
|
||||||
|
ORDER BY depth DESC`,
|
||||||
|
[newGroupId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupPath = groupPathResult.rows.map((r: any) => r.group_name);
|
||||||
|
|
||||||
|
// 회사 이름 조회
|
||||||
|
let companyName = "공통";
|
||||||
|
if (finalCompanyCode !== "*") {
|
||||||
|
const companyResult = await pool.query(
|
||||||
|
`SELECT company_name FROM company_mng WHERE company_code = $1`,
|
||||||
|
[finalCompanyCode]
|
||||||
|
);
|
||||||
|
if (companyResult.rows.length > 0) {
|
||||||
|
companyName = companyResult.rows[0].company_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다국어 카테고리 생성
|
||||||
|
await multiLangService.ensureScreenGroupCategory(finalCompanyCode, companyName, groupPath);
|
||||||
|
logger.info("화면 그룹 다국어 카테고리 자동 생성 완료", { groupPath, companyCode: finalCompanyCode });
|
||||||
|
} catch (multilangError: any) {
|
||||||
|
// 다국어 카테고리 생성 실패해도 그룹 생성은 성공으로 처리
|
||||||
|
logger.warn("화면 그룹 다국어 카테고리 생성 실패 (무시하고 계속):", multilangError.message);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("화면 그룹 생성", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id });
|
logger.info("화면 그룹 생성", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id });
|
||||||
|
|
||||||
res.json({ success: true, data: updatedResult.rows[0], message: "화면 그룹이 생성되었습니다." });
|
res.json({ success: true, data: updatedResult.rows[0], message: "화면 그룹이 생성되었습니다." });
|
||||||
|
|
|
||||||
|
|
@ -1475,14 +1475,115 @@ export class MultiLangService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화면 그룹 경로 조회 (screen_groups에서 계층 구조 조회)
|
||||||
|
* @param screenId 화면 ID
|
||||||
|
* @returns 그룹 경로 배열 (최상위 → 현재 그룹 순서)
|
||||||
|
*/
|
||||||
|
async getScreenGroupPath(screenId: number): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
// 화면이 속한 그룹 조회
|
||||||
|
const screenGroup = await queryOne<{ group_id: number }>(
|
||||||
|
`SELECT group_id FROM screen_group_screens WHERE screen_id = $1 LIMIT 1`,
|
||||||
|
[screenId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!screenGroup) {
|
||||||
|
logger.info("화면이 그룹에 속하지 않음", { screenId });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그룹의 계층 구조 경로 조회 (최상위 → 현재 그룹 순서)
|
||||||
|
const groups = await query<{ group_name: string; group_level: number }>(
|
||||||
|
`WITH RECURSIVE group_path AS (
|
||||||
|
SELECT id, parent_group_id, group_name, group_level, 1 as depth
|
||||||
|
FROM screen_groups
|
||||||
|
WHERE id = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT g.id, g.parent_group_id, g.group_name, g.group_level, gp.depth + 1
|
||||||
|
FROM screen_groups g
|
||||||
|
INNER JOIN group_path gp ON g.id = gp.parent_group_id
|
||||||
|
WHERE g.parent_group_id IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT group_name, group_level FROM group_path
|
||||||
|
ORDER BY depth DESC`,
|
||||||
|
[screenGroup.group_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return groups.map((g) => g.group_name);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("화면 그룹 경로 조회 실패:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화면 그룹 기반 카테고리 확인 또는 생성
|
||||||
|
* @param companyCode 회사 코드
|
||||||
|
* @param companyName 회사 이름
|
||||||
|
* @param groupPath 그룹 경로 (상위 → 하위 순서)
|
||||||
|
* @returns 최종 카테고리 ID
|
||||||
|
*/
|
||||||
|
async ensureScreenGroupCategory(
|
||||||
|
companyCode: string,
|
||||||
|
companyName: string,
|
||||||
|
groupPath: string[]
|
||||||
|
): Promise<number> {
|
||||||
|
try {
|
||||||
|
if (groupPath.length === 0) {
|
||||||
|
// 그룹이 없으면 회사 카테고리만 반환
|
||||||
|
return await this.ensureCompanyCategory(companyCode, companyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentId = await this.ensureCompanyCategory(companyCode, companyName);
|
||||||
|
let currentLevel = 3; // SCREEN(1) > Company(2) > Group(3)
|
||||||
|
|
||||||
|
for (const groupName of groupPath) {
|
||||||
|
// 현재 그룹 카테고리 확인
|
||||||
|
const existing = await queryOne<{ category_id: number }>(
|
||||||
|
`SELECT category_id FROM multi_lang_category
|
||||||
|
WHERE category_name = $1 AND parent_id = $2`,
|
||||||
|
[groupName, parentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
parentId = existing.category_id;
|
||||||
|
} else {
|
||||||
|
// 그룹 카테고리 생성
|
||||||
|
const groupCode = `${companyCode}_GROUP_${groupName}`.replace(/\s+/g, "_");
|
||||||
|
const keyPrefix = groupName.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`,
|
||||||
|
[groupCode, groupName, parentId, currentLevel, keyPrefix, `${groupName} 화면 그룹의 다국어`]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("화면 그룹 카테고리 생성", { groupName, categoryId: result?.category_id });
|
||||||
|
parentId = result!.category_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentId;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("화면 그룹 카테고리 생성 실패:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 화면 라벨 다국어 키 자동 생성
|
* 화면 라벨 다국어 키 자동 생성
|
||||||
|
* 화면 그룹 기반으로 카테고리 생성 (기존 메뉴 기반에서 변경)
|
||||||
*/
|
*/
|
||||||
async generateScreenLabelKeys(params: {
|
async generateScreenLabelKeys(params: {
|
||||||
screenId: number;
|
screenId: number;
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
menuObjId?: string;
|
menuObjId?: string; // 하위 호환성 유지 (미사용, 화면 그룹 기반으로 변경)
|
||||||
labels: Array<{ componentId: string; label: string; type?: string }>;
|
labels: Array<{ componentId: string; label: string; type?: string }>;
|
||||||
}): Promise<Array<{ componentId: string; keyId: number; langKey: string }>> {
|
}): Promise<Array<{ componentId: string; keyId: number; langKey: string }>> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1492,16 +1593,15 @@ export class MultiLangService {
|
||||||
labelCount: params.labels.length,
|
labelCount: params.labels.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 메뉴 경로 조회
|
// 화면 그룹 경로 조회 (화면이 속한 그룹의 계층 구조)
|
||||||
const menuPath = params.menuObjId
|
const groupPath = await this.getScreenGroupPath(params.screenId);
|
||||||
? await this.getMenuPath(params.menuObjId)
|
logger.info("화면 그룹 경로 조회 완료", { screenId: params.screenId, groupPath });
|
||||||
: [];
|
|
||||||
|
|
||||||
// 메뉴 카테고리 확보
|
// 화면 그룹 기반 카테고리 확보
|
||||||
const categoryId = await this.ensureMenuCategory(
|
const categoryId = await this.ensureScreenGroupCategory(
|
||||||
params.companyCode,
|
params.companyCode,
|
||||||
params.companyName,
|
params.companyName,
|
||||||
menuPath
|
groupPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// 카테고리 경로 조회 (키 생성용)
|
// 카테고리 경로 조회 (키 생성용)
|
||||||
|
|
|
||||||
|
|
@ -1518,12 +1518,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const updatedComponents = applyMultilangMappings(layout.components, response.data);
|
const updatedComponents = applyMultilangMappings(layout.components, response.data);
|
||||||
|
|
||||||
// 레이아웃 업데이트
|
// 레이아웃 업데이트
|
||||||
setLayout((prev) => ({
|
const updatedLayout = {
|
||||||
...prev,
|
...layout,
|
||||||
components: updatedComponents,
|
components: updatedComponents,
|
||||||
}));
|
screenResolution: screenResolution,
|
||||||
|
};
|
||||||
|
|
||||||
|
setLayout(updatedLayout);
|
||||||
|
|
||||||
toast.success(`${response.data.length}개의 다국어 키가 생성되고 컴포넌트에 매핑되었습니다.`);
|
// 자동 저장 (매핑 정보가 손실되지 않도록)
|
||||||
|
try {
|
||||||
|
await screenApi.saveLayout(selectedScreen.screenId, updatedLayout);
|
||||||
|
toast.success(`${response.data.length}개의 다국어 키가 생성되고 자동 저장되었습니다.`);
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error("다국어 매핑 저장 실패:", saveError);
|
||||||
|
toast.warning(`${response.data.length}개의 다국어 키가 생성되었습니다. 저장 버튼을 눌러 매핑을 저장하세요.`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error?.details || "다국어 키 생성에 실패했습니다.");
|
toast.error(response.error?.details || "다국어 키 생성에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
@ -1533,7 +1543,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingMultilang(false);
|
setIsGeneratingMultilang(false);
|
||||||
}
|
}
|
||||||
}, [selectedScreen, layout.components, menuObjid]);
|
}, [selectedScreen, layout, screenResolution, menuObjid]);
|
||||||
|
|
||||||
// 템플릿 드래그 처리
|
// 템플릿 드래그 처리
|
||||||
const handleTemplateDrop = useCallback(
|
const handleTemplateDrop = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -468,9 +468,17 @@ export const MultilangSettingsModal: React.FC<MultilangSettingsModalProps> = ({
|
||||||
addLabel(comp.id, anyComp.title, "title", parentType, parentLabel);
|
addLabel(comp.id, anyComp.title, "title", parentType, parentLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 버튼 텍스트
|
// 3. 버튼 텍스트 (componentId에 _button 접미사 추가하여 라벨과 구분)
|
||||||
if (config?.text && typeof config.text === "string") {
|
if (config?.text && typeof config.text === "string") {
|
||||||
addLabel(comp.id, config.text, "button", parentType, parentLabel);
|
addLabel(
|
||||||
|
`${comp.id}_button`,
|
||||||
|
config.text,
|
||||||
|
"button",
|
||||||
|
parentType,
|
||||||
|
parentLabel,
|
||||||
|
config.langKeyId,
|
||||||
|
config.langKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. placeholder
|
// 4. placeholder
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,17 @@ export function extractMultilangLabels(
|
||||||
addLabel(comp.id, anyComp.title, "title", parentType, parentLabel);
|
addLabel(comp.id, anyComp.title, "title", parentType, parentLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 버튼 텍스트
|
// 3. 버튼 텍스트 (componentId에 _button 접미사 추가하여 라벨과 구분)
|
||||||
if (config?.text && typeof config.text === "string") {
|
if (config?.text && typeof config.text === "string") {
|
||||||
addLabel(comp.id, config.text, "button", parentType, parentLabel);
|
addLabel(
|
||||||
|
`${comp.id}_button`,
|
||||||
|
config.text,
|
||||||
|
"button",
|
||||||
|
parentType,
|
||||||
|
parentLabel,
|
||||||
|
config.langKeyId,
|
||||||
|
config.langKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. placeholder
|
// 4. placeholder
|
||||||
|
|
@ -326,15 +334,16 @@ export function applyMultilangMappings(
|
||||||
if (labelMapping) {
|
if (labelMapping) {
|
||||||
updated.langKeyId = labelMapping.keyId;
|
updated.langKeyId = labelMapping.keyId;
|
||||||
updated.langKey = labelMapping.langKey;
|
updated.langKey = labelMapping.langKey;
|
||||||
|
}
|
||||||
// 버튼 컴포넌트의 경우 componentConfig에도 매핑
|
|
||||||
if (config?.text) {
|
// 버튼 텍스트 매핑 (componentId_button 형식으로 조회)
|
||||||
updated.componentConfig = {
|
const buttonMapping = mappingMap.get(`${comp.id}_button`);
|
||||||
...updated.componentConfig,
|
if (buttonMapping && config?.text) {
|
||||||
langKeyId: labelMapping.keyId,
|
updated.componentConfig = {
|
||||||
langKey: labelMapping.langKey,
|
...updated.componentConfig,
|
||||||
};
|
langKeyId: buttonMapping.keyId,
|
||||||
}
|
langKey: buttonMapping.langKey,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 매핑
|
// 컬럼 매핑
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue