회사관리, 메뉴관리 수정,삭제 기능

This commit is contained in:
kjs
2025-08-25 11:07:39 +09:00
parent caacd0e0a4
commit 8667cb4780
19 changed files with 1471 additions and 584 deletions

View File

@@ -21,7 +21,7 @@
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.7",
"pg": "^8.11.3",
"pg": "^8.16.3",
"prisma": "^5.7.1",
"redis": "^4.6.10",
"winston": "^3.11.0"
@@ -39,7 +39,7 @@
"@types/node": "^20.10.5",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.14",
"@types/pg": "^8.10.9",
"@types/pg": "^8.15.5",
"@types/sanitize-html": "^2.9.5",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.14.0",

View File

@@ -27,49 +27,49 @@
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"prisma": "^5.7.1",
"@prisma/client": "^5.7.1",
"pg": "^8.11.3",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"helmet": "^7.1.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.7",
"winston": "^3.11.0",
"joi": "^17.11.0",
"pg": "^8.16.3",
"prisma": "^5.7.1",
"redis": "^4.6.10",
"compression": "^1.7.4",
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1"
"winston": "^3.11.0"
},
"devDependencies": {
"typescript": "^5.3.3",
"@types/node": "^20.10.5",
"@types/express": "^4.17.21",
"@types/pg": "^8.10.9",
"@types/jsonwebtoken": "^9.0.5",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/multer": "^1.4.11",
"@types/nodemailer": "^6.4.14",
"@types/morgan": "^1.9.9",
"@types/compression": "^1.7.5",
"@types/sanitize-html": "^2.9.5",
"@types/node-cron": "^3.0.11",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"jest": "^29.7.0",
"@types/jest": "^29.5.11",
"supertest": "^6.3.3",
"@types/jsonwebtoken": "^9.0.5",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.11",
"@types/node": "^20.10.5",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.14",
"@types/pg": "^8.15.5",
"@types/sanitize-html": "^2.9.5",
"@types/supertest": "^6.0.2",
"ts-jest": "^29.1.1",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"eslint": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"prettier": "^3.1.0"
"eslint": "^8.55.0",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"prettier": "^3.1.0",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=20.10.0",

View File

@@ -1,9 +1,9 @@
import { Response } from "express";
import { Request } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { AdminService } from "../services/adminService";
import { Request, Response } from "express";
import { logger } from "../utils/logger";
import { ApiResponse } from "../types/auth";
import { AuthenticatedRequest } from "../types/auth";
import { ApiResponse } from "../types/common";
import { Client } from "pg";
import { AdminService } from "../services/adminService";
/**
* 관리자 메뉴 목록 조회
@@ -250,15 +250,47 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
export const getUserLocale = async (
req: AuthenticatedRequest,
res: Response
) => {
): Promise<void> => {
try {
logger.info("사용자 로케일 조회 요청", {
query: req.query,
user: req.user,
});
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
const userLocale = "ko"; // 기본값
if (!req.user?.userId) {
res.status(400).json({
success: false,
message: "사용자 정보가 없습니다.",
});
return;
}
// 데이터베이스에서 사용자 로케일 조회
const prisma = (await import("../config/database")).default;
const userInfo = await prisma.user_info.findFirst({
where: {
user_id: req.user.userId,
},
select: {
locale: true,
},
});
let userLocale = "en"; // 기본값
if (userInfo?.locale) {
userLocale = userInfo.locale;
logger.info("데이터베이스에서 사용자 로케일 조회 성공", {
userId: req.user.userId,
locale: userLocale,
});
} else {
logger.info("사용자 로케일이 설정되지 않음, 기본값 사용", {
userId: req.user.userId,
defaultLocale: userLocale,
});
}
const response = {
success: true,
@@ -268,6 +300,8 @@ export const getUserLocale = async (
logger.info("사용자 로케일 조회 성공", {
userLocale,
userId: req.user.userId,
fromDatabase: !!userInfo?.locale,
});
res.status(200).json(response);
@@ -282,7 +316,76 @@ export const getUserLocale = async (
};
/**
* GET /api/admin/companies
* 사용자 로케일 설정
*/
export const setUserLocale = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
logger.info("사용자 로케일 설정 요청", {
body: req.body,
user: req.user,
});
if (!req.user?.userId) {
res.status(400).json({
success: false,
message: "사용자 정보가 없습니다.",
});
return;
}
const { locale } = req.body;
if (!locale || !["ko", "en", "ja", "zh"].includes(locale)) {
res.status(400).json({
success: false,
message: "유효하지 않은 로케일입니다. (ko, en, ja, zh 중 선택)",
});
return;
}
// 데이터베이스에 사용자 로케일 저장
const prisma = (await import("../config/database")).default;
await prisma.user_info.update({
where: {
user_id: req.user.userId,
},
data: {
locale: locale,
},
});
logger.info("사용자 로케일을 데이터베이스에 저장 완료", {
locale,
userId: req.user.userId,
});
const response = {
success: true,
data: locale,
message: "사용자 로케일 설정 성공",
};
logger.info("사용자 로케일 설정 성공", {
locale,
userId: req.user.userId,
});
res.status(200).json(response);
} catch (error) {
logger.error("사용자 로케일 설정 실패", { error });
res.status(500).json({
success: false,
message: "사용자 로케일 설정 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* 회사 목록 조회 API
*/
export const getCompanyList = async (
@@ -300,17 +403,27 @@ export const getCompanyList = async (
{
company_code: "ILSHIN",
company_name: "일신제강",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
},
{
company_code: "HUTECH",
company_name: "후테크",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
},
{
company_code: "DAIN",
company_name: "다인",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
},
];
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response = {
success: true,
data: dummyCompanies,
@@ -319,6 +432,7 @@ export const getCompanyList = async (
logger.info("회사 목록 조회 성공", {
totalCount: dummyCompanies.length,
response: response,
});
res.status(200).json(response);
@@ -390,7 +504,10 @@ export async function getLangKeyList(
res: Response
): Promise<void> {
try {
logger.info("다국어 키 목록 조회 요청");
logger.info("다국어 키 목록 조회 요청", {
query: req.query,
user: req.user,
});
// 더미 데이터 반환
const langKeys = [
@@ -401,6 +518,10 @@ export async function getLangKeyList(
langKey: "user.management.title",
description: "사용자 관리 페이지 제목",
isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
},
{
keyId: 2,
@@ -409,6 +530,10 @@ export async function getLangKeyList(
langKey: "menu.management.title",
description: "메뉴 관리 페이지 제목",
isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
},
{
keyId: 3,
@@ -417,15 +542,25 @@ export async function getLangKeyList(
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,
});
res.status(200).json(response);
} catch (error) {
logger.error("다국어 키 목록 조회 실패:", error);
@@ -724,3 +859,403 @@ export async function toggleLanguageStatus(
});
}
}
/**
* 메뉴 저장 (추가/수정)
*/
export async function saveMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const menuData = req.body;
logger.info("메뉴 저장 요청", { menuData, user: req.user });
// PostgreSQL 클라이언트 생성
const client = new Client({
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/ilshin",
});
await client.connect();
// 실제 데이터베이스에 저장
const query = `
INSERT INTO menu_info (
objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
seq, menu_url, menu_desc, writer, regdate, status,
system_name, company_code, lang_key, lang_key_desc
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) RETURNING *
`;
const values = [
Date.now(), // objid
menuData.menuType || null, // menu_type
menuData.parentObjId || null, // parent_obj_id
menuData.menuNameKor, // menu_name_kor
menuData.menuNameEng || null, // menu_name_eng
menuData.seq || null, // seq
menuData.menuUrl || null, // menu_url
menuData.menuDesc || null, // menu_desc
req.user?.userId || "admin", // writer
new Date(), // regdate
menuData.status || "active", // status
menuData.systemName || "PLM", // system_name
menuData.companyCode || "*", // company_code
menuData.langKey || null, // lang_key
menuData.langKeyDesc || null, // lang_key_desc
];
const result = await client.query(query, values);
const savedMenu = result.rows[0];
await client.end();
logger.info("메뉴 저장 성공", { savedMenu });
const response: ApiResponse<any> = {
success: true,
message: "메뉴가 성공적으로 저장되었습니다.",
data: {
objid: savedMenu.objid,
menuNameKor: savedMenu.menu_name_kor,
menuNameEng: savedMenu.menu_name_eng,
menuUrl: savedMenu.menu_url,
menuDesc: savedMenu.menu_desc,
status: savedMenu.status,
writer: savedMenu.writer,
regdate: savedMenu.regdate,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 저장 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 저장 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 메뉴 수정
*/
export async function updateMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuId } = req.params;
const menuData = req.body;
logger.info(`메뉴 수정 요청: menuId = ${menuId}`, {
menuData,
user: req.user,
});
// PostgreSQL 클라이언트 생성
const client = new Client({
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/ilshin",
});
await client.connect();
// 실제 데이터베이스에서 메뉴 수정
const query = `
UPDATE menu_info
SET
menu_type = $1,
parent_obj_id = $2,
menu_name_kor = $3,
menu_name_eng = $4,
seq = $5,
menu_url = $6,
menu_desc = $7,
status = $8,
system_name = $9,
company_code = $10,
lang_key = $11,
lang_key_desc = $12
WHERE objid = $13
RETURNING *
`;
const values = [
menuData.menuType ? BigInt(menuData.menuType) : null, // menu_type
menuData.parentObjId ? BigInt(menuData.parentObjId) : null, // parent_obj_id
menuData.menuNameKor, // menu_name_kor
menuData.menuNameEng || null, // menu_name_eng
menuData.seq ? BigInt(menuData.seq) : null, // seq
menuData.menuUrl || null, // menu_url
menuData.menuDesc || null, // menu_desc
menuData.status || "active", // status
menuData.systemName || "PLM", // system_name
menuData.companyCode || "*", // company_code
menuData.langKey || null, // lang_key
menuData.langKeyDesc || null, // lang_key_desc
BigInt(menuId), // objid (WHERE 조건)
];
const result = await client.query(query, values);
if (result.rowCount === 0) {
await client.end();
res.status(404).json({
success: false,
message: "수정할 메뉴를 찾을 수 없습니다.",
});
return;
}
const updatedMenu = result.rows[0];
await client.end();
logger.info("메뉴 수정 성공", { updatedMenu });
const response: ApiResponse<any> = {
success: true,
message: "메뉴가 성공적으로 수정되었습니다.",
data: {
objid: updatedMenu.objid.toString(),
menuNameKor: updatedMenu.menu_name_kor,
menuNameEng: updatedMenu.menu_name_eng,
menuUrl: updatedMenu.menu_url,
menuDesc: updatedMenu.menu_desc,
status: updatedMenu.status,
writer: updatedMenu.writer,
regdate: updatedMenu.regdate,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 수정 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 수정 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 메뉴 삭제
*/
export async function deleteMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuId } = req.params;
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
// PostgreSQL 클라이언트 생성
const client = new Client({
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/ilshin",
});
await client.connect();
// 실제 데이터베이스에서 메뉴 삭제
const query = `
DELETE FROM menu_info
WHERE objid = $1
RETURNING *
`;
const result = await client.query(query, [BigInt(menuId)]);
if (result.rowCount === 0) {
await client.end();
res.status(404).json({
success: false,
message: "삭제할 메뉴를 찾을 수 없습니다.",
});
return;
}
const deletedMenu = result.rows[0];
await client.end();
logger.info("메뉴 삭제 성공", { deletedMenu });
const response: ApiResponse<any> = {
success: true,
message: "메뉴가 성공적으로 삭제되었습니다.",
data: {
objid: deletedMenu.objid.toString(),
menuNameKor: deletedMenu.menu_name_kor,
menuNameEng: deletedMenu.menu_name_eng,
menuUrl: deletedMenu.menu_url,
menuDesc: deletedMenu.menu_desc,
status: deletedMenu.status,
writer: deletedMenu.writer,
regdate: deletedMenu.regdate,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 삭제 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 메뉴 일괄 삭제
*/
export async function deleteMenusBatch(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const menuIds = req.body as string[];
logger.info("메뉴 일괄 삭제 요청", { menuIds, user: req.user });
if (!Array.isArray(menuIds) || menuIds.length === 0) {
res.status(400).json({
success: false,
message: "삭제할 메뉴 ID 목록이 필요합니다.",
});
return;
}
// PostgreSQL 클라이언트 생성
const client = new Client({
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/ilshin",
});
await client.connect();
let deletedCount = 0;
let failedCount = 0;
const deletedMenus: any[] = [];
const failedMenuIds: string[] = [];
// 각 메뉴 ID에 대해 삭제 시도
for (const menuId of menuIds) {
try {
const query = `
DELETE FROM menu_info
WHERE objid = $1
RETURNING *
`;
const result = await client.query(query, [BigInt(menuId)]);
if (result.rowCount && result.rowCount > 0) {
deletedCount++;
deletedMenus.push(result.rows[0]);
} else {
failedCount++;
failedMenuIds.push(menuId);
}
} catch (error) {
logger.error(`메뉴 삭제 실패 (ID: ${menuId}):`, error);
failedCount++;
failedMenuIds.push(menuId);
}
}
await client.end();
logger.info("메뉴 일괄 삭제 완료", {
total: menuIds.length,
deletedCount,
failedCount,
deletedMenus,
failedMenuIds,
});
const response: ApiResponse<any> = {
success: true,
message: `메뉴 일괄 삭제 완료: ${deletedCount}개 삭제, ${failedCount}개 실패`,
data: {
deletedCount,
failedCount,
total: menuIds.length,
deletedMenus,
failedMenuIds,
},
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 일괄 삭제 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 일괄 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* 회사 목록 조회 (실제 데이터베이스에서)
*/
export async function getCompanyListFromDB(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("회사 목록 조회 요청 (DB)", { user: req.user });
// PostgreSQL 클라이언트 생성
const client = new Client({
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/ilshin",
});
await client.connect();
// company_mng 테이블에서 회사 목록 조회
const query = `
SELECT
company_code,
company_name,
writer,
regdate,
status
FROM company_mng
ORDER BY regdate DESC
`;
const result = await client.query(query);
const companies = result.rows;
await client.end();
logger.info("회사 목록 조회 성공 (DB)", { count: companies.length });
const response: ApiResponse<any> = {
success: true,
message: "회사 목록 조회 성공",
data: companies,
total: companies.length,
};
res.status(200).json(response);
} catch (error) {
logger.error("회사 목록 조회 실패 (DB):", error);
res.status(500).json({
success: false,
message: "회사 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}

View File

@@ -1,17 +1,202 @@
import { Response } from "express";
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; // 배열로 여러 키 전달
logger.info("다국어 텍스트 배치 조회 요청", {
companyCode,
menuCode,
userLang,
keyCount: langKeys?.length || 0,
user: req.user,
});
if (!langKeys || !Array.isArray(langKeys) || langKeys.length === 0) {
res.status(400).json({
success: false,
message: "langKeys 배열이 필요합니다.",
});
return;
}
// 캐시 키 생성
const cacheKey = `${companyCode}_${userLang}_${langKeys.sort().join("_")}`;
// 캐시 확인
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,
});
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,
});
if (langKeyMasters.length === 0) {
// 마스터 데이터가 없으면 기본값 반환
const defaultTranslations = getDefaultTranslations(
langKeys,
userLang as string
);
// 캐시에 저장
translationCache.set(cacheKey, {
data: defaultTranslations,
timestamp: Date.now(),
});
res.status(200).json({
success: true,
data: defaultTranslations,
message: "기본값으로 다국어 텍스트 조회 성공",
fromCache: false,
});
return;
}
// 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);
}
}
// 5. 캐시에 저장
translationCache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
logger.info("다국어 텍스트 배치 조회 완료", {
requestedKeys: langKeys.length,
resultKeys: Object.keys(result).length,
cacheKey,
});
res.status(200).json({
success: true,
data: result,
message: "다국어 텍스트 배치 조회 성공",
fromCache: false,
});
} catch (error) {
logger.error("다국어 텍스트 배치 조회 실패", { error });
res.status(500).json({
success: false,
message: "다국어 텍스트 배치 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
};
/**
* GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
* 다국어 텍스트 조회 API
* 단일 다국어 텍스트 조회 API (하위 호환성 유지)
*/
export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode, menuCode, langKey } = req.params;
const { userLang } = req.query;
logger.info("다국어 텍스트 조회 요청", {
logger.info("단일 다국어 텍스트 조회 요청", {
companyCode,
menuCode,
langKey,
@@ -19,22 +204,20 @@ export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
user: req.user,
});
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회)
const dummyText = `${menuCode}_${langKey}_${userLang}`;
// 배치 API를 사용하여 단일 키 조회
const batchResult = await getBatchTranslations(
{
...req,
body: { langKeys: [langKey] },
query: { companyCode, menuCode, userLang },
} as any,
res
);
const response = {
success: true,
data: dummyText,
message: "다국어 텍스트 조회 성공",
};
logger.info("다국어 텍스트 조회 성공", {
text: dummyText,
});
res.status(200).json(response);
// 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음
return;
} catch (error) {
logger.error("다국어 텍스트 조회 실패", { error });
logger.error("단일 다국어 텍스트 조회 실패", { error });
res.status(500).json({
success: false,
message: "다국어 텍스트 조회 중 오류가 발생했습니다.",
@@ -42,3 +225,101 @@ export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
});
}
};
/**
* 기본 번역 텍스트 반환 (개별 키)
*/
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;
}
/**
* 기본 번역 텍스트 반환 (배치)
*/
function getDefaultTranslations(
langKeys: string[],
userLang: string
): Record<string, string> {
const result: Record<string, string> = {};
for (const langKey of langKeys) {
result[langKey] = getDefaultTranslation(langKey, userLang);
}
return result;
}
/**
* 캐시 초기화 (개발/테스트용)
*/
export const clearCache = async (req: AuthenticatedRequest, res: Response) => {
try {
const beforeSize = translationCache.size;
translationCache.clear();
logger.info("다국어 캐시 초기화 완료", {
beforeSize,
afterSize: 0,
user: req.user,
});
res.status(200).json({
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",
});
}
};

View File

@@ -3,9 +3,15 @@ import {
getAdminMenus,
getUserMenus,
getMenuInfo,
saveMenu, // 메뉴 추가
updateMenu, // 메뉴 수정
deleteMenu, // 메뉴 삭제
deleteMenusBatch, // 메뉴 일괄 삭제
getUserList,
getCompanyList,
getCompanyListFromDB, // 실제 DB에서 회사 목록 조회
getUserLocale,
setUserLocale,
getLanguageList,
getLangKeyList,
getLangTextList,
@@ -29,15 +35,21 @@ router.use(authenticateToken);
router.get("/menus", getAdminMenus);
router.get("/user-menus", getUserMenus);
router.get("/menus/:menuId", getMenuInfo);
router.post("/menus", saveMenu); // 메뉴 추가
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
router.delete("/menus/:menuId", deleteMenu); // 메뉴 삭제
// 사용자 관리 API
router.get("/users", getUserList);
// 회사 관리 API
router.get("/companies", getCompanyList);
router.get("/companies/db", getCompanyListFromDB); // 실제 DB에서 회사 목록 조회
// 사용자 로케일 API
router.get("/user-locale", getUserLocale);
router.post("/user-locale", setUserLocale);
// 다국어 관리 API
router.get("/multilang/languages", getLanguageList);

View File

@@ -1,5 +1,9 @@
import { Router } from "express";
import { getUserText } from "../controllers/multilangController";
import {
getUserText,
getBatchTranslations,
clearCache,
} from "../controllers/multilangController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = Router();
@@ -10,4 +14,10 @@ router.use(authenticateToken);
// 다국어 텍스트 API
router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText);
// 다국어 텍스트 배치 조회 API (새로운 방식)
router.post("/batch", getBatchTranslations);
// 캐시 초기화 API (개발/테스트용)
router.delete("/cache", clearCache);
export default router;

View File

@@ -5,6 +5,7 @@ export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
total?: number;
error?: {
code: string;
details?: any;