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

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

View File

@ -27,49 +27,49 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2",
"prisma": "^5.7.1",
"@prisma/client": "^5.7.1", "@prisma/client": "^5.7.1",
"pg": "^8.11.3",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"helmet": "^7.1.0", "compression": "^1.7.4",
"cors": "^2.8.5", "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", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"winston": "^3.11.0", "pg": "^8.16.3",
"joi": "^17.11.0", "prisma": "^5.7.1",
"redis": "^4.6.10", "redis": "^4.6.10",
"compression": "^1.7.4", "winston": "^3.11.0"
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1"
}, },
"devDependencies": { "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/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/compression": "^1.7.5",
"@types/sanitize-html": "^2.9.5", "@types/cors": "^2.8.17",
"@types/node-cron": "^3.0.11", "@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"jest": "^29.7.0",
"@types/jest": "^29.5.11", "@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", "@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/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^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": { "engines": {
"node": ">=20.10.0", "node": ">=20.10.0",

View File

@ -1,9 +1,9 @@
import { Response } from "express"; import { Request, Response } from "express";
import { Request } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { AdminService } from "../services/adminService";
import { logger } from "../utils/logger"; 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 ( export const getUserLocale = async (
req: AuthenticatedRequest, req: AuthenticatedRequest,
res: Response res: Response
) => { ): Promise<void> => {
try { try {
logger.info("사용자 로케일 조회 요청", { logger.info("사용자 로케일 조회 요청", {
query: req.query, query: req.query,
user: req.user, user: req.user,
}); });
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회) if (!req.user?.userId) {
const userLocale = "ko"; // 기본값 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 = { const response = {
success: true, success: true,
@ -268,6 +300,8 @@ export const getUserLocale = async (
logger.info("사용자 로케일 조회 성공", { logger.info("사용자 로케일 조회 성공", {
userLocale, userLocale,
userId: req.user.userId,
fromDatabase: !!userInfo?.locale,
}); });
res.status(200).json(response); 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 * API
*/ */
export const getCompanyList = async ( export const getCompanyList = async (
@ -300,17 +403,27 @@ export const getCompanyList = async (
{ {
company_code: "ILSHIN", company_code: "ILSHIN",
company_name: "일신제강", company_name: "일신제강",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
}, },
{ {
company_code: "HUTECH", company_code: "HUTECH",
company_name: "후테크", company_name: "후테크",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
}, },
{ {
company_code: "DAIN", company_code: "DAIN",
company_name: "다인", company_name: "다인",
status: "active",
writer: "admin",
regdate: new Date().toISOString(),
}, },
]; ];
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response = { const response = {
success: true, success: true,
data: dummyCompanies, data: dummyCompanies,
@ -319,6 +432,7 @@ export const getCompanyList = async (
logger.info("회사 목록 조회 성공", { logger.info("회사 목록 조회 성공", {
totalCount: dummyCompanies.length, totalCount: dummyCompanies.length,
response: response,
}); });
res.status(200).json(response); res.status(200).json(response);
@ -390,7 +504,10 @@ export async function getLangKeyList(
res: Response res: Response
): Promise<void> { ): Promise<void> {
try { try {
logger.info("다국어 키 목록 조회 요청"); logger.info("다국어 키 목록 조회 요청", {
query: req.query,
user: req.user,
});
// 더미 데이터 반환 // 더미 데이터 반환
const langKeys = [ const langKeys = [
@ -401,6 +518,10 @@ export async function getLangKeyList(
langKey: "user.management.title", langKey: "user.management.title",
description: "사용자 관리 페이지 제목", description: "사용자 관리 페이지 제목",
isActive: "Y", isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
}, },
{ {
keyId: 2, keyId: 2,
@ -409,6 +530,10 @@ export async function getLangKeyList(
langKey: "menu.management.title", langKey: "menu.management.title",
description: "메뉴 관리 페이지 제목", description: "메뉴 관리 페이지 제목",
isActive: "Y", isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
}, },
{ {
keyId: 3, keyId: 3,
@ -417,15 +542,25 @@ export async function getLangKeyList(
langKey: "dashboard.title", langKey: "dashboard.title",
description: "대시보드 페이지 제목", description: "대시보드 페이지 제목",
isActive: "Y", isActive: "Y",
createdDate: new Date().toISOString(),
createdBy: "admin",
updatedDate: new Date().toISOString(),
updatedBy: "admin",
}, },
]; ];
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response: ApiResponse<any[]> = { const response: ApiResponse<any[]> = {
success: true, success: true,
message: "다국어 키 목록 조회 성공", message: "다국어 키 목록 조회 성공",
data: langKeys, data: langKeys,
}; };
logger.info("다국어 키 목록 조회 성공", {
totalCount: langKeys.length,
response: response,
});
res.status(200).json(response); res.status(200).json(response);
} catch (error) { } catch (error) {
logger.error("다국어 키 목록 조회 실패:", 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 { Response } from "express";
import { AuthenticatedRequest } from "../types/auth"; import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger"; 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 * GET /api/multilang/user-text/:companyCode/:menuCode/:langKey
* API * API ( )
*/ */
export const getUserText = async (req: AuthenticatedRequest, res: Response) => { export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
try { try {
const { companyCode, menuCode, langKey } = req.params; const { companyCode, menuCode, langKey } = req.params;
const { userLang } = req.query; const { userLang } = req.query;
logger.info("다국어 텍스트 조회 요청", { logger.info("단일 다국어 텍스트 조회 요청", {
companyCode, companyCode,
menuCode, menuCode,
langKey, langKey,
@ -19,22 +204,20 @@ export const getUserText = async (req: AuthenticatedRequest, res: Response) => {
user: req.user, user: req.user,
}); });
// 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회) // 배치 API를 사용하여 단일 키 조회
const dummyText = `${menuCode}_${langKey}_${userLang}`; const batchResult = await getBatchTranslations(
{
...req,
body: { langKeys: [langKey] },
query: { companyCode, menuCode, userLang },
} as any,
res
);
const response = { // 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음
success: true, return;
data: dummyText,
message: "다국어 텍스트 조회 성공",
};
logger.info("다국어 텍스트 조회 성공", {
text: dummyText,
});
res.status(200).json(response);
} catch (error) { } catch (error) {
logger.error("다국어 텍스트 조회 실패", { error }); logger.error("단일 다국어 텍스트 조회 실패", { error });
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "다국어 텍스트 조회 중 오류가 발생했습니다.", 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, getAdminMenus,
getUserMenus, getUserMenus,
getMenuInfo, getMenuInfo,
saveMenu, // 메뉴 추가
updateMenu, // 메뉴 수정
deleteMenu, // 메뉴 삭제
deleteMenusBatch, // 메뉴 일괄 삭제
getUserList, getUserList,
getCompanyList, getCompanyList,
getCompanyListFromDB, // 실제 DB에서 회사 목록 조회
getUserLocale, getUserLocale,
setUserLocale,
getLanguageList, getLanguageList,
getLangKeyList, getLangKeyList,
getLangTextList, getLangTextList,
@ -29,15 +35,21 @@ router.use(authenticateToken);
router.get("/menus", getAdminMenus); router.get("/menus", getAdminMenus);
router.get("/user-menus", getUserMenus); router.get("/user-menus", getUserMenus);
router.get("/menus/:menuId", getMenuInfo); 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 // 사용자 관리 API
router.get("/users", getUserList); router.get("/users", getUserList);
// 회사 관리 API // 회사 관리 API
router.get("/companies", getCompanyList); router.get("/companies", getCompanyList);
router.get("/companies/db", getCompanyListFromDB); // 실제 DB에서 회사 목록 조회
// 사용자 로케일 API // 사용자 로케일 API
router.get("/user-locale", getUserLocale); router.get("/user-locale", getUserLocale);
router.post("/user-locale", setUserLocale);
// 다국어 관리 API // 다국어 관리 API
router.get("/multilang/languages", getLanguageList); router.get("/multilang/languages", getLanguageList);

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import { CompanyManagement } from "@/components/admin/CompanyManagement";
/**
*
*/
export default function CompanyPage() {
return <CompanyManagement />;
}

View File

@ -359,43 +359,59 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const loadTranslations = async () => { const loadTranslations = async () => {
try { try {
// 전역 언어 상태에서 현재 언어 가져오기 // 현재 사용자 언어 사용
const currentUserLang = (typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG) || userLang || "KR"; const currentUserLang = userLang || "en";
console.log("🌐 Admin Layout 번역 로드 시작", { console.log("🌐 Admin Layout 번역 로드 시작", {
userLang, userLang,
globalUserLang: typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG,
currentUserLang, currentUserLang,
}); });
// API 직접 호출로 현재 언어 사용 // API 직접 호출로 현재 언어 사용 (배치 조회 방식)
const companyCode = "*"; const companyCode = "*";
const [titleResponse, descriptionResponse] = await Promise.all([ try {
apiClient.get( // 배치 조회 API 사용
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`, const response = await apiClient.post(
), "/multilang/batch",
apiClient.get( {
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`, langKeys: [MENU_MANAGEMENT_KEYS.TITLE, MENU_MANAGEMENT_KEYS.DESCRIPTION],
), },
]); {
params: {
companyCode,
menuCode: "MENU_MANAGEMENT",
userLang: currentUserLang,
},
},
);
const titleData = titleResponse.data; if (response.data.success && response.data.data) {
const descriptionData = descriptionResponse.data; const translations = response.data.data;
const title = translations[MENU_MANAGEMENT_KEYS.TITLE] || "메뉴 관리";
const description =
translations[MENU_MANAGEMENT_KEYS.DESCRIPTION] || "시스템의 메뉴 구조와 권한을 관리합니다.";
const title = titleData.success ? titleData.data : "메뉴 관리"; // 번역 캐시에 저장
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다."; setTranslationCache(currentUserLang, translations);
// 번역 캐시에 저장 // 상태 업데이트
const translations = { setMenuTranslations({ title, description });
[MENU_MANAGEMENT_KEYS.TITLE]: title,
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: description,
};
setTranslationCache(currentUserLang, translations);
// 상태 업데이트 console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang: currentUserLang });
setMenuTranslations({ title, description }); } else {
// 기본값 사용
console.log("🌐 Admin Layout 번역 로드 완료", { title, description, userLang: currentUserLang }); const title = "메뉴 관리";
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
setMenuTranslations({ title, description });
console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: currentUserLang });
}
} catch (error) {
console.error("❌ Admin Layout 배치 번역 로드 실패:", error);
// 오류 시 기본값 사용
const title = "메뉴 관리";
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
setMenuTranslations({ title, description });
}
} catch (error) { } catch (error) {
console.error("❌ Admin Layout 번역 로드 실패:", error); console.error("❌ Admin Layout 번역 로드 실패:", error);
} }

View File

@ -1,3 +1,5 @@
"use client";
import { useState } from "react"; import { useState } from "react";
import { CompanyDeleteState } from "@/types/company"; import { CompanyDeleteState } from "@/types/company";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";

View File

@ -1,5 +1,6 @@
"use client";
import { useCompanyManagement } from "@/hooks/useCompanyManagement"; import { useCompanyManagement } from "@/hooks/useCompanyManagement";
import { MOCK_COMPANIES } from "@/constants/company";
import { CompanyToolbar } from "./CompanyToolbar"; import { CompanyToolbar } from "./CompanyToolbar";
import { CompanyTable } from "./CompanyTable"; import { CompanyTable } from "./CompanyTable";
import { CompanyFormModal } from "./CompanyFormModal"; import { CompanyFormModal } from "./CompanyFormModal";
@ -48,7 +49,7 @@ export function CompanyManagement() {
{/* 툴바 - 검색, 필터, 등록 버튼 */} {/* 툴바 - 검색, 필터, 등록 버튼 */}
<CompanyToolbar <CompanyToolbar
searchFilter={searchFilter} searchFilter={searchFilter}
totalCount={MOCK_COMPANIES.length} totalCount={companies.length} // 실제 API에서 가져온 데이터 개수 사용
filteredCount={companies.length} filteredCount={companies.length}
onSearchChange={updateSearchFilter} onSearchChange={updateSearchFilter}
onSearchClear={clearSearchFilter} onSearchClear={clearSearchFilter}

View File

@ -39,17 +39,29 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
level, level,
parentCompanyCode, parentCompanyCode,
}) => { }) => {
console.log("🎯 MenuFormModal 렌더링 - Props:", {
isOpen,
menuId,
parentId,
menuType,
level,
parentCompanyCode,
});
console.log("🔍 MenuFormModal 컴포넌트 마운트됨");
const [formData, setFormData] = useState<MenuFormData>({ const [formData, setFormData] = useState<MenuFormData>({
parentObjId: parentId || "0", parentObjId: parentId || "0",
menuNameKor: "", menuNameKor: "",
menuUrl: "", menuUrl: "",
menuDesc: "", menuDesc: "",
seq: 1, seq: 1,
menuType: menuType || "1", menuType: "1",
status: "active", status: "ACTIVE",
companyCode: "", companyCode: parentCompanyCode || "none",
langKey: "", // 다국어 키 추가 langKey: "",
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [companies, setCompanies] = useState<Company[]>([]); const [companies, setCompanies] = useState<Company[]>([]);
@ -57,110 +69,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false); const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false);
const [langKeySearchText, setLangKeySearchText] = useState(""); const [langKeySearchText, setLangKeySearchText] = useState("");
// 회사 목록 로드 // loadMenuData 함수를 먼저 정의
useEffect(() => {
if (isOpen) {
loadCompanies();
}
}, [isOpen]);
// 다국어 키 목록 로드
useEffect(() => {
if (isOpen && formData.companyCode) {
loadLangKeys();
}
}, [isOpen, formData.companyCode]);
// 드롭다운 외부 클릭 시 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest(".langkey-dropdown")) {
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}
};
if (isLangKeyDropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isLangKeyDropdownOpen]);
const loadCompanies = async () => {
try {
const companyList = await companyAPI.getList({ status: "active" });
setCompanies(companyList);
} catch (error) {
console.error("회사 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
}
};
const loadLangKeys = async () => {
console.log(`🔤 다국어 키 목록 조회 시작 - companyCode:`, formData.companyCode);
try {
const response = await menuApi.getLangKeys({
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
});
if (response.success && response.data) {
// 활성화된 다국어 키만 필터링
const activeKeys = response.data.filter((key) => key.isActive === "Y");
console.log(`🔤 다국어 키 목록 조회 성공:`, activeKeys.length, "개 (활성화된 키)");
setLangKeys(activeKeys);
}
} catch (error) {
console.error("❌ 다국어 키 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
setLangKeys([]);
}
};
useEffect(() => {
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
if (menuId) {
console.log("메뉴 수정 모드 - menuId:", menuId);
setIsEdit(true);
loadMenuData();
} else {
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
setIsEdit(false);
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
let defaultMenuType = "1"; // 기본값은 사용자
if (menuType === "0" || menuType === "admin") {
defaultMenuType = "0"; // 관리자
} else if (menuType === "1" || menuType === "user") {
defaultMenuType = "1"; // 사용자
}
setFormData({
parentObjId: parentId || "0",
menuNameKor: "",
menuUrl: "",
menuDesc: "",
seq: 1,
menuType: defaultMenuType,
status: "ACTIVE", // 기본값은 활성화
companyCode: parentCompanyCode || "none", // 상위 메뉴의 회사 코드를 기본값으로 설정
langKey: "", // 다국어 키 초기화
});
console.log("메뉴 등록 기본값 설정:", {
parentObjId: parentId || "0",
menuType: defaultMenuType,
status: "ACTIVE",
companyCode: "",
langKey: "",
});
}
}, [menuId, parentId, menuType]);
const loadMenuData = async () => { const loadMenuData = async () => {
console.log("loadMenuData 호출됨 - menuId:", menuId); console.log("loadMenuData 호출됨 - menuId:", menuId);
if (!menuId) { if (!menuId) {
@ -246,6 +155,129 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
} }
}; };
// useEffect를 loadMenuData 함수 정의 후로 이동
useEffect(() => {
console.log("🚀 MenuFormModal useEffect 실행됨!");
console.log("📋 useEffect 파라미터:", { menuId, parentId, menuType });
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
if (menuId) {
console.log("메뉴 수정 모드 - menuId:", menuId);
setIsEdit(true);
loadMenuData();
} else {
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
setIsEdit(false);
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
let defaultMenuType = "1"; // 기본값은 사용자
if (menuType === "0" || menuType === "admin") {
defaultMenuType = "0"; // 관리자
} else if (menuType === "1" || menuType === "user") {
defaultMenuType = "1"; // 사용자
}
setFormData({
parentObjId: parentId || "0",
menuNameKor: "",
menuUrl: "",
menuDesc: "",
seq: 1,
menuType: defaultMenuType,
status: "ACTIVE", // 기본값은 활성화
companyCode: parentCompanyCode || "none", // 상위 메뉴의 회사 코드를 기본값으로 설정
langKey: "", // 다국어 키 초기화
});
console.log("메뉴 등록 기본값 설정:", {
parentObjId: parentId || "0",
menuType: defaultMenuType,
status: "ACTIVE",
companyCode: "",
langKey: "",
});
}
}, [menuId, parentId, menuType]);
// 강제로 useEffect 실행시키기 위한 별도 useEffect
useEffect(() => {
console.log("🔧 강제 useEffect 실행 - 컴포넌트 마운트됨");
console.log("🔧 현재 props:", { isOpen, menuId, parentId, menuType });
// isOpen이 true일 때만 실행
if (isOpen && menuId) {
console.log("🔧 모달이 열렸고 menuId가 있음 - 강제 실행");
// 약간의 지연 후 실행
setTimeout(() => {
console.log("🔧 setTimeout으로 loadMenuData 실행");
loadMenuData();
}, 100);
}
}, [isOpen]); // isOpen만 의존성으로 설정
// 회사 목록 로드
useEffect(() => {
if (isOpen) {
loadCompanies();
}
}, [isOpen]);
// 다국어 키 목록 로드
useEffect(() => {
if (isOpen && formData.companyCode) {
loadLangKeys();
}
}, [isOpen, formData.companyCode]);
// 드롭다운 외부 클릭 시 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest(".langkey-dropdown")) {
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}
};
if (isLangKeyDropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isLangKeyDropdownOpen]);
const loadCompanies = async () => {
try {
const companyList = await companyAPI.getList({ status: "active" });
setCompanies(companyList);
} catch (error) {
console.error("회사 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
}
};
const loadLangKeys = async () => {
console.log("🔤 다국어 키 목록 조회 시작 - companyCode:", formData.companyCode);
try {
const response = await menuApi.getLangKeys({
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
});
if (response.success && response.data) {
// 활성화된 다국어 키만 필터링
const activeKeys = response.data.filter((key) => key.isActive === "Y");
console.log("🔤 다국어 키 목록 조회 성공:", activeKeys.length, "개 (활성화된 키)");
setLangKeys(activeKeys);
}
} catch (error) {
console.error("❌ 다국어 키 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
setLangKeys([]);
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -270,7 +302,18 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
}; };
console.log("저장할 데이터:", submitData); console.log("저장할 데이터:", submitData);
const response = await menuApi.saveMenu(submitData);
let response;
if (isEdit && menuId) {
// 수정 모드: updateMenu API 호출
console.log("🔧 메뉴 수정 API 호출:", menuId);
response = await menuApi.updateMenu(menuId, submitData);
} else {
// 추가 모드: saveMenu API 호출
console.log(" 메뉴 추가 API 호출");
response = await menuApi.saveMenu(submitData);
}
if (response.success) { if (response.success) {
toast.success(response.message); toast.success(response.message);
@ -280,6 +323,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
toast.error(response.message); toast.error(response.message);
} }
} catch (error) { } catch (error) {
console.error("메뉴 저장/수정 실패:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED)); toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -460,13 +460,34 @@ export const MenuManagement: React.FC = () => {
}; };
const handleEditMenu = (menuId: string) => { const handleEditMenu = (menuId: string) => {
setFormData({ console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
menuId,
parentId: "", // 현재 메뉴 정보 찾기
menuType: "", const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
level: 0, const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
parentCompanyCode: "",
}); if (menuToEdit) {
console.log("수정할 메뉴 정보:", menuToEdit);
setFormData({
menuId: menuId,
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
menuType: selectedMenuType, // 현재 선택된 메뉴 타입
level: 0, // 기본값
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
});
console.log("설정된 formData:", {
menuId: menuId,
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
menuType: selectedMenuType,
level: 0,
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
});
} else {
console.error("수정할 메뉴를 찾을 수 없음:", menuId);
}
setFormModalOpen(true); setFormModalOpen(true);
}; };

View File

@ -38,14 +38,19 @@ export const useCompanyManagement = () => {
setError(null); setError(null);
try { try {
// 실제 데이터베이스에서 회사 목록 조회
const searchParams = { const searchParams = {
company_name: searchFilter.company_name, company_name: searchFilter.company_name,
status: searchFilter.status === "all" ? undefined : searchFilter.status, status: searchFilter.status === "all" ? undefined : searchFilter.status,
}; };
// 더미 데이터 대신 실제 API 호출
const data = await companyAPI.getList(searchParams); const data = await companyAPI.getList(searchParams);
setCompanies(data); setCompanies(data);
console.log("✅ 실제 DB에서 회사 목록 조회 성공:", data.length, "개");
} catch (err) { } catch (err) {
console.error("❌ 회사 목록 조회 실패:", err);
setError(err instanceof Error ? err.message : "회사 목록 조회에 실패했습니다."); setError(err instanceof Error ? err.message : "회사 목록 조회에 실패했습니다.");
setCompanies([]); setCompanies([]);
} finally { } finally {

View File

@ -1,38 +1,36 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { setTranslationCache } from "@/lib/utils/multilang";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
interface UseMultiLangOptions { // 전역 언어 상태 (다른 컴포넌트에서 접근 가능)
companyCode?: string;
defaultLang?: string;
}
// 전역 언어 상태 관리
let globalUserLang = "KR"; let globalUserLang = "KR";
let globalChangeLangCallback: ((lang: string) => void) | null = null; let globalChangeLangCallback: ((lang: string) => void) | null = null;
export function useMultiLang(options: UseMultiLangOptions = {}) { export const useMultiLang = (options: { companyCode?: string } = {}) => {
const { companyCode = "ILSHIN", defaultLang = "KR" } = options; const [userLang, setUserLang] = useState<string>("KR");
const [userLang, setUserLang] = useState(globalUserLang || defaultLang); const companyCode = options.companyCode || "*";
// 전역 언어 상태 업데이트 // 전역 언어 상태 동기화
useEffect(() => { useEffect(() => {
globalUserLang = userLang; if (globalUserLang !== userLang) {
// window 객체에 전역 언어 상태 저장 (API 클라이언트에서 접근용) setUserLang(globalUserLang);
if (typeof window !== "undefined") {
(window as any).__GLOBAL_USER_LANG = userLang;
console.log("전역 언어 상태 설정:", userLang);
} }
}, [globalUserLang]);
// 언어 변경 시 전역 콜백 호출
useEffect(() => {
if (globalChangeLangCallback) { if (globalChangeLangCallback) {
globalChangeLangCallback(userLang); globalChangeLangCallback(userLang);
} }
}, [userLang]); }, [userLang]);
// API 기본 URL 설정 // 사용자 로케일 조회 (한 번만 실행)
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
// 브라우저 언어 감지
useEffect(() => { useEffect(() => {
// 이미 로케일이 설정되어 있으면 중복 호출 방지
if (globalUserLang && globalUserLang !== "KR") {
setUserLang(globalUserLang);
return;
}
const fetchUserLocale = async () => { const fetchUserLocale = async () => {
try { try {
console.log("🔍 사용자 로케일 조회 시작"); console.log("🔍 사용자 로케일 조회 시작");
@ -42,47 +40,31 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
const userLocale = response.data.data; const userLocale = response.data.data;
console.log("✅ 사용자 로케일 조회 성공:", userLocale); console.log("✅ 사용자 로케일 조회 성공:", userLocale);
// 사용자 로케일을 데이터베이스 언어 코드로 매핑 // 데이터베이스의 locale 값을 그대로 사용 (매핑 없음)
const langMapping: Record<string, string> = { setUserLang(userLocale);
ko: "KR", globalUserLang = userLocale; // 전역 상태도 업데이트
en: "US",
ja: "JP",
zh: "CN",
};
const mappedLang = langMapping[userLocale] || userLocale;
console.log("🔄 언어 매핑:", userLocale, "->", mappedLang);
setUserLang(mappedLang);
return; return;
} }
// API 호출 실패 시 브라우저 언어 사용 // API 호출 실패 시 브라우저 언어 사용
console.warn("⚠️ 사용자 로케일 조회 실패, 브라우저 언어 사용"); console.warn("⚠️ 사용자 로케일 조회 실패, 브라우저 언어 사용");
const browserLang = navigator.language.split("-")[0]; const browserLang = navigator.language.split("-")[0];
const langMapping: Record<string, string> = {
ko: "KR",
en: "US",
ja: "JP",
zh: "CN",
};
if (langMapping[browserLang]) { // 브라우저 언어를 그대로 사용 (매핑 없음)
setUserLang(langMapping[browserLang]); if (["ko", "en", "ja", "zh"].includes(browserLang)) {
setUserLang(browserLang);
globalUserLang = browserLang;
} }
} catch (error) { } catch (error) {
console.error("❌ 사용자 로케일 조회 중 오류:", error); console.error("❌ 사용자 로케일 조회 중 오류:", error);
// 오류 시 브라우저 언어 사용 // 오류 시 브라우저 언어 사용
const browserLang = navigator.language.split("-")[0]; const browserLang = navigator.language.split("-")[0];
const langMapping: Record<string, string> = {
ko: "KR",
en: "US",
ja: "JP",
zh: "CN",
};
if (langMapping[browserLang]) { // 브라우저 언어를 그대로 사용 (매핑 없음)
setUserLang(langMapping[browserLang]); if (["ko", "en", "ja", "zh"].includes(browserLang)) {
setUserLang(browserLang);
globalUserLang = browserLang;
} }
} }
}; };
@ -90,42 +72,69 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
fetchUserLocale(); fetchUserLocale();
}, []); }, []);
// 다국어 텍스트 가져오기 // 다국어 텍스트 가져오기 (배치 조회 방식)
const getText = async (menuCode: string, langKey: string, fallback?: string): Promise<string> => { const getText = async (menuCode: string, langKey: string, fallback?: string): Promise<string> => {
console.log(`🔍 다국어 텍스트 요청:`, { menuCode, langKey, userLang, companyCode }); console.log(`🔍 다국어 텍스트 요청 (배치 방식):`, { menuCode, langKey, userLang, companyCode });
try { try {
const url = `/multilang/user-text/${companyCode}/${menuCode}/${langKey}?userLang=${userLang}`; // 배치 조회 API 사용
console.log(`📡 API 요청 URL:`, url); const response = await apiClient.post(
"/multilang/batch",
{
langKeys: [langKey],
},
{
params: {
companyCode,
menuCode,
userLang,
},
},
);
const response = await apiClient.get(url); console.log(`📡 배치 API 응답 상태:`, response.status, response.statusText);
console.log(`📡 API 응답 상태:`, response.status, response.statusText); if (response.data.success && response.data.data && response.data.data[langKey]) {
// 번역 텍스트를 캐시에 저장
if (response.data.success && response.data.data) {
// 개별 번역 텍스트를 캐시에 저장
const cacheKey = `${menuCode}.${langKey}`; const cacheKey = `${menuCode}.${langKey}`;
const currentCache = (window as any).__TRANSLATION_CACHE || {}; const currentCache = (window as any).__TRANSLATION_CACHE || {};
currentCache[cacheKey] = response.data.data; currentCache[cacheKey] = response.data.data[langKey];
(window as any).__TRANSLATION_CACHE = currentCache; (window as any).__TRANSLATION_CACHE = currentCache;
return response.data.data; return response.data.data[langKey];
} }
// 실패 시 fallback 또는 키 반환 // 실패 시 fallback 또는 키 반환
console.log(`🔄 API 성공했지만 데이터 없음, fallback 반환:`, fallback || langKey); console.log(`🔄 배치 API 성공했지만 데이터 없음, fallback 반환:`, fallback || langKey);
return fallback || langKey; return fallback || langKey;
} catch (error) { } catch (error) {
console.error("❌ 다국어 텍스트 조회 실패:", error); console.error("❌ 다국어 텍스트 배치 조회 실패:", error);
console.log(`🔄 에러 시 fallback 반환:`, fallback || langKey); console.log(`🔄 에러 시 fallback 반환:`, fallback || langKey);
return fallback || langKey; return fallback || langKey;
} }
}; };
// 언어 변경 // 언어 변경
const changeLang = (newLang: string) => { const changeLang = async (newLang: string) => {
setUserLang(newLang); try {
globalUserLang = newLang; // 백엔드에 사용자 로케일 설정 요청
const response = await apiClient.post("/admin/user-locale", {
locale: newLang,
});
if (response.data.success) {
setUserLang(newLang);
globalUserLang = newLang;
console.log("✅ 사용자 로케일 변경 성공:", newLang);
} else {
console.error("❌ 사용자 로케일 변경 실패:", response.data.message);
}
} catch (error) {
console.error("❌ 사용자 로케일 변경 중 오류:", error);
// 오류 시에도 로컬 상태는 변경
setUserLang(newLang);
globalUserLang = newLang;
}
}; };
// 전역 언어 상태 접근자 // 전역 언어 상태 접근자
@ -142,4 +151,4 @@ export function useMultiLang(options: UseMultiLangOptions = {}) {
getGlobalUserLang, getGlobalUserLang,
setGlobalChangeLangCallback, setGlobalChangeLangCallback,
}; };
} };

View File

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

View File

@ -3,6 +3,7 @@
*/ */
import { Company, CompanyFormData } from "@/types/company"; import { Company, CompanyFormData } from "@/types/company";
import { apiClient } from "./client";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api";
@ -15,31 +16,6 @@ interface ApiResponse<T = any> {
errorCode?: string; errorCode?: string;
} }
// API 호출 헬퍼 함수
async function apiCall<T = any>(endpoint: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
credentials: "include", // 세션 쿠키를 포함
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || `HTTP ${response.status}`);
}
return data;
} catch (error) {
console.error(`API Error [${endpoint}]:`, error);
throw error;
}
}
/** /**
* *
*/ */
@ -54,28 +30,37 @@ export async function getCompanyList(params?: { company_name?: string; status?:
} }
const queryString = searchParams.toString(); const queryString = searchParams.toString();
const endpoint = `/admin/companies${queryString ? `?${queryString}` : ""}`; // 실제 데이터베이스에서 회사 목록 조회하는 엔드포인트 사용
const endpoint = `/admin/companies/db${queryString ? `?${queryString}` : ""}`;
const response = await apiCall<Company[]>(endpoint); console.log("🔍 실제 DB에서 회사 목록 조회 API 호출:", endpoint);
if (response.success && response.data) { try {
return response.data; const response = await apiClient.get(endpoint);
if (response.data.success && response.data.data) {
console.log("✅ 실제 DB에서 회사 목록 조회 성공:", response.data.data.length, "개");
return response.data.data;
}
throw new Error(response.data.message || "회사 목록 조회에 실패했습니다.");
} catch (error) {
console.error("❌ 실제 DB에서 회사 목록 조회 실패:", error);
throw error;
} }
throw new Error(response.message || "회사 목록 조회에 실패했습니다.");
} }
/** /**
* *
*/ */
export async function getCompanyInfo(companyCode: string): Promise<Company> { export async function getCompanyInfo(companyCode: string): Promise<Company> {
const response = await apiCall<Company>(`/admin/companies/${companyCode}`); const response = await apiClient.get(`/admin/companies/${companyCode}`);
if (response.success && response.data) { if (response.data.success && response.data.data) {
return response.data; return response.data.data;
} }
throw new Error(response.message || "회사 정보 조회에 실패했습니다."); throw new Error(response.data.message || "회사 정보 조회에 실패했습니다.");
} }
/** /**
@ -84,22 +69,19 @@ export async function getCompanyInfo(companyCode: string): Promise<Company> {
export async function createCompany(formData: CompanyFormData): Promise<Company> { export async function createCompany(formData: CompanyFormData): Promise<Company> {
console.log("회사 등록 요청:", formData); console.log("회사 등록 요청:", formData);
const response = await apiCall<Company>("/admin/companies", { const response = await apiClient.post("/admin/companies", formData);
method: "POST",
body: JSON.stringify(formData),
});
if (response.success && response.data) { if (response.data.success && response.data.data) {
console.log("회사 등록 완료:", { console.log("회사 등록 완료:", {
code: response.data.company_code, code: response.data.data.company_code,
name: response.data.company_name, name: response.data.data.company_name,
writer: response.data.writer, writer: response.data.data.writer,
}); });
return response.data; return response.data.data;
} }
throw new Error(response.message || "회사 등록에 실패했습니다."); throw new Error(response.data.message || "회사 등록에 실패했습니다.");
} }
/** /**
@ -109,28 +91,23 @@ export async function updateCompany(
companyCode: string, companyCode: string,
formData: Partial<CompanyFormData> & { status?: string }, formData: Partial<CompanyFormData> & { status?: string },
): Promise<Company> { ): Promise<Company> {
const response = await apiCall<Company>(`/admin/companies/${companyCode}`, { const response = await apiClient.put(`/admin/companies/${companyCode}`, formData);
method: "PUT",
body: JSON.stringify(formData),
});
if (response.success && response.data) { if (response.data.success && response.data.data) {
return response.data; return response.data.data;
} }
throw new Error(response.message || "회사 정보 수정에 실패했습니다."); throw new Error(response.data.message || "회사 정보 수정에 실패했습니다.");
} }
/** /**
* *
*/ */
export async function deleteCompany(companyCode: string): Promise<void> { export async function deleteCompany(companyCode: string): Promise<void> {
const response = await apiCall(`/admin/companies/${companyCode}`, { const response = await apiClient.delete(`/admin/companies/${companyCode}`);
method: "DELETE",
});
if (!response.success) { if (!response.data.success) {
throw new Error(response.message || "회사 삭제에 실패했습니다."); throw new Error(response.data.message || "회사 삭제에 실패했습니다.");
} }
} }

View File

@ -107,6 +107,12 @@ export const menuApi = {
return response.data; return response.data;
}, },
// 메뉴 수정
updateMenu: async (menuId: string, menuData: MenuFormData): Promise<ApiResponse<void>> => {
const response = await apiClient.put(`/admin/menus/${menuId}`, menuData);
return response.data;
},
// 메뉴 삭제 // 메뉴 삭제
deleteMenu: async (menuId: string): Promise<ApiResponse<void>> => { deleteMenu: async (menuId: string): Promise<ApiResponse<void>> => {
const response = await apiClient.delete(`/admin/menus/${menuId}`); const response = await apiClient.delete(`/admin/menus/${menuId}`);
@ -139,7 +145,16 @@ export const menuApi = {
menuCode?: string; menuCode?: string;
keyType?: string; keyType?: string;
}): Promise<ApiResponse<LangKey[]>> => { }): Promise<ApiResponse<LangKey[]>> => {
const response = await apiClient.get("/multilang/keys", { params }); console.log("🔍 다국어 키 목록 조회 API 호출:", "/admin/multilang/keys", params);
return response.data;
try {
// Node.js 백엔드의 실제 라우팅과 일치하도록 수정
const response = await apiClient.get("/admin/multilang/keys", { params });
console.log("✅ 다국어 키 목록 조회 성공:", response.data);
return response.data;
} catch (error) {
console.error("❌ 다국어 키 목록 조회 실패:", error);
throw error;
}
}, },
}; };

View File

@ -1,25 +1,22 @@
import { useMultiLang } from "@/hooks/useMultiLang";
import { apiClient } from "../api/client"; import { apiClient } from "../api/client";
// 메뉴 관리 화면 다국어 키 상수 // 메뉴 관리 화면 다국어 키 상수
export const MENU_MANAGEMENT_KEYS = { export const MENU_MANAGEMENT_KEYS = {
// 메뉴 타입 관련 // 기본 정보
TITLE: "menu.management.title", TITLE: "title",
DESCRIPTION: "menu.management.description", DESCRIPTION: "description",
MENU_TYPE_TITLE: "menu.type.title", MENU_TYPE_TITLE: "menu.type.title",
ADMIN_MENU: "menu.management.admin",
USER_MENU: "menu.management.user",
ADMIN_DESCRIPTION: "menu.management.admin.description",
USER_DESCRIPTION: "menu.management.user.description",
MENU_TYPE_ADMIN: "menu.type.admin", MENU_TYPE_ADMIN: "menu.type.admin",
MENU_TYPE_USER: "menu.type.user", MENU_TYPE_USER: "menu.type.user",
ADMIN_MENU: "admin.menu",
USER_MENU: "user.menu",
ADMIN_DESCRIPTION: "admin.description",
USER_DESCRIPTION: "user.description",
LIST_TITLE: "list.title",
LIST_TOTAL: "list.total",
LIST_SEARCH_RESULT: "list.search.result",
// 메뉴 목록 관련 // 필터 관련
LIST_TITLE: "menu.list.title",
LIST_TOTAL: "menu.list.total",
LIST_SEARCH_RESULT: "menu.list.search.result",
// 필터 및 검색 관련
FILTER_COMPANY: "filter.company", FILTER_COMPANY: "filter.company",
FILTER_COMPANY_ALL: "filter.company.all", FILTER_COMPANY_ALL: "filter.company.all",
FILTER_COMPANY_COMMON: "filter.company.common", FILTER_COMPANY_COMMON: "filter.company.common",
@ -43,7 +40,7 @@ export const MENU_MANAGEMENT_KEYS = {
BUTTON_REGISTER: "button.register", BUTTON_REGISTER: "button.register",
BUTTON_MODIFY: "button.modify", BUTTON_MODIFY: "button.modify",
// 메뉴 폼 관련 // 폼 관련
FORM_MENU_TYPE: "form.menu.type", FORM_MENU_TYPE: "form.menu.type",
FORM_MENU_TYPE_ADMIN: "form.menu.type.admin", FORM_MENU_TYPE_ADMIN: "form.menu.type.admin",
FORM_MENU_TYPE_USER: "form.menu.type.user", FORM_MENU_TYPE_USER: "form.menu.type.user",
@ -115,215 +112,172 @@ export const MENU_MANAGEMENT_KEYS = {
UI_LANGUAGE: "ui.language", UI_LANGUAGE: "ui.language",
} as const; } as const;
// 다국어 텍스트 가져오기 함수 // 다국어 텍스트 캐시 (메모리 기반)
export const useMenuManagementText = () => { const translationCache: Record<string, Record<string, string>> = {};
const { getText } = useMultiLang({ companyCode: "*" });
const getMenuText = async (key: string, params?: Record<string, string | number>): Promise<string> => { // 배치 조회를 위한 키 수집기
let text = await getText("MENU_MANAGEMENT", key); const pendingKeys: Set<string> = new Set();
let batchTimeout: NodeJS.Timeout | null = null;
const BATCH_DELAY = 50; // 50ms 지연으로 배치 처리
// 파라미터 치환 /**
if (params) { *
Object.entries(params).forEach(([paramKey, paramValue]) => { * API
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString()); */
}); async function fetchBatchTranslations(
keys: string[],
companyCode: string = "*",
menuCode: string = "MENU_MANAGEMENT",
userLang: string = "KR",
): Promise<Record<string, string>> {
try {
console.log(`🚀 배치 조회 시작: ${keys.length}개 키`);
const response = await apiClient.post(
"/multilang/batch",
{
langKeys: keys,
},
{
params: {
companyCode,
menuCode,
userLang,
},
},
);
if (response.data.success) {
console.log(`✅ 배치 조회 성공: ${keys.length}개 키`);
return response.data.data || {};
} else {
console.error("❌ 배치 조회 실패:", response.data.message);
return {};
} }
} catch (error) {
console.error("❌ 배치 조회 오류:", error);
return {};
}
}
return text; /**
}; * ( )
* API
return { */
getMenuText, export async function getMultilangText(
keys: MENU_MANAGEMENT_KEYS, key: string,
}; companyCode: string = "*",
}; menuCode: string = "MENU_MANAGEMENT",
userLang: string = "KR",
// 전역 번역 캐시 ): Promise<string> {
let translationCache: Record<string, Record<string, string>> = {}; // 1. 캐시에서 먼저 확인
const cacheKey = `${userLang}_${companyCode}_${menuCode}`;
// 번역 캐시 설정 함수 if (translationCache[cacheKey]?.[key]) {
export const setTranslationCache = (lang: string, translations: Record<string, string>) => { return translationCache[cacheKey][key];
translationCache[lang] = translations;
};
// 번역 캐시 가져오기 함수
export const getTranslationCache = (lang: string): Record<string, string> => {
return translationCache[lang] || {};
};
// 동기적 다국어 텍스트 가져오기 (캐시된 값 사용)
export const getMenuTextSync = (key: string, params?: Record<string, any>): string => {
// 전역 언어 상태 확인
const userLang = (typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG) || "KR";
// 현재 언어가 한국어가 아니면 캐시에서 번역 텍스트 찾기
if (userLang !== "KR") {
// 1. 먼저 전역 캐시에서 찾기
const cachedTranslations = getTranslationCache(userLang);
if (cachedTranslations[key]) {
let text = cachedTranslations[key];
// 파라미터 치환
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
});
}
return text;
}
// 2. 개별 캐시에서 찾기
const individualCache = (typeof window !== "undefined" && (window as any).__TRANSLATION_CACHE) || {};
const cacheKey = `MENU_MANAGEMENT.${key}`;
if (individualCache[cacheKey]) {
let text = individualCache[cacheKey];
// 파라미터 치환
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
});
}
console.log(`✅ 개별 캐시에서 번역 사용:`, { key, result: text, userLang });
return text;
}
// 3. 캐시에 없으면 비동기적으로 로드 시도
console.log(`⚠️ getMenuTextSync: 캐시에 번역이 없습니다. 키: ${key}, 언어: ${userLang}`);
// 비동기적으로 번역 로드 (백그라운드에서)
if (typeof window !== "undefined") {
const companyCode = "*";
apiClient
.get(`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${key}?userLang=${userLang}`)
.then((response) => {
if (response.data.success && response.data.data) {
// 개별 캐시에 저장
const currentCache = (window as any).__TRANSLATION_CACHE || {};
currentCache[cacheKey] = data.data;
(window as any).__TRANSLATION_CACHE = currentCache;
// 전역 캐시에도 저장
const globalCache = getTranslationCache(userLang);
globalCache[key] = data.data;
setTranslationCache(userLang, globalCache);
// 페이지 리렌더링을 위해 이벤트 발생
window.dispatchEvent(
new CustomEvent("translation-loaded", {
detail: { key, text: data.data, userLang },
}),
);
}
})
.catch((error) => {
console.error(`❌ 백그라운드 번역 로드 실패:`, { key, error });
});
}
// 캐시에 없으면 기본 텍스트에서 찾기
const defaultTexts: Record<string, string> = {
[MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리",
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.",
[MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE]: "메뉴 타입",
[MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN]: "관리자",
[MENU_MANAGEMENT_KEYS.MENU_TYPE_USER]: "사용자",
[MENU_MANAGEMENT_KEYS.ADMIN_MENU]: "관리자 메뉴",
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT]: "선택 삭제 ({count})",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING]: "삭제 중...",
[MENU_MANAGEMENT_KEYS.BUTTON_CANCEL]: "취소",
[MENU_MANAGEMENT_KEYS.BUTTON_SAVE]: "저장",
[MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING]: "저장 중...",
[MENU_MANAGEMENT_KEYS.BUTTON_REGISTER]: "등록",
[MENU_MANAGEMENT_KEYS.BUTTON_MODIFY]: "수정",
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE]: "메뉴 타입",
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN]: "관리자",
[MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER]: "사용자",
[MENU_MANAGEMENT_KEYS.FORM_STATUS]: "상태",
[MENU_MANAGEMENT_KEYS.FORM_STATUS_ACTIVE]: "활성화",
[MENU_MANAGEMENT_KEYS.FORM_STATUS_INACTIVE]: "비활성화",
[MENU_MANAGEMENT_KEYS.FORM_COMPANY]: "회사",
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT]: "회사를 선택하세요",
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON]: "공통",
[MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE]: "하위 메뉴는 상위 메뉴와 동일한 회사를 가져야 합니다.",
[MENU_MANAGEMENT_KEYS.FORM_MENU_NAME]: "메뉴명",
[MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER]: "메뉴명을 입력하세요",
[MENU_MANAGEMENT_KEYS.FORM_MENU_URL]: "URL",
[MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER]: "메뉴 URL을 입력하세요",
[MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION]: "설명",
[MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER]: "메뉴 설명을 입력하세요",
[MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE]: "순서",
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY]: "다국어 키",
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT]: "다국어 키를 선택하세요",
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE]: "다국어 키 없음",
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH]: "다국어 키 검색...",
[MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED]: "선택된 키: {key} - {description}",
[MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE]: "메뉴 등록",
[MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE]: "메뉴 수정",
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_TITLE]: "메뉴 삭제",
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_DESCRIPTION]: "해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
[MENU_MANAGEMENT_KEYS.MODAL_DELETE_BATCH_DESCRIPTION]:
"선택된 {count}개의 메뉴를 영구적으로 삭제하시겠습니까?\n\n⚠ 주의: 상위 메뉴를 삭제하면 하위 메뉴들도 함께 삭제됩니다.\n이 작업은 되돌릴 수 없습니다.",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_SELECT]: "선택",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME]: "메뉴명",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL]: "URL",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE]: "메뉴 타입",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS]: "상태",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY]: "회사",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE]: "순서",
[MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS]: "작업",
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
[MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴 삭제 중...",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 저장되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 삭제되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]:
"✅ {count}개의 메뉴(및 하위 메뉴)가 성공적으로 삭제되었습니다!",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "⚠️ {success}개 삭제 성공, {failed}개 삭제 실패",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
[MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
[MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
[MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
};
let text = defaultTexts[key] || key;
// 파라미터 치환
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
});
}
return text;
} }
// 한국어인 경우 기본 텍스트 반환 // 2. 기본 텍스트에서 확인
const defaultText = getDefaultText(key);
if (defaultText) {
return defaultText;
}
// 3. 배치 처리에 추가
pendingKeys.add(key);
// 4. 배치 타임아웃 설정
if (batchTimeout) {
clearTimeout(batchTimeout);
}
return new Promise((resolve) => {
batchTimeout = setTimeout(async () => {
try {
const keysToFetch = Array.from(pendingKeys);
pendingKeys.clear();
if (keysToFetch.length > 0) {
const translations = await fetchBatchTranslations(keysToFetch, companyCode, menuCode, userLang);
// 캐시에 저장
if (!translationCache[cacheKey]) {
translationCache[cacheKey] = {};
}
Object.assign(translationCache[cacheKey], translations);
// 요청된 키에 대한 번역 반환
if (translations[key]) {
resolve(translations[key]);
} else {
resolve(defaultText || key);
}
} else {
resolve(defaultText || key);
}
} catch (error) {
console.error("❌ 배치 처리 오류:", error);
resolve(defaultText || key);
}
}, BATCH_DELAY);
});
}
/**
* ()
* UI
*/
export function getMultilangTextSync(
key: string,
companyCode: string = "*",
menuCode: string = "MENU_MANAGEMENT",
userLang: string = "KR",
): string {
// 1. 캐시에서 확인
const cacheKey = `${userLang}_${companyCode}_${menuCode}`;
if (translationCache[cacheKey]?.[key]) {
return translationCache[cacheKey][key];
}
// 2. 기본 텍스트에서 확인
const defaultText = getDefaultText(key);
if (defaultText) {
return defaultText;
}
// 3. 캐시에 없으면 비동기적으로 로드 (백그라운드)
if (typeof window !== "undefined") {
getMultilangText(key, companyCode, menuCode, userLang).then((text) => {
// 페이지 리렌더링을 위해 이벤트 발생
window.dispatchEvent(
new CustomEvent("translation-loaded", {
detail: { key, text, userLang },
}),
);
});
}
return defaultText || key;
}
/**
* ( )
*/
export async function getMenuText(key: string, userLang: string = "KR"): Promise<string> {
return getMultilangText(key, "*", "MENU_MANAGEMENT", userLang);
}
/**
*
*/
export function getMenuTextSync(key: string, userLang: string = "KR"): string {
return getMultilangTextSync(key, "*", "MENU_MANAGEMENT", userLang);
}
/**
* ()
*/
function getDefaultText(key: string): string {
const defaultTexts: Record<string, string> = { const defaultTexts: Record<string, string> = {
[MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리", [MENU_MANAGEMENT_KEYS.TITLE]: "메뉴 관리",
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.", [MENU_MANAGEMENT_KEYS.DESCRIPTION]: "시스템의 메뉴 구조와 권한을 관리합니다.",
@ -334,16 +288,6 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴", [MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴", [MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴", [MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
[MENU_MANAGEMENT_KEYS.LIST_TITLE]: "메뉴 목록",
[MENU_MANAGEMENT_KEYS.LIST_TOTAL]: "총 {count}개의 메뉴가 있습니다.",
[MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT]: "검색 결과: {count}개",
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY]: "회사",
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL]: "전체 회사",
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON]: "공통",
[MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH]: "회사명 검색...",
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH]: "검색어",
[MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER]: "메뉴명 또는 URL 검색",
[MENU_MANAGEMENT_KEYS.FILTER_RESET]: "초기화",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가", [MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가", [MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가", [MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
@ -396,38 +340,44 @@ export const getMenuTextSync = (key: string, params?: Record<string, any>): stri
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화", [MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화", [MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정", [MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
[MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴 삭제 중...",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 저장되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 삭제되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]:
"✅ {count}개의 메뉴(및 하위 메뉴)가 성공적으로 삭제되었습니다!",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "⚠️ {success}개 삭제 성공, {failed}개 삭제 실패",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택해주세요.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
[MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
[MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
[MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
[MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
}; };
let text = defaultTexts[key] || key; return defaultTexts[key] || key;
}
// 파라미터 치환 /**
if (params) { *
Object.entries(params).forEach(([paramKey, paramValue]) => { */
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString()); export const setTranslationCache = (lang: string, translations: Record<string, string>) => {
}); translationCache[lang] = translations;
} };
return text; /**
*
*/
export const getTranslationCache = (lang: string): Record<string, string> => {
return translationCache[lang] || {};
};
/**
* ( )
*/
export const useMenuManagementText = () => {
const getMenuText = async (key: string, params?: Record<string, string | number>): Promise<string> => {
let text = await getMultilangText(key, "*", "MENU_MANAGEMENT", "KR");
// 파라미터 치환
if (params) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), paramValue.toString());
});
}
return text;
};
return {
getMenuText,
keys: MENU_MANAGEMENT_KEYS,
};
}; };