ERP-node/backend-node/src/controllers/multilangController.ts

326 lines
9.6 KiB
TypeScript
Raw Normal View History

import { Response } from "express";
2025-08-25 15:12:31 +09:00
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import prisma from "../config/database";
// 메모리 캐시 (개발 환경용, 운영에서는 Redis 사용 권장)
const translationCache = new Map<string, any>();
const CACHE_TTL = 5 * 60 * 1000; // 5분
interface CacheEntry {
data: any;
timestamp: number;
}
/**
* GET /api/multilang/batch
* API -
*/
export const getBatchTranslations = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode, menuCode, userLang } = req.query;
const { langKeys } = req.body; // 배열로 여러 키 전달
2025-08-25 15:12:31 +09:00
logger.info("다국어 텍스트 배치 조회 요청", {
companyCode,
menuCode,
userLang,
keyCount: langKeys?.length || 0,
user: req.user,
2025-08-25 15:12:31 +09:00
});
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
res.status(400).json({
success: false,
message: "langKeys 배열이 필요합니다.",
});
return;
}
// 캐시 키 생성
const cacheKey = `${companyCode}_${userLang}_${langKeys.sort().join("_")}`;
2025-08-25 15:12:31 +09:00
// 캐시 확인
const cached = translationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
logger.info("캐시된 번역 데이터 사용", {
cacheKey,
keyCount: langKeys.length,
});
res.status(200).json({
success: true,
data: cached.data,
message: "캐시된 다국어 텍스트 조회 성공",
fromCache: true,
});
return;
}
// 1. 모든 키에 대한 마스터 정보를 한번에 조회
logger.info("다국어 키 마스터 배치 조회 시작", {
keyCount: langKeys.length,
2025-08-25 15:12:31 +09:00
});
const langKeyMasters = await prisma.$queryRaw<any[]>`
SELECT key_id, lang_key, company_code
FROM multi_lang_key_master
WHERE lang_key = ANY(${langKeys}::varchar[])
AND (company_code = ${companyCode}::varchar OR company_code = '*')
ORDER BY
CASE WHEN company_code = ${companyCode}::varchar THEN 1 ELSE 2 END,
lang_key,
company_code
`;
logger.info("다국어 키 마스터 배치 조회 결과", {
requestedKeys: langKeys.length,
foundKeys: langKeyMasters.length,
2025-08-25 15:12:31 +09:00
});
if (langKeyMasters.length === 0) {
// 마스터 데이터가 없으면 기본값 반환
const defaultTranslations = getDefaultTranslations(
langKeys,
userLang as string
);
// 캐시에 저장
translationCache.set(cacheKey, {
data: defaultTranslations,
timestamp: Date.now(),
});
2025-08-25 15:12:31 +09:00
res.status(200).json({
success: true,
data: defaultTranslations,
message: "기본값으로 다국어 텍스트 조회 성공",
fromCache: false,
});
return;
}
2025-08-25 15:12:31 +09:00
// 2. 모든 key_id를 추출
const keyIds = langKeyMasters.map((master) => master.key_id);
// 3. 요청된 언어와 한국어 번역을 한번에 조회
const translations = await prisma.$queryRaw<any[]>`
SELECT
mlt.key_id,
mlt.lang_code,
mlt.lang_text,
mlkm.lang_key
FROM multi_lang_text mlt
JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id
WHERE mlt.key_id = ANY(${keyIds}::numeric[])
AND mlt.lang_code IN (${userLang}::varchar, 'KR')
ORDER BY
mlt.key_id,
CASE WHEN mlt.lang_code = ${userLang}::varchar THEN 1 ELSE 2 END
`;
logger.info("번역 텍스트 배치 조회 결과", {
keyIds: keyIds.length,
translations: translations.length,
});
// 4. 결과를 키별로 정리
const result: Record<string, string> = {};
for (const langKey of langKeys) {
const master = langKeyMasters.find((m) => m.lang_key === langKey);
if (master) {
const keyId = master.key_id;
// 요청된 언어 번역 찾기
let translation = translations.find(
(t) => t.key_id === keyId && t.lang_code === userLang
);
// 요청된 언어가 없으면 한국어 번역 찾기
if (!translation) {
translation = translations.find(
(t) => t.key_id === keyId && t.lang_code === "KR"
);
}
// 번역이 있으면 사용, 없으면 기본값
if (translation) {
result[langKey] = translation.lang_text;
} else {
result[langKey] = getDefaultTranslation(langKey, userLang as string);
}
} else {
// 마스터 데이터가 없으면 기본값
result[langKey] = getDefaultTranslation(langKey, userLang as string);
}
}
2025-08-25 15:12:31 +09:00
// 5. 캐시에 저장
translationCache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
2025-08-25 15:12:31 +09:00
logger.info("다국어 텍스트 배치 조회 완료", {
requestedKeys: langKeys.length,
resultKeys: Object.keys(result).length,
cacheKey,
2025-08-25 15:12:31 +09:00
});
res.status(200).json({
2025-09-01 11:00:38 +09:00
success: true,
data: result,
message: "다국어 텍스트 배치 조회 성공",
fromCache: false,
2025-08-25 15:12:31 +09:00
});
} catch (error) {
logger.error("다국어 텍스트 배치 조회 실패", { error });
2025-08-25 15:12:31 +09:00
res.status(500).json({
success: false,
message: "다국어 텍스트 배치 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
* API ( )
*/
export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode, menuCode, langKey } = req.params;
const { userLang } = req.query;
logger.info("단일 다국어 텍스트 조회 요청", {
companyCode,
menuCode,
langKey,
userLang,
user: req.user,
});
// 배치 API를 사용하여 단일 키 조회
const batchResult = await getBatchTranslations(
{
...req,
body: { langKeys: [langKey] },
query: { companyCode, menuCode, userLang },
} as any,
res
);
2025-08-25 15:12:31 +09:00
// 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음
return;
} catch (error) {
logger.error("단일 다국어 텍스트 조회 실패", { error });
res.status(500).json({
success: false,
message: "다국어 텍스트 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* ( )
*/
function getDefaultTranslation(langKey: string, userLang: string): string {
const defaultKoreanTexts: Record<string, string> = {
"button.add": "추가",
"button.add.top.level": "최상위 메뉴 추가",
"button.add.sub": "하위 메뉴 추가",
"button.edit": "수정",
"button.delete": "삭제",
"button.cancel": "취소",
"button.save": "저장",
"button.register": "등록",
"form.menu.name": "메뉴명",
"form.menu.url": "URL",
"form.menu.description": "설명",
"form.menu.type": "메뉴 타입",
"form.status": "상태",
"form.company": "회사",
"table.header.menu.name": "메뉴명",
"table.header.menu.url": "URL",
"table.header.status": "상태",
"table.header.company": "회사",
"table.header.actions": "작업",
"filter.company": "회사",
"filter.search": "검색",
"filter.reset": "초기화",
"menu.type.title": "메뉴 타입",
"menu.type.admin": "관리자",
"menu.type.user": "사용자",
"status.active": "활성화",
"status.inactive": "비활성화",
"form.lang.key": "언어 키",
"form.lang.key.select": "언어 키 선택",
"form.menu.name.placeholder": "메뉴명을 입력하세요",
"form.menu.url.placeholder": "URL을 입력하세요",
"form.menu.description.placeholder": "설명을 입력하세요",
"form.menu.sequence": "순서",
"form.menu.sequence.placeholder": "순서를 입력하세요",
"form.status.active": "활성",
"form.status.inactive": "비활성",
"form.company.select": "회사 선택",
"form.company.common": "공통",
"form.company.submenu.note": "하위메뉴는 회사별로 관리됩니다",
"filter.company.common": "공통",
"filter.search.placeholder": "검색어를 입력하세요",
"modal.menu.register.title": "메뉴 등록",
};
return defaultKoreanTexts[langKey] || langKey;
}
2025-08-29 10:44:55 +09:00
/**
* ()
2025-08-29 10:44:55 +09:00
*/
function getDefaultTranslations(
langKeys: string[],
userLang: string
): Record<string, string> {
const result: Record<string, string> = {};
for (const langKey of langKeys) {
result[langKey] = getDefaultTranslation(langKey, userLang);
2025-08-29 10:44:55 +09:00
}
return result;
}
2025-08-29 10:44:55 +09:00
/**
* (/)
*/
export const clearCache = async (req: AuthenticatedRequest, res: Response) => {
try {
const beforeSize = translationCache.size;
translationCache.clear();
logger.info("다국어 캐시 초기화 완료", {
beforeSize,
afterSize: 0,
user: req.user,
});
2025-08-25 15:12:31 +09:00
res.status(200).json({
2025-09-01 11:00:38 +09:00
success: true,
message: "캐시가 초기화되었습니다.",
beforeSize,
afterSize: 0,
});
} catch (error) {
logger.error("캐시 초기화 실패", { error });
res.status(500).json({
success: false,
message: "캐시 초기화 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};