Merge pull request '다국어 관리 페이지' (#1) from multilang into dev

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/1
This commit is contained in:
kjs 2025-08-25 15:17:17 +09:00
commit 0e89393a14
8 changed files with 1900 additions and 393 deletions

View File

@ -665,7 +665,7 @@ export async function getLanguageList(
}
/**
* ( )
*
*/
export async function getLangKeyList(
req: AuthenticatedRequest,
@ -677,59 +677,61 @@ export async function getLangKeyList(
user: req.user,
});
// 더미 데이터 반환
const langKeys = [
{
keyId: 1,
companyCode: "ILSHIN",
menuName: "사용자 관리",
langKey: "user.management.title",
description: "사용자 관리 페이지 제목",
isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
},
{
keyId: 2,
companyCode: "ILSHIN",
menuName: "메뉴 관리",
langKey: "menu.management.title",
description: "메뉴 관리 페이지 제목",
isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
},
{
keyId: 3,
companyCode: "HUTECH",
menuName: "대시보드",
langKey: "dashboard.title",
description: "대시보드 페이지 제목",
isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
},
];
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response: ApiResponse<any[]> = {
success: true,
message: "다국어 키 목록 조회 성공",
data: langKeys,
};
logger.info("다국어 키 목록 조회 성공", {
totalCount: langKeys.length,
response: response,
// 실제 데이터베이스에서 데이터 조회
const client = new Client({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME || "ilshin",
user: process.env.DB_USER || "postgres",
password: process.env.DB_PASSWORD || "postgres",
});
res.status(200).json(response);
await client.connect();
try {
const query = `
SELECT
key_id as "keyId",
company_code as "companyCode",
menu_name as "menuName",
lang_key as "langKey",
description,
is_active as "isActive",
created_date as "createdDate",
created_by as "createdBy",
updated_date as "updatedDate",
updated_by as "updatedBy"
FROM multi_lang_key_master
ORDER BY company_code, menu_name, lang_key
`;
const result = await client.query(query);
const langKeys = result.rows.map((row) => ({
...row,
createdDate: row.createdDate
? new Date(row.createdDate).toISOString()
: undefined,
updatedDate: row.updatedDate
? new Date(row.updatedDate).toISOString()
: undefined,
}));
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response: ApiResponse<any[]> = {
success: true,
message: "다국어 키 목록 조회 성공",
data: langKeys,
};
logger.info("다국어 키 목록 조회 성공", {
totalCount: langKeys.length,
response: response,
});
res.status(200).json(response);
} finally {
await client.end();
}
} catch (error) {
logger.error("다국어 키 목록 조회 실패:", error);
res.status(500).json({

File diff suppressed because it is too large Load Diff

View File

@ -19,17 +19,6 @@ import {
deleteCompany, // 회사 삭제
getUserLocale,
setUserLocale,
getLanguageList,
getLangKeyList,
getLangTextList,
saveLangTexts,
saveLangKey,
updateLangKey,
deleteLangKey,
toggleLangKeyStatus,
saveLanguage,
updateLanguage,
toggleLanguageStatus,
} from "../controllers/adminController";
import { authenticateToken } from "../middleware/authMiddleware";
@ -67,17 +56,4 @@ router.delete("/companies/:companyCode", deleteCompany); // 회사 삭제
router.get("/user-locale", getUserLocale);
router.post("/user-locale", setUserLocale);
// 다국어 관리 API
router.get("/multilang/languages", getLanguageList);
router.get("/multilang/keys", getLangKeyList);
router.get("/multilang/keys/:keyId/texts", getLangTextList);
router.post("/multilang/keys/:keyId/texts", saveLangTexts);
router.post("/multilang/keys", saveLangKey);
router.put("/multilang/keys/:keyId", updateLangKey);
router.delete("/multilang/keys/:keyId", deleteLangKey);
router.put("/multilang/keys/:keyId/toggle", toggleLangKeyStatus);
router.post("/multilang/languages", saveLanguage);
router.put("/multilang/languages/:langCode", updateLanguage);
router.put("/multilang/languages/:langCode/toggle", toggleLanguageStatus);
export default router;

View File

@ -1,23 +1,50 @@
import { Router } from "express";
import {
getUserText,
getBatchTranslations,
clearCache,
} from "../controllers/multilangController";
import express from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import {
// 언어 관리 API
getLanguages,
createLanguage,
updateLanguage,
toggleLanguage,
const router = Router();
// 다국어 키 관리 API
getLangKeys,
getLangTexts,
createLangKey,
updateLangKey,
deleteLangKey,
toggleLangKey,
// 모든 multilang 라우트에 인증 미들웨어 적용
// 다국어 텍스트 관리 API
saveLangTexts,
getUserText,
getLangText,
getBatchTranslations,
} from "../controllers/multilangController";
const router = express.Router();
// 모든 다국어 관리 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
// 다국어 텍스트 API
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText);
// 언어 관리 API
router.get("/languages", getLanguages); // 언어 목록 조회
router.post("/languages", createLanguage); // 언어 생성
router.put("/languages/:langCode", updateLanguage); // 언어 수정
router.put("/languages/:langCode/toggle", toggleLanguage); // 언어 상태 토글
// 다국어 텍스트 배치 조회 API (새로운 방식)
router.post("/batch", getBatchTranslations);
// 다국어 키 관리 API
router.get("/keys", getLangKeys); // 다국어 키 목록 조회
router.get("/keys/:keyId/texts", getLangTexts); // 특정 키의 다국어 텍스트 조회
router.post("/keys", createLangKey); // 다국어 키 생성
router.put("/keys/:keyId", updateLangKey); // 다국어 키 수정
router.delete("/keys/:keyId", deleteLangKey); // 다국어 키 삭제
router.put("/keys/:keyId/toggle", toggleLangKey); // 다국어 키 상태 토글
// 캐시 초기화 API (개발/테스트용)
router.delete("/cache", clearCache);
// 다국어 텍스트 관리 API
router.post("/keys/:keyId/texts", saveLangTexts); // 다국어 텍스트 저장/수정
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText); // 사용자별 다국어 텍스트 조회
router.get("/text/:companyCode/:langKey/:langCode", getLangText); // 특정 키의 다국어 텍스트 조회
router.post("/batch", getBatchTranslations); // 다국어 텍스트 배치 조회
export default router;

View File

@ -0,0 +1,855 @@
import { Client } from "pg";
import { logger } from "../utils/logger";
import {
Language,
LangKey,
LangText,
CreateLanguageRequest,
UpdateLanguageRequest,
CreateLangKeyRequest,
UpdateLangKeyRequest,
SaveLangTextsRequest,
GetLangKeysParams,
GetUserTextParams,
BatchTranslationRequest,
ApiResponse,
} from "../types/multilang";
export class MultiLangService {
private client: Client;
constructor(client: Client) {
this.client = client;
}
/**
*
*/
async getLanguages(): Promise<Language[]> {
try {
logger.info("언어 목록 조회 시작");
const query = `
SELECT
lang_code as "langCode",
lang_name as "langName",
lang_native as "langNative",
is_active as "isActive",
sort_order as "sortOrder",
created_date as "createdDate",
created_by as "createdBy",
updated_date as "updatedDate",
updated_by as "updatedBy"
FROM language_master
ORDER BY sort_order, lang_code
`;
const result = await this.client.query(query);
const languages = result.rows.map((row) => ({
...row,
createdDate: row.createdDate ? new Date(row.createdDate) : undefined,
updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined,
}));
logger.info(`언어 목록 조회 완료: ${languages.length}`);
return languages;
} catch (error) {
logger.error("언어 목록 조회 중 오류 발생:", error);
throw new Error(
`언어 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async createLanguage(languageData: CreateLanguageRequest): Promise<Language> {
try {
logger.info("언어 생성 시작", { languageData });
// 중복 체크
const duplicateCheckQuery = `
SELECT lang_code FROM language_master WHERE lang_code = $1
`;
const duplicateResult = await this.client.query(duplicateCheckQuery, [
languageData.langCode,
]);
if (duplicateResult.rows.length > 0) {
throw new Error(
`이미 존재하는 언어 코드입니다: ${languageData.langCode}`
);
}
// 언어 생성
const insertQuery = `
INSERT INTO language_master (
lang_code, lang_name, lang_native, is_active, sort_order,
created_date, created_by, updated_date, updated_by
) VALUES ($1, $2, $3, $4, $5, now(), $6, now(), $7)
RETURNING *
`;
const insertValues = [
languageData.langCode,
languageData.langName,
languageData.langNative,
languageData.isActive || "Y",
languageData.sortOrder || 0,
languageData.createdBy || "system",
languageData.updatedBy || "system",
];
const result = await this.client.query(insertQuery, insertValues);
const createdLanguage = result.rows[0];
logger.info("언어 생성 완료", { langCode: createdLanguage.lang_code });
return {
langCode: createdLanguage.lang_code,
langName: createdLanguage.lang_name,
langNative: createdLanguage.lang_native,
isActive: createdLanguage.is_active,
sortOrder: createdLanguage.sort_order,
createdDate: createdLanguage.created_date
? new Date(createdLanguage.created_date)
: undefined,
createdBy: createdLanguage.created_by,
updatedDate: createdLanguage.updated_date
? new Date(createdLanguage.updated_date)
: undefined,
updatedBy: createdLanguage.updated_by,
};
} catch (error) {
logger.error("언어 생성 중 오류 발생:", error);
throw new Error(
`언어 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async updateLanguage(
langCode: string,
languageData: UpdateLanguageRequest
): Promise<Language> {
try {
logger.info("언어 수정 시작", { langCode, languageData });
// 기존 언어 확인
const checkQuery = `
SELECT * FROM language_master WHERE lang_code = $1
`;
const checkResult = await this.client.query(checkQuery, [langCode]);
if (checkResult.rows.length === 0) {
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
}
// 언어 수정
const updateQuery = `
UPDATE language_master
SET
lang_name = COALESCE($2, lang_name),
lang_native = COALESCE($3, lang_native),
is_active = COALESCE($4, is_active),
sort_order = COALESCE($5, sort_order),
updated_date = now(),
updated_by = $6
WHERE lang_code = $1
RETURNING *
`;
const updateValues = [
langCode,
languageData.langName,
languageData.langNative,
languageData.isActive,
languageData.sortOrder,
languageData.updatedBy || "system",
];
const result = await this.client.query(updateQuery, updateValues);
const updatedLanguage = result.rows[0];
logger.info("언어 수정 완료", { langCode });
return {
langCode: updatedLanguage.lang_code,
langName: updatedLanguage.lang_name,
langNative: updatedLanguage.lang_native,
isActive: updatedLanguage.is_active,
sortOrder: updatedLanguage.sort_order,
createdDate: updatedLanguage.created_date
? new Date(updatedLanguage.created_date)
: undefined,
createdBy: updatedLanguage.created_by,
updatedDate: updatedLanguage.updated_date
? new Date(updatedLanguage.updated_date)
: undefined,
updatedBy: updatedLanguage.updated_by,
};
} catch (error) {
logger.error("언어 수정 중 오류 발생:", error);
throw new Error(
`언어 수정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async toggleLanguage(langCode: string): Promise<string> {
try {
logger.info("언어 상태 토글 시작", { langCode });
// 현재 상태 조회
const currentQuery = `
SELECT is_active FROM language_master WHERE lang_code = $1
`;
const currentResult = await this.client.query(currentQuery, [langCode]);
if (currentResult.rows.length === 0) {
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
}
const currentStatus = currentResult.rows[0].is_active;
const newStatus = currentStatus === "Y" ? "N" : "Y";
// 상태 업데이트
const updateQuery = `
UPDATE language_master
SET is_active = $2, updated_date = now(), updated_by = 'system'
WHERE lang_code = $1
`;
await this.client.query(updateQuery, [langCode, newStatus]);
const result = newStatus === "Y" ? "활성화" : "비활성화";
logger.info("언어 상태 토글 완료", { langCode, result });
return result;
} catch (error) {
logger.error("언어 상태 토글 중 오류 발생:", error);
throw new Error(
`언어 상태 토글 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async getLangKeys(params: GetLangKeysParams): Promise<LangKey[]> {
try {
logger.info("다국어 키 목록 조회 시작", { params });
let query = `
SELECT
key_id as "keyId",
company_code as "companyCode",
menu_name as "menuName",
lang_key as "langKey",
description,
is_active as "isActive",
created_date as "createdDate",
created_by as "createdBy",
updated_date as "updatedDate",
updated_by as "updatedBy"
FROM multi_lang_key_master
WHERE 1=1
`;
const queryParams: any[] = [];
let paramIndex = 1;
// 회사 코드 필터
if (params.companyCode) {
query += ` AND company_code = $${paramIndex}`;
queryParams.push(params.companyCode);
paramIndex++;
}
// 메뉴 코드 필터
if (params.menuCode) {
query += ` AND menu_name = $${paramIndex}`;
queryParams.push(params.menuCode);
paramIndex++;
}
// 검색 조건
if (params.searchText) {
query += ` AND (
lang_key ILIKE $${paramIndex} OR
description ILIKE $${paramIndex} OR
menu_name ILIKE $${paramIndex}
)`;
queryParams.push(`%${params.searchText}%`);
paramIndex++;
}
// 정렬
query += ` ORDER BY company_code, menu_name, lang_key`;
const result = await this.client.query(query, queryParams);
const langKeys = result.rows.map((row) => ({
...row,
createdDate: row.createdDate ? new Date(row.createdDate) : undefined,
updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined,
}));
logger.info(`다국어 키 목록 조회 완료: ${langKeys.length}`);
return langKeys;
} catch (error) {
logger.error("다국어 키 목록 조회 중 오류 발생:", error);
throw new Error(
`다국어 키 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async getLangTexts(keyId: number): Promise<LangText[]> {
try {
logger.info("다국어 텍스트 조회 시작", { keyId });
const query = `
SELECT
text_id as "textId",
key_id as "keyId",
lang_code as "langCode",
lang_text as "langText",
is_active as "isActive",
created_date as "createdDate",
created_by as "createdBy",
updated_date as "updatedDate",
updated_by as "updatedBy"
FROM multi_lang_text
WHERE key_id = $1 AND is_active = 'Y'
ORDER BY lang_code
`;
const result = await this.client.query(query, [keyId]);
const langTexts = result.rows.map((row) => ({
...row,
createdDate: row.createdDate ? new Date(row.createdDate) : undefined,
updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined,
}));
logger.info(`다국어 텍스트 조회 완료: ${langTexts.length}`);
return langTexts;
} catch (error) {
logger.error("다국어 텍스트 조회 중 오류 발생:", error);
throw new Error(
`다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async createLangKey(keyData: CreateLangKeyRequest): Promise<number> {
try {
logger.info("다국어 키 생성 시작", { keyData });
// 중복 체크
const duplicateCheckQuery = `
SELECT key_id FROM multi_lang_key_master
WHERE company_code = $1 AND lang_key = $2
`;
const duplicateResult = await this.client.query(duplicateCheckQuery, [
keyData.companyCode,
keyData.langKey,
]);
if (duplicateResult.rows.length > 0) {
throw new Error(
`동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}`
);
}
// 다국어 키 생성
const insertQuery = `
INSERT INTO multi_lang_key_master (
company_code, menu_name, lang_key, description, is_active,
created_date, created_by, updated_date, updated_by
) VALUES ($1, $2, $3, $4, $5, now(), $6, now(), $7)
RETURNING key_id
`;
const insertValues = [
keyData.companyCode,
keyData.menuName || null,
keyData.langKey,
keyData.description || null,
keyData.isActive || "Y",
keyData.createdBy || "system",
keyData.updatedBy || "system",
];
const result = await this.client.query(insertQuery, insertValues);
const keyId = result.rows[0].key_id;
logger.info("다국어 키 생성 완료", { keyId, langKey: keyData.langKey });
return keyId;
} catch (error) {
logger.error("다국어 키 생성 중 오류 발생:", error);
throw new Error(
`다국어 키 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async updateLangKey(
keyId: number,
keyData: UpdateLangKeyRequest
): Promise<void> {
try {
logger.info("다국어 키 수정 시작", { keyId, keyData });
// 기존 키 확인
const checkQuery = `
SELECT key_id FROM multi_lang_key_master WHERE key_id = $1
`;
const checkResult = await this.client.query(checkQuery, [keyId]);
if (checkResult.rows.length === 0) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
// 중복 체크 (자신을 제외하고)
if (keyData.companyCode && keyData.langKey) {
const duplicateCheckQuery = `
SELECT key_id FROM multi_lang_key_master
WHERE company_code = $1 AND lang_key = $2 AND key_id != $3
`;
const duplicateResult = await this.client.query(duplicateCheckQuery, [
keyData.companyCode,
keyData.langKey,
keyId,
]);
if (duplicateResult.rows.length > 0) {
throw new Error(
`동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}`
);
}
}
// 다국어 키 수정
const updateQuery = `
UPDATE multi_lang_key_master
SET
company_code = COALESCE($2, company_code),
menu_name = COALESCE($3, menu_name),
lang_key = COALESCE($4, lang_key),
description = COALESCE($5, description),
updated_date = now(),
updated_by = $6
WHERE key_id = $1
`;
const updateValues = [
keyId,
keyData.companyCode,
keyData.menuName,
keyData.langKey,
keyData.description,
keyData.updatedBy || "system",
];
await this.client.query(updateQuery, updateValues);
logger.info("다국어 키 수정 완료", { keyId });
} catch (error) {
logger.error("다국어 키 수정 중 오류 발생:", error);
throw new Error(
`다국어 키 수정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async deleteLangKey(keyId: number): Promise<void> {
try {
logger.info("다국어 키 삭제 시작", { keyId });
// 기존 키 확인
const checkQuery = `
SELECT key_id FROM multi_lang_key_master WHERE key_id = $1
`;
const checkResult = await this.client.query(checkQuery, [keyId]);
if (checkResult.rows.length === 0) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
// 트랜잭션 시작
await this.client.query("BEGIN");
try {
// 관련된 다국어 텍스트 삭제
const deleteTextsQuery = `
DELETE FROM multi_lang_text WHERE key_id = $1
`;
await this.client.query(deleteTextsQuery, [keyId]);
// 다국어 키 삭제
const deleteKeyQuery = `
DELETE FROM multi_lang_key_master WHERE key_id = $1
`;
await this.client.query(deleteKeyQuery, [keyId]);
// 트랜잭션 커밋
await this.client.query("COMMIT");
logger.info("다국어 키 삭제 완료", { keyId });
} catch (error) {
// 트랜잭션 롤백
await this.client.query("ROLLBACK");
throw error;
}
} catch (error) {
logger.error("다국어 키 삭제 중 오류 발생:", error);
throw new Error(
`다국어 키 삭제 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async toggleLangKey(keyId: number): Promise<string> {
try {
logger.info("다국어 키 상태 토글 시작", { keyId });
// 현재 상태 조회
const currentQuery = `
SELECT is_active FROM multi_lang_key_master WHERE key_id = $1
`;
const currentResult = await this.client.query(currentQuery, [keyId]);
if (currentResult.rows.length === 0) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
const currentStatus = currentResult.rows[0].is_active;
const newStatus = currentStatus === "Y" ? "N" : "Y";
// 상태 업데이트
const updateQuery = `
UPDATE multi_lang_key_master
SET is_active = $2, updated_date = now(), updated_by = 'system'
WHERE key_id = $1
`;
await this.client.query(updateQuery, [keyId, newStatus]);
const result = newStatus === "Y" ? "활성화" : "비활성화";
logger.info("다국어 키 상태 토글 완료", { keyId, result });
return result;
} catch (error) {
logger.error("다국어 키 상태 토글 중 오류 발생:", error);
throw new Error(
`다국어 키 상태 토글 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* /
*/
async saveLangTexts(
keyId: number,
textData: SaveLangTextsRequest
): Promise<void> {
try {
logger.info("다국어 텍스트 저장 시작", {
keyId,
textCount: textData.texts.length,
});
// 기존 키 확인
const checkQuery = `
SELECT key_id FROM multi_lang_key_master WHERE key_id = $1
`;
const checkResult = await this.client.query(checkQuery, [keyId]);
if (checkResult.rows.length === 0) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
// 트랜잭션 시작
await this.client.query("BEGIN");
try {
// 기존 텍스트 삭제
const deleteTextsQuery = `
DELETE FROM multi_lang_text WHERE key_id = $1
`;
await this.client.query(deleteTextsQuery, [keyId]);
// 새로운 텍스트 삽입
for (const text of textData.texts) {
const insertTextQuery = `
INSERT INTO multi_lang_text (
key_id, lang_code, lang_text, is_active,
created_date, created_by, updated_date, updated_by
) VALUES ($1, $2, $3, $4, now(), $5, now(), $6)
`;
const insertValues = [
keyId,
text.langCode,
text.langText,
text.isActive || "Y",
text.createdBy || "system",
text.updatedBy || "system",
];
await this.client.query(insertTextQuery, insertValues);
}
// 트랜잭션 커밋
await this.client.query("COMMIT");
logger.info("다국어 텍스트 저장 완료", {
keyId,
savedCount: textData.texts.length,
});
} catch (error) {
// 트랜잭션 롤백
await this.client.query("ROLLBACK");
throw error;
}
} catch (error) {
logger.error("다국어 텍스트 저장 중 오류 발생:", error);
throw new Error(
`다국어 텍스트 저장 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async getUserText(params: GetUserTextParams): Promise<string> {
try {
logger.info("사용자별 다국어 텍스트 조회 시작", { params });
const query = `
SELECT t.lang_text as "langText"
FROM multi_lang_key_master km
JOIN multi_lang_text t ON km.key_id = t.key_id
WHERE km.company_code = $1
AND km.menu_name = $2
AND km.lang_key = $3
AND t.lang_code = $4
AND km.is_active = 'Y'
AND t.is_active = 'Y'
LIMIT 1
`;
const result = await this.client.query(query, [
params.companyCode,
params.menuCode,
params.langKey,
params.userLang,
]);
if (result.rows.length === 0) {
logger.warn("사용자별 다국어 텍스트를 찾을 수 없음", { params });
return params.langKey; // 기본값으로 키 반환
}
const langText = result.rows[0].langText;
logger.info("사용자별 다국어 텍스트 조회 완료", { params, langText });
return langText;
} catch (error) {
logger.error("사용자별 다국어 텍스트 조회 중 오류 발생:", error);
throw new Error(
`사용자별 다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async getLangText(
companyCode: string,
langKey: string,
langCode: string
): Promise<string> {
try {
logger.info("특정 키의 다국어 텍스트 조회 시작", {
companyCode,
langKey,
langCode,
});
const query = `
SELECT t.lang_text as "langText"
FROM multi_lang_text t
JOIN multi_lang_key_master k ON t.key_id = k.key_id
WHERE k.company_code = $1
AND k.lang_key = $2
AND t.lang_code = $3
AND t.is_active = 'Y'
AND k.is_active = 'Y'
`;
const result = await this.client.query(query, [
companyCode,
langKey,
langCode,
]);
if (result.rows.length === 0) {
logger.warn("특정 키의 다국어 텍스트를 찾을 수 없음", {
companyCode,
langKey,
langCode,
});
return langKey; // 기본값으로 키 반환
}
const langText = result.rows[0].langText;
logger.info("특정 키의 다국어 텍스트 조회 완료", {
companyCode,
langKey,
langCode,
langText,
});
return langText;
} catch (error) {
logger.error("특정 키의 다국어 텍스트 조회 중 오류 발생:", error);
throw new Error(
`특정 키의 다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
*
*/
async getBatchTranslations(
params: BatchTranslationRequest
): Promise<Record<string, string>> {
try {
logger.info("배치 번역 조회 시작", {
companyCode: params.companyCode,
menuCode: params.menuCode,
userLang: params.userLang,
keyCount: params.langKeys.length,
});
if (params.langKeys.length === 0) {
return {};
}
// 모든 키에 대한 마스터 정보를 한번에 조회
const langKeyMastersQuery = `
SELECT key_id, lang_key, company_code
FROM multi_lang_key_master
WHERE lang_key = ANY($1::varchar[])
AND (company_code = $2::varchar OR company_code = '*')
ORDER BY
CASE WHEN company_code = $2::varchar THEN 1 ELSE 2 END,
lang_key,
company_code
`;
const langKeyMasters = await this.client.query(langKeyMastersQuery, [
params.langKeys,
params.companyCode,
]);
if (langKeyMasters.rows.length === 0) {
logger.warn("배치 번역: 언어키 마스터를 찾을 수 없음", { params });
return this.createDefaultTranslations(params.langKeys);
}
// 찾은 키들의 ID 목록
const keyIds = langKeyMasters.rows.map((row) => row.key_id);
const foundKeys = langKeyMasters.rows.map((row) => row.lang_key);
// 누락된 키들 (기본값으로 설정)
const missingKeys = params.langKeys.filter(
(key) => !foundKeys.includes(key)
);
const result: Record<string, string> = {};
// 기본값으로 누락된 키들 설정
missingKeys.forEach((key) => {
result[key] = key;
});
// 실제 번역 텍스트 조회
if (keyIds.length > 0) {
const textsQuery = `
SELECT t.key_id, t.lang_text, km.lang_key
FROM multi_lang_text t
JOIN multi_lang_key_master km ON t.key_id = km.key_id
WHERE t.key_id = ANY($1::int[])
AND t.lang_code = $2
AND t.is_active = 'Y'
`;
const texts = await this.client.query(textsQuery, [
keyIds,
params.userLang,
]);
// 결과 매핑
texts.rows.forEach((row) => {
result[row.lang_key] = row.lang_text;
});
}
logger.info("배치 번역 조회 완료", {
totalKeys: params.langKeys.length,
foundKeys: foundKeys.length,
missingKeys: missingKeys.length,
resultKeys: Object.keys(result).length,
});
return result;
} catch (error) {
logger.error("배치 번역 조회 중 오류 발생:", error);
throw new Error(
`배치 번역 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* ( )
*/
private createDefaultTranslations(
langKeys: string[]
): Record<string, string> {
const result: Record<string, string> = {};
langKeys.forEach((key) => {
result[key] = key;
});
return result;
}
}

View File

@ -0,0 +1,130 @@
export interface Language {
langCode: string;
langName: string;
langNative: string;
isActive: string;
sortOrder?: number;
createdDate?: Date;
createdBy?: string;
updatedDate?: Date;
updatedBy?: string;
}
export interface LangKey {
keyId?: number;
companyCode: string;
menuName?: string;
langKey: string;
description?: string;
isActive: string;
createdDate?: Date;
createdBy?: string;
updatedDate?: Date;
updatedBy?: string;
}
export interface LangText {
textId?: number;
keyId: number;
langCode: string;
langText: string;
isActive: string;
createdDate?: Date;
createdBy?: string;
updatedDate?: Date;
updatedBy?: string;
}
export interface LangKeyWithTexts extends LangKey {
texts: LangText[];
}
export interface CreateLanguageRequest {
langCode: string;
langName: string;
langNative: string;
isActive?: string;
sortOrder?: number;
createdBy?: string;
updatedBy?: string;
}
export interface UpdateLanguageRequest {
langName?: string;
langNative?: string;
isActive?: string;
sortOrder?: number;
updatedBy?: string;
}
export interface CreateLangKeyRequest {
companyCode: string;
menuName?: string;
langKey: string;
description?: string;
isActive?: string;
createdBy?: string;
updatedBy?: string;
}
export interface UpdateLangKeyRequest {
companyCode?: string;
menuName?: string;
langKey?: string;
description?: string;
updatedBy?: string;
}
export interface SaveLangTextsRequest {
texts: Array<{
langCode: string;
langText: string;
isActive?: string;
createdBy?: string;
updatedBy?: string;
}>;
}
export interface GetLangKeysParams {
companyCode?: string;
menuCode?: string;
keyType?: string;
searchText?: string;
page?: number;
limit?: number;
}
export interface GetUserTextParams {
companyCode: string;
menuCode: string;
langKey: string;
userLang: string;
}
export interface BatchTranslationRequest {
companyCode: string;
menuCode?: string;
userLang: string;
langKeys: string[];
}
export interface TranslationCacheEntry {
data: Record<string, string>;
timestamp: number;
}
export interface ApiResponse<T = any> {
success: boolean;
message?: string;
data?: T;
error?: {
code: string;
details?: any;
};
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}

View File

@ -65,7 +65,7 @@ export default function MultiLangPage() {
const fetchCompanies = async () => {
try {
console.log("회사 목록 조회 시작");
const response = await apiClient.get("/api/admin/companies");
const response = await apiClient.get("/admin/companies");
console.log("회사 목록 응답 데이터:", response.data);
const data = response.data;
@ -87,7 +87,7 @@ export default function MultiLangPage() {
// 언어 목록 조회
const fetchLanguages = async () => {
try {
const response = await apiClient.get("/api/admin/multilang/languages");
const response = await apiClient.get("/multilang/languages");
const data = response.data;
if (data.success) {
setLanguages(data.data);
@ -100,7 +100,7 @@ export default function MultiLangPage() {
// 다국어 키 목록 조회
const fetchLangKeys = async () => {
try {
const response = await apiClient.get("/api/admin/multilang/keys");
const response = await apiClient.get("/multilang/keys");
const data = response.data;
if (data.success) {
console.log("✅ 전체 키 목록 로드:", data.data.length, "개");
@ -147,7 +147,7 @@ export default function MultiLangPage() {
const fetchLangTexts = async (keyId: number) => {
try {
console.log("다국어 텍스트 조회 시작: keyId =", keyId);
const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`);
const response = await apiClient.get(`/multilang/keys/${keyId}/texts`);
const data = response.data;
console.log("다국어 텍스트 조회 응답:", data);
if (data.success) {
@ -203,7 +203,18 @@ export default function MultiLangPage() {
if (!selectedKey) return;
try {
const response = await apiClient.post(`/api/admin/multilang/keys/${selectedKey.keyId}/texts`, editingTexts);
// 백엔드가 기대하는 형식으로 데이터 변환
const requestData = {
texts: editingTexts.map((text) => ({
langCode: text.langCode,
langText: text.langText,
isActive: text.isActive || "Y",
createdBy: user?.userId || "system",
updatedBy: user?.userId || "system",
})),
};
const response = await apiClient.post(`/multilang/keys/${selectedKey.keyId}/texts`, requestData);
const data = response.data;
if (data.success) {
alert("저장되었습니다.");
@ -245,9 +256,9 @@ export default function MultiLangPage() {
let response;
if (editingLanguage) {
response = await apiClient.put(`/api/admin/multilang/languages/${editingLanguage.langCode}`, requestData);
response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData);
} else {
response = await apiClient.post("/api/admin/multilang/languages", requestData);
response = await apiClient.post("/multilang/languages", requestData);
}
const result = response.data;
@ -282,17 +293,11 @@ export default function MultiLangPage() {
try {
const deletePromises = Array.from(selectedLanguages).map((langCode) =>
fetch(`${API_BASE_URL}/multilang/languages/${langCode}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}),
apiClient.delete(`/admin/multilang/languages/${langCode}`),
);
const responses = await Promise.all(deletePromises);
const failedDeletes = responses.filter((response) => !response.ok);
const failedDeletes = responses.filter((response) => !response.data.success);
if (failedDeletes.length === 0) {
alert("선택된 언어가 삭제되었습니다.");
@ -344,9 +349,9 @@ export default function MultiLangPage() {
let response;
if (editingKey) {
response = await apiClient.put(`/api/admin/multilang/keys/${editingKey.keyId}`, requestData);
response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData);
} else {
response = await apiClient.post("/api/admin/multilang/keys", requestData);
response = await apiClient.post("/multilang/keys", requestData);
}
const data = response.data;
@ -383,7 +388,7 @@ export default function MultiLangPage() {
// 키 상태 토글
const handleToggleStatus = async (keyId: number) => {
try {
const response = await apiClient.put(`/api/admin/multilang/keys/${keyId}/toggle`);
const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`);
const data = response.data;
if (data.success) {
alert(`키가 ${data.data}되었습니다.`);
@ -400,7 +405,7 @@ export default function MultiLangPage() {
// 언어 상태 토글
const handleToggleLanguageStatus = async (langCode: string) => {
try {
const response = await apiClient.put(`/api/admin/multilang/languages/${langCode}/toggle`);
const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`);
const data = response.data;
if (data.success) {
alert(`언어가 ${data.data}되었습니다.`);
@ -440,9 +445,7 @@ export default function MultiLangPage() {
}
try {
const deletePromises = Array.from(selectedKeys).map((keyId) =>
apiClient.delete(`/api/admin/multilang/keys/${keyId}`),
);
const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`));
const responses = await Promise.all(deletePromises);
const allSuccess = responses.every((response) => response.data.success);
@ -472,7 +475,7 @@ export default function MultiLangPage() {
}
try {
const response = await apiClient.delete(`/api/admin/multilang/keys/${keyId}`);
const response = await apiClient.delete(`/multilang/keys/${keyId}`);
const data = response.data;
if (data.success) {
alert("언어 키가 영구적으로 삭제되었습니다.");

View File

@ -1,7 +1,7 @@
import axios, { AxiosResponse, AxiosError } from "axios";
// API 기본 URL 설정
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api";
// JWT 토큰 관리 유틸리티
const TokenManager = {