From 96c601a0cf409c72dc3af857de3b3275734659b3 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 25 Aug 2025 15:12:31 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/adminController.ts | 106 +- .../src/controllers/multilangController.ts | 1072 ++++++++++++----- backend-node/src/routes/adminRoutes.ts | 24 - backend-node/src/routes/multilangRoutes.ts | 55 +- backend-node/src/services/multilangService.ts | 855 +++++++++++++ backend-node/src/types/multilang.ts | 130 ++ frontend/components/admin/MultiLang.tsx | 49 +- frontend/lib/api/client.ts | 2 +- 8 files changed, 1900 insertions(+), 393 deletions(-) create mode 100644 backend-node/src/services/multilangService.ts create mode 100644 backend-node/src/types/multilang.ts diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index a73cf117..bac13d71 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -665,7 +665,7 @@ export async function getLanguageList( } /** - * 다국어 키 목록 조회 (더미 데이터) + * 다국어 키 목록 조회 */ export async function getLangKeyList( req: AuthenticatedRequest, @@ -677,59 +677,61 @@ export async function getLangKeyList( user: req.user, }); - // 더미 데이터 반환 - const langKeys = [ - { - keyId: 1, - companyCode: "ILSHIN", - menuName: "사용자 관리", - langKey: "user.management.title", - description: "사용자 관리 페이지 제목", - isActive: "Y", - createdDate: new Date().toISOString(), - createdBy: "admin", - updatedDate: new Date().toISOString(), - updatedBy: "admin", - }, - { - keyId: 2, - companyCode: "ILSHIN", - menuName: "메뉴 관리", - langKey: "menu.management.title", - description: "메뉴 관리 페이지 제목", - isActive: "Y", - createdDate: new Date().toISOString(), - createdBy: "admin", - updatedDate: new Date().toISOString(), - updatedBy: "admin", - }, - { - keyId: 3, - companyCode: "HUTECH", - menuName: "대시보드", - langKey: "dashboard.title", - description: "대시보드 페이지 제목", - isActive: "Y", - createdDate: new Date().toISOString(), - createdBy: "admin", - updatedDate: new Date().toISOString(), - updatedBy: "admin", - }, - ]; - - // 프론트엔드에서 기대하는 응답 형식으로 변환 - const response: ApiResponse = { - success: true, - message: "다국어 키 목록 조회 성공", - data: langKeys, - }; - - logger.info("다국어 키 목록 조회 성공", { - totalCount: langKeys.length, - response: response, + // 실제 데이터베이스에서 데이터 조회 + const client = new Client({ + host: process.env.DB_HOST || "localhost", + port: parseInt(process.env.DB_PORT || "5432"), + database: process.env.DB_NAME || "ilshin", + user: process.env.DB_USER || "postgres", + password: process.env.DB_PASSWORD || "postgres", }); - res.status(200).json(response); + await client.connect(); + + try { + const query = ` + SELECT + key_id as "keyId", + company_code as "companyCode", + menu_name as "menuName", + lang_key as "langKey", + description, + is_active as "isActive", + created_date as "createdDate", + created_by as "createdBy", + updated_date as "updatedDate", + updated_by as "updatedBy" + FROM multi_lang_key_master + ORDER BY company_code, menu_name, lang_key + `; + + const result = await client.query(query); + const langKeys = result.rows.map((row) => ({ + ...row, + createdDate: row.createdDate + ? new Date(row.createdDate).toISOString() + : undefined, + updatedDate: row.updatedDate + ? new Date(row.updatedDate).toISOString() + : undefined, + })); + + // 프론트엔드에서 기대하는 응답 형식으로 변환 + const response: ApiResponse = { + success: true, + message: "다국어 키 목록 조회 성공", + data: langKeys, + }; + + logger.info("다국어 키 목록 조회 성공", { + totalCount: langKeys.length, + response: response, + }); + + res.status(200).json(response); + } finally { + await client.end(); + } } catch (error) { logger.error("다국어 키 목록 조회 실패:", error); res.status(500).json({ diff --git a/backend-node/src/controllers/multilangController.ts b/backend-node/src/controllers/multilangController.ts index 65600672..738d486d 100644 --- a/backend-node/src/controllers/multilangController.ts +++ b/backend-node/src/controllers/multilangController.ts @@ -1,20 +1,761 @@ -import { Response } from "express"; -import { AuthenticatedRequest } from "../types/auth"; +import { Request, Response } from "express"; import { logger } from "../utils/logger"; -import prisma from "../config/database"; - -// 메모리 캐시 (개발 환경용, 운영에서는 Redis 사용 권장) -const translationCache = new Map(); -const CACHE_TTL = 5 * 60 * 1000; // 5분 - -interface CacheEntry { - data: any; - timestamp: number; -} +import { AuthenticatedRequest } from "../types/auth"; +import { MultiLangService } from "../services/multilangService"; +import { Client } from "pg"; +import { + CreateLanguageRequest, + UpdateLanguageRequest, + CreateLangKeyRequest, + UpdateLangKeyRequest, + SaveLangTextsRequest, + GetUserTextParams, + BatchTranslationRequest, + ApiResponse, +} from "../types/multilang"; /** - * GET /api/multilang/batch - * 다국어 텍스트 배치 조회 API - 여러 키를 한번에 조회 + * GET /api/multilang/languages + * 언어 목록 조회 API + */ +export const getLanguages = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + logger.info("언어 목록 조회 요청", { user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const languages = await multiLangService.getLanguages(); + + const response: ApiResponse = { + success: true, + message: "언어 목록 조회 성공", + data: languages, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("언어 목록 조회 실패:", error); + res.status(500).json({ + success: false, + message: "언어 목록 조회 중 오류가 발생했습니다.", + error: { + code: "LANGUAGE_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * POST /api/multilang/languages + * 언어 생성 API + */ +export const createLanguage = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const languageData: CreateLanguageRequest = req.body; + logger.info("언어 생성 요청", { languageData, user: req.user }); + + // 필수 입력값 검증 + if ( + !languageData.langCode || + !languageData.langName || + !languageData.langNative + ) { + res.status(400).json({ + success: false, + message: "언어 코드, 언어명, 원어명은 필수입니다.", + error: { + code: "MISSING_REQUIRED_FIELDS", + details: "langCode, langName, langNative are required", + }, + }); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const createdLanguage = await multiLangService.createLanguage({ + ...languageData, + createdBy: req.user?.userId || "system", + updatedBy: req.user?.userId || "system", + }); + + const response: ApiResponse = { + success: true, + message: "언어가 성공적으로 생성되었습니다.", + data: createdLanguage, + }; + + res.status(201).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("언어 생성 실패:", error); + res.status(500).json({ + success: false, + message: "언어 생성 중 오류가 발생했습니다.", + error: { + code: "LANGUAGE_CREATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * PUT /api/multilang/languages/:langCode + * 언어 수정 API + */ +export const updateLanguage = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { langCode } = req.params; + const languageData: UpdateLanguageRequest = req.body; + + logger.info("언어 수정 요청", { langCode, languageData, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const updatedLanguage = await multiLangService.updateLanguage(langCode, { + ...languageData, + updatedBy: req.user?.userId || "system", + }); + + const response: ApiResponse = { + success: true, + message: "언어가 성공적으로 수정되었습니다.", + data: updatedLanguage, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("언어 수정 실패:", error); + res.status(500).json({ + success: false, + message: "언어 수정 중 오류가 발생했습니다.", + error: { + code: "LANGUAGE_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * PUT /api/multilang/languages/:langCode/toggle + * 언어 상태 토글 API + */ +export const toggleLanguage = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { langCode } = req.params; + logger.info("언어 상태 토글 요청", { langCode, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const result = await multiLangService.toggleLanguage(langCode); + + const response: ApiResponse = { + success: true, + message: `언어가 ${result}되었습니다.`, + data: result, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("언어 상태 토글 실패:", error); + res.status(500).json({ + success: false, + message: "언어 상태 변경 중 오류가 발생했습니다.", + error: { + code: "LANGUAGE_TOGGLE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * GET /api/multilang/keys + * 다국어 키 목록 조회 API + */ +export const getLangKeys = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { companyCode, menuCode, keyType, searchText } = req.query; + logger.info("다국어 키 목록 조회 요청", { + query: req.query, + user: req.user, + }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const langKeys = await multiLangService.getLangKeys({ + companyCode: companyCode as string, + menuCode: menuCode as string, + keyType: keyType as string, + searchText: searchText as string, + }); + + const response: ApiResponse = { + success: true, + message: "다국어 키 목록 조회 성공", + data: langKeys, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 키 목록 조회 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 키 목록 조회 중 오류가 발생했습니다.", + error: { + code: "LANG_KEYS_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * GET /api/multilang/keys/:keyId/texts + * 특정 키의 다국어 텍스트 조회 API + */ +export const getLangTexts = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { keyId } = req.params; + logger.info("다국어 텍스트 조회 요청", { keyId, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const langTexts = await multiLangService.getLangTexts(parseInt(keyId)); + + const response: ApiResponse = { + success: true, + message: "다국어 텍스트 조회 성공", + data: langTexts, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 텍스트 조회 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 텍스트 조회 중 오류가 발생했습니다.", + error: { + code: "LANG_TEXTS_LIST_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * POST /api/multilang/keys + * 다국어 키 생성 API + */ +export const createLangKey = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const keyData: CreateLangKeyRequest = req.body; + logger.info("다국어 키 생성 요청", { keyData, user: req.user }); + + // 필수 입력값 검증 + if (!keyData.companyCode || !keyData.langKey) { + res.status(400).json({ + success: false, + message: "회사 코드와 언어 키는 필수입니다.", + error: { + code: "MISSING_REQUIRED_FIELDS", + details: "companyCode and langKey are required", + }, + }); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const keyId = await multiLangService.createLangKey({ + ...keyData, + createdBy: req.user?.userId || "system", + updatedBy: req.user?.userId || "system", + }); + + const response: ApiResponse = { + success: true, + message: "다국어 키가 성공적으로 생성되었습니다.", + data: keyId, + }; + + res.status(201).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 키 생성 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 키 생성 중 오류가 발생했습니다.", + error: { + code: "LANG_KEY_CREATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * PUT /api/multilang/keys/:keyId + * 다국어 키 수정 API + */ +export const updateLangKey = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { keyId } = req.params; + const keyData: UpdateLangKeyRequest = req.body; + + logger.info("다국어 키 수정 요청", { keyId, keyData, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + await multiLangService.updateLangKey(parseInt(keyId), { + ...keyData, + updatedBy: req.user?.userId || "system", + }); + + const response: ApiResponse = { + success: true, + message: "다국어 키가 성공적으로 수정되었습니다.", + data: "수정 완료", + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 키 수정 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 키 수정 중 오류가 발생했습니다.", + error: { + code: "LANG_KEY_UPDATE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * DELETE /api/multilang/keys/:keyId + * 다국어 키 삭제 API + */ +export const deleteLangKey = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { keyId } = req.params; + logger.info("다국어 키 삭제 요청", { keyId, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + await multiLangService.deleteLangKey(parseInt(keyId)); + + const response: ApiResponse = { + success: true, + message: "다국어 키가 성공적으로 삭제되었습니다.", + data: "삭제 완료", + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 키 삭제 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 키 삭제 중 오류가 발생했습니다.", + error: { + code: "LANG_KEY_DELETE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * PUT /api/multilang/keys/:keyId/toggle + * 다국어 키 상태 토글 API + */ +export const toggleLangKey = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { keyId } = req.params; + logger.info("다국어 키 상태 토글 요청", { keyId, user: req.user }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const result = await multiLangService.toggleLangKey(parseInt(keyId)); + + const response: ApiResponse = { + success: true, + message: `다국어 키가 ${result}되었습니다.`, + data: result, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 키 상태 토글 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 키 상태 변경 중 오류가 발생했습니다.", + error: { + code: "LANG_KEY_TOGGLE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * POST /api/multilang/keys/:keyId/texts + * 다국어 텍스트 저장/수정 API + */ +export const saveLangTexts = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { keyId } = req.params; + const textData: SaveLangTextsRequest = req.body; + + logger.info("다국어 텍스트 저장 요청", { keyId, textData, user: req.user }); + + // 필수 입력값 검증 + if ( + !textData.texts || + !Array.isArray(textData.texts) || + textData.texts.length === 0 + ) { + res.status(400).json({ + success: false, + message: "텍스트 데이터는 필수입니다.", + error: { + code: "MISSING_REQUIRED_FIELDS", + details: "texts array is required", + }, + }); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + await multiLangService.saveLangTexts(parseInt(keyId), { + texts: textData.texts.map((text) => ({ + ...text, + createdBy: req.user?.userId || "system", + updatedBy: req.user?.userId || "system", + })), + }); + + const response: ApiResponse = { + success: true, + message: "다국어 텍스트가 성공적으로 저장되었습니다.", + data: "저장 완료", + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("다국어 텍스트 저장 실패:", error); + res.status(500).json({ + success: false, + message: "다국어 텍스트 저장 중 오류가 발생했습니다.", + error: { + code: "LANG_TEXTS_SAVE_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * GET /api/multilang/user-text/:companyCode/:menuCode/:langKey + * 사용자별 다국어 텍스트 조회 API + */ +export const getUserText = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { companyCode, menuCode, langKey } = req.params; + const { userLang } = req.query; + + logger.info("사용자별 다국어 텍스트 조회 요청", { + companyCode, + menuCode, + langKey, + userLang, + user: req.user, + }); + + if (!userLang) { + res.status(400).json({ + success: false, + message: "사용자 언어는 필수입니다.", + error: { + code: "MISSING_USER_LANG", + details: "userLang query parameter is required", + }, + }); + return; + } + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const langText = await multiLangService.getUserText({ + companyCode, + menuCode, + langKey, + userLang: userLang as string, + }); + + const response: ApiResponse = { + success: true, + message: "사용자별 다국어 텍스트 조회 성공", + data: langText, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("사용자별 다국어 텍스트 조회 실패:", error); + res.status(500).json({ + success: false, + message: "사용자별 다국어 텍스트 조회 중 오류가 발생했습니다.", + error: { + code: "USER_TEXT_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * GET /api/multilang/text/:companyCode/:langKey/:langCode + * 특정 키의 다국어 텍스트 조회 API + */ +export const getLangText = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { companyCode, langKey, langCode } = req.params; + + logger.info("특정 키의 다국어 텍스트 조회 요청", { + companyCode, + langKey, + langCode, + user: req.user, + }); + + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", + }); + + await client.connect(); + + try { + const multiLangService = new MultiLangService(client); + const langText = await multiLangService.getLangText( + companyCode, + langKey, + langCode + ); + + const response: ApiResponse = { + success: true, + message: "특정 키의 다국어 텍스트 조회 성공", + data: langText, + }; + + res.status(200).json(response); + } finally { + await client.end(); + } + } catch (error) { + logger.error("특정 키의 다국어 텍스트 조회 실패:", error); + res.status(500).json({ + success: false, + message: "특정 키의 다국어 텍스트 조회 중 오류가 발생했습니다.", + error: { + code: "LANG_TEXT_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }); + } +}; + +/** + * POST /api/multilang/batch + * 다국어 텍스트 배치 조회 API */ export const getBatchTranslations = async ( req: AuthenticatedRequest, @@ -22,7 +763,7 @@ export const getBatchTranslations = async ( ): Promise => { try { const { companyCode, menuCode, userLang } = req.query; - const { langKeys } = req.body; // 배열로 여러 키 전달 + const { langKeys } = req.body; logger.info("다국어 텍스트 배치 조회 요청", { companyCode, @@ -36,290 +777,63 @@ export const getBatchTranslations = async ( res.status(400).json({ success: false, message: "langKeys 배열이 필요합니다.", + error: { + code: "MISSING_LANG_KEYS", + details: "langKeys array is required", + }, }); 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, + if (!companyCode || !userLang) { + res.status(400).json({ + success: false, + message: "companyCode와 userLang은 필수입니다.", + error: { + code: "MISSING_REQUIRED_PARAMS", + details: "companyCode and userLang are required", + }, }); return; } - // 1. 모든 키에 대한 마스터 정보를 한번에 조회 - logger.info("다국어 키 마스터 배치 조회 시작", { - keyCount: langKeys.length, + // PostgreSQL 클라이언트 생성 + const client = new Client({ + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/ilshin", }); - const langKeyMasters = await prisma.$queryRaw` - 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 - `; + await client.connect(); - logger.info("다국어 키 마스터 배치 조회 결과", { - requestedKeys: langKeys.length, - foundKeys: langKeyMasters.length, - }); - - if (langKeyMasters.length === 0) { - // 마스터 데이터가 없으면 기본값 반환 - const defaultTranslations = getDefaultTranslations( + try { + const multiLangService = new MultiLangService(client); + const translations = await multiLangService.getBatchTranslations({ + companyCode: companyCode as string, + menuCode: menuCode as string, + userLang: userLang as string, langKeys, - userLang as string - ); - - // 캐시에 저장 - translationCache.set(cacheKey, { - data: defaultTranslations, - timestamp: Date.now(), }); - res.status(200).json({ + const response: ApiResponse> = { success: true, - data: defaultTranslations, - message: "기본값으로 다국어 텍스트 조회 성공", - fromCache: false, - }); - return; + message: "다국어 텍스트 배치 조회 성공", + data: translations, + }; + + res.status(200).json(response); + } finally { + await client.end(); } - - // 2. 모든 key_id를 추출 - const keyIds = langKeyMasters.map((master) => master.key_id); - - // 3. 요청된 언어와 한국어 번역을 한번에 조회 - const translations = await prisma.$queryRaw` - 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 = {}; - - 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 }); + 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 (하위 호환성 유지) - */ -export const getUserText = async (req: AuthenticatedRequest, res: Response) => { - try { - const { companyCode, menuCode, langKey } = req.params; - const { userLang } = req.query; - - logger.info("단일 다국어 텍스트 조회 요청", { - companyCode, - menuCode, - langKey, - userLang, - user: req.user, - }); - - // 배치 API를 사용하여 단일 키 조회 - const batchResult = await getBatchTranslations( - { - ...req, - body: { langKeys: [langKey] }, - query: { companyCode, menuCode, userLang }, - } as any, - res - ); - - // 배치 API에서 이미 응답을 보냈으므로 여기서는 아무것도 하지 않음 - return; - } catch (error) { - logger.error("단일 다국어 텍스트 조회 실패", { error }); - res.status(500).json({ - success: false, - message: "다국어 텍스트 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "Unknown error", - }); - } -}; - -/** - * 기본 번역 텍스트 반환 (개별 키) - */ -function getDefaultTranslation(langKey: string, userLang: string): string { - const defaultKoreanTexts: Record = { - "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 { - const result: Record = {}; - - 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", + error: { + code: "BATCH_TRANSLATION_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, }); } }; diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index fc098728..1d40c7b9 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -19,17 +19,6 @@ import { deleteCompany, // 회사 삭제 getUserLocale, setUserLocale, - getLanguageList, - getLangKeyList, - getLangTextList, - saveLangTexts, - saveLangKey, - updateLangKey, - deleteLangKey, - toggleLangKeyStatus, - saveLanguage, - updateLanguage, - toggleLanguageStatus, } from "../controllers/adminController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -67,17 +56,4 @@ router.delete("/companies/:companyCode", deleteCompany); // 회사 삭제 router.get("/user-locale", getUserLocale); router.post("/user-locale", setUserLocale); -// 다국어 관리 API -router.get("/multilang/languages", getLanguageList); -router.get("/multilang/keys", getLangKeyList); -router.get("/multilang/keys/:keyId/texts", getLangTextList); -router.post("/multilang/keys/:keyId/texts", saveLangTexts); -router.post("/multilang/keys", saveLangKey); -router.put("/multilang/keys/:keyId", updateLangKey); -router.delete("/multilang/keys/:keyId", deleteLangKey); -router.put("/multilang/keys/:keyId/toggle", toggleLangKeyStatus); -router.post("/multilang/languages", saveLanguage); -router.put("/multilang/languages/:langCode", updateLanguage); -router.put("/multilang/languages/:langCode/toggle", toggleLanguageStatus); - export default router; diff --git a/backend-node/src/routes/multilangRoutes.ts b/backend-node/src/routes/multilangRoutes.ts index 30782c10..cf07b5bd 100644 --- a/backend-node/src/routes/multilangRoutes.ts +++ b/backend-node/src/routes/multilangRoutes.ts @@ -1,23 +1,50 @@ -import { Router } from "express"; -import { - getUserText, - getBatchTranslations, - clearCache, -} from "../controllers/multilangController"; +import express from "express"; import { authenticateToken } from "../middleware/authMiddleware"; +import { + // 언어 관리 API + getLanguages, + createLanguage, + updateLanguage, + toggleLanguage, -const router = Router(); + // 다국어 키 관리 API + getLangKeys, + getLangTexts, + createLangKey, + updateLangKey, + deleteLangKey, + toggleLangKey, -// 모든 multilang 라우트에 인증 미들웨어 적용 + // 다국어 텍스트 관리 API + saveLangTexts, + getUserText, + getLangText, + getBatchTranslations, +} from "../controllers/multilangController"; + +const router = express.Router(); + +// 모든 다국어 관리 라우트에 인증 미들웨어 적용 router.use(authenticateToken); -// 다국어 텍스트 API -router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText); +// 언어 관리 API +router.get("/languages", getLanguages); // 언어 목록 조회 +router.post("/languages", createLanguage); // 언어 생성 +router.put("/languages/:langCode", updateLanguage); // 언어 수정 +router.put("/languages/:langCode/toggle", toggleLanguage); // 언어 상태 토글 -// 다국어 텍스트 배치 조회 API (새로운 방식) -router.post("/batch", getBatchTranslations); +// 다국어 키 관리 API +router.get("/keys", getLangKeys); // 다국어 키 목록 조회 +router.get("/keys/:keyId/texts", getLangTexts); // 특정 키의 다국어 텍스트 조회 +router.post("/keys", createLangKey); // 다국어 키 생성 +router.put("/keys/:keyId", updateLangKey); // 다국어 키 수정 +router.delete("/keys/:keyId", deleteLangKey); // 다국어 키 삭제 +router.put("/keys/:keyId/toggle", toggleLangKey); // 다국어 키 상태 토글 -// 캐시 초기화 API (개발/테스트용) -router.delete("/cache", clearCache); +// 다국어 텍스트 관리 API +router.post("/keys/:keyId/texts", saveLangTexts); // 다국어 텍스트 저장/수정 +router.get("/user-text/:companyCode/:menuCode/:langKey", getUserText); // 사용자별 다국어 텍스트 조회 +router.get("/text/:companyCode/:langKey/:langCode", getLangText); // 특정 키의 다국어 텍스트 조회 +router.post("/batch", getBatchTranslations); // 다국어 텍스트 배치 조회 export default router; diff --git a/backend-node/src/services/multilangService.ts b/backend-node/src/services/multilangService.ts new file mode 100644 index 00000000..480b9c60 --- /dev/null +++ b/backend-node/src/services/multilangService.ts @@ -0,0 +1,855 @@ +import { Client } from "pg"; +import { logger } from "../utils/logger"; +import { + Language, + LangKey, + LangText, + CreateLanguageRequest, + UpdateLanguageRequest, + CreateLangKeyRequest, + UpdateLangKeyRequest, + SaveLangTextsRequest, + GetLangKeysParams, + GetUserTextParams, + BatchTranslationRequest, + ApiResponse, +} from "../types/multilang"; + +export class MultiLangService { + private client: Client; + + constructor(client: Client) { + this.client = client; + } + + /** + * 언어 목록 조회 + */ + async getLanguages(): Promise { + try { + logger.info("언어 목록 조회 시작"); + + const query = ` + SELECT + lang_code as "langCode", + lang_name as "langName", + lang_native as "langNative", + is_active as "isActive", + sort_order as "sortOrder", + created_date as "createdDate", + created_by as "createdBy", + updated_date as "updatedDate", + updated_by as "updatedBy" + FROM language_master + ORDER BY sort_order, lang_code + `; + + const result = await this.client.query(query); + const languages = result.rows.map((row) => ({ + ...row, + createdDate: row.createdDate ? new Date(row.createdDate) : undefined, + updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined, + })); + + logger.info(`언어 목록 조회 완료: ${languages.length}개`); + return languages; + } catch (error) { + logger.error("언어 목록 조회 중 오류 발생:", error); + throw new Error( + `언어 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 언어 생성 + */ + async createLanguage(languageData: CreateLanguageRequest): Promise { + try { + logger.info("언어 생성 시작", { languageData }); + + // 중복 체크 + const duplicateCheckQuery = ` + SELECT lang_code FROM language_master WHERE lang_code = $1 + `; + const duplicateResult = await this.client.query(duplicateCheckQuery, [ + languageData.langCode, + ]); + + if (duplicateResult.rows.length > 0) { + throw new Error( + `이미 존재하는 언어 코드입니다: ${languageData.langCode}` + ); + } + + // 언어 생성 + const insertQuery = ` + INSERT INTO language_master ( + lang_code, lang_name, lang_native, is_active, sort_order, + created_date, created_by, updated_date, updated_by + ) VALUES ($1, $2, $3, $4, $5, now(), $6, now(), $7) + RETURNING * + `; + + const insertValues = [ + languageData.langCode, + languageData.langName, + languageData.langNative, + languageData.isActive || "Y", + languageData.sortOrder || 0, + languageData.createdBy || "system", + languageData.updatedBy || "system", + ]; + + const result = await this.client.query(insertQuery, insertValues); + const createdLanguage = result.rows[0]; + + logger.info("언어 생성 완료", { langCode: createdLanguage.lang_code }); + + return { + langCode: createdLanguage.lang_code, + langName: createdLanguage.lang_name, + langNative: createdLanguage.lang_native, + isActive: createdLanguage.is_active, + sortOrder: createdLanguage.sort_order, + createdDate: createdLanguage.created_date + ? new Date(createdLanguage.created_date) + : undefined, + createdBy: createdLanguage.created_by, + updatedDate: createdLanguage.updated_date + ? new Date(createdLanguage.updated_date) + : undefined, + updatedBy: createdLanguage.updated_by, + }; + } catch (error) { + logger.error("언어 생성 중 오류 발생:", error); + throw new Error( + `언어 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 언어 수정 + */ + async updateLanguage( + langCode: string, + languageData: UpdateLanguageRequest + ): Promise { + try { + logger.info("언어 수정 시작", { langCode, languageData }); + + // 기존 언어 확인 + const checkQuery = ` + SELECT * FROM language_master WHERE lang_code = $1 + `; + const checkResult = await this.client.query(checkQuery, [langCode]); + + if (checkResult.rows.length === 0) { + throw new Error(`언어를 찾을 수 없습니다: ${langCode}`); + } + + // 언어 수정 + const updateQuery = ` + UPDATE language_master + SET + lang_name = COALESCE($2, lang_name), + lang_native = COALESCE($3, lang_native), + is_active = COALESCE($4, is_active), + sort_order = COALESCE($5, sort_order), + updated_date = now(), + updated_by = $6 + WHERE lang_code = $1 + RETURNING * + `; + + const updateValues = [ + langCode, + languageData.langName, + languageData.langNative, + languageData.isActive, + languageData.sortOrder, + languageData.updatedBy || "system", + ]; + + const result = await this.client.query(updateQuery, updateValues); + const updatedLanguage = result.rows[0]; + + logger.info("언어 수정 완료", { langCode }); + + return { + langCode: updatedLanguage.lang_code, + langName: updatedLanguage.lang_name, + langNative: updatedLanguage.lang_native, + isActive: updatedLanguage.is_active, + sortOrder: updatedLanguage.sort_order, + createdDate: updatedLanguage.created_date + ? new Date(updatedLanguage.created_date) + : undefined, + createdBy: updatedLanguage.created_by, + updatedDate: updatedLanguage.updated_date + ? new Date(updatedLanguage.updated_date) + : undefined, + updatedBy: updatedLanguage.updated_by, + }; + } catch (error) { + logger.error("언어 수정 중 오류 발생:", error); + throw new Error( + `언어 수정 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 언어 상태 토글 + */ + async toggleLanguage(langCode: string): Promise { + try { + logger.info("언어 상태 토글 시작", { langCode }); + + // 현재 상태 조회 + const currentQuery = ` + SELECT is_active FROM language_master WHERE lang_code = $1 + `; + const currentResult = await this.client.query(currentQuery, [langCode]); + + if (currentResult.rows.length === 0) { + throw new Error(`언어를 찾을 수 없습니다: ${langCode}`); + } + + const currentStatus = currentResult.rows[0].is_active; + const newStatus = currentStatus === "Y" ? "N" : "Y"; + + // 상태 업데이트 + const updateQuery = ` + UPDATE language_master + SET is_active = $2, updated_date = now(), updated_by = 'system' + WHERE lang_code = $1 + `; + + await this.client.query(updateQuery, [langCode, newStatus]); + + const result = newStatus === "Y" ? "활성화" : "비활성화"; + logger.info("언어 상태 토글 완료", { langCode, result }); + + return result; + } catch (error) { + logger.error("언어 상태 토글 중 오류 발생:", error); + throw new Error( + `언어 상태 토글 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 키 목록 조회 + */ + async getLangKeys(params: GetLangKeysParams): Promise { + try { + logger.info("다국어 키 목록 조회 시작", { params }); + + let query = ` + SELECT + key_id as "keyId", + company_code as "companyCode", + menu_name as "menuName", + lang_key as "langKey", + description, + is_active as "isActive", + created_date as "createdDate", + created_by as "createdBy", + updated_date as "updatedDate", + updated_by as "updatedBy" + FROM multi_lang_key_master + WHERE 1=1 + `; + + const queryParams: any[] = []; + let paramIndex = 1; + + // 회사 코드 필터 + if (params.companyCode) { + query += ` AND company_code = $${paramIndex}`; + queryParams.push(params.companyCode); + paramIndex++; + } + + // 메뉴 코드 필터 + if (params.menuCode) { + query += ` AND menu_name = $${paramIndex}`; + queryParams.push(params.menuCode); + paramIndex++; + } + + // 검색 조건 + if (params.searchText) { + query += ` AND ( + lang_key ILIKE $${paramIndex} OR + description ILIKE $${paramIndex} OR + menu_name ILIKE $${paramIndex} + )`; + queryParams.push(`%${params.searchText}%`); + paramIndex++; + } + + // 정렬 + query += ` ORDER BY company_code, menu_name, lang_key`; + + const result = await this.client.query(query, queryParams); + const langKeys = result.rows.map((row) => ({ + ...row, + createdDate: row.createdDate ? new Date(row.createdDate) : undefined, + updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined, + })); + + logger.info(`다국어 키 목록 조회 완료: ${langKeys.length}개`); + return langKeys; + } catch (error) { + logger.error("다국어 키 목록 조회 중 오류 발생:", error); + throw new Error( + `다국어 키 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 특정 키의 다국어 텍스트 조회 + */ + async getLangTexts(keyId: number): Promise { + try { + logger.info("다국어 텍스트 조회 시작", { keyId }); + + const query = ` + SELECT + text_id as "textId", + key_id as "keyId", + lang_code as "langCode", + lang_text as "langText", + is_active as "isActive", + created_date as "createdDate", + created_by as "createdBy", + updated_date as "updatedDate", + updated_by as "updatedBy" + FROM multi_lang_text + WHERE key_id = $1 AND is_active = 'Y' + ORDER BY lang_code + `; + + const result = await this.client.query(query, [keyId]); + const langTexts = result.rows.map((row) => ({ + ...row, + createdDate: row.createdDate ? new Date(row.createdDate) : undefined, + updatedDate: row.updatedDate ? new Date(row.updatedDate) : undefined, + })); + + logger.info(`다국어 텍스트 조회 완료: ${langTexts.length}개`); + return langTexts; + } catch (error) { + logger.error("다국어 텍스트 조회 중 오류 발생:", error); + throw new Error( + `다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 키 생성 + */ + async createLangKey(keyData: CreateLangKeyRequest): Promise { + try { + logger.info("다국어 키 생성 시작", { keyData }); + + // 중복 체크 + const duplicateCheckQuery = ` + SELECT key_id FROM multi_lang_key_master + WHERE company_code = $1 AND lang_key = $2 + `; + const duplicateResult = await this.client.query(duplicateCheckQuery, [ + keyData.companyCode, + keyData.langKey, + ]); + + if (duplicateResult.rows.length > 0) { + throw new Error( + `동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}` + ); + } + + // 다국어 키 생성 + const insertQuery = ` + INSERT INTO multi_lang_key_master ( + company_code, menu_name, lang_key, description, is_active, + created_date, created_by, updated_date, updated_by + ) VALUES ($1, $2, $3, $4, $5, now(), $6, now(), $7) + RETURNING key_id + `; + + const insertValues = [ + keyData.companyCode, + keyData.menuName || null, + keyData.langKey, + keyData.description || null, + keyData.isActive || "Y", + keyData.createdBy || "system", + keyData.updatedBy || "system", + ]; + + const result = await this.client.query(insertQuery, insertValues); + const keyId = result.rows[0].key_id; + + logger.info("다국어 키 생성 완료", { keyId, langKey: keyData.langKey }); + + return keyId; + } catch (error) { + logger.error("다국어 키 생성 중 오류 발생:", error); + throw new Error( + `다국어 키 생성 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 키 수정 + */ + async updateLangKey( + keyId: number, + keyData: UpdateLangKeyRequest + ): Promise { + try { + logger.info("다국어 키 수정 시작", { keyId, keyData }); + + // 기존 키 확인 + const checkQuery = ` + SELECT key_id FROM multi_lang_key_master WHERE key_id = $1 + `; + const checkResult = await this.client.query(checkQuery, [keyId]); + + if (checkResult.rows.length === 0) { + throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); + } + + // 중복 체크 (자신을 제외하고) + if (keyData.companyCode && keyData.langKey) { + const duplicateCheckQuery = ` + SELECT key_id FROM multi_lang_key_master + WHERE company_code = $1 AND lang_key = $2 AND key_id != $3 + `; + const duplicateResult = await this.client.query(duplicateCheckQuery, [ + keyData.companyCode, + keyData.langKey, + keyId, + ]); + + if (duplicateResult.rows.length > 0) { + throw new Error( + `동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}` + ); + } + } + + // 다국어 키 수정 + const updateQuery = ` + UPDATE multi_lang_key_master + SET + company_code = COALESCE($2, company_code), + menu_name = COALESCE($3, menu_name), + lang_key = COALESCE($4, lang_key), + description = COALESCE($5, description), + updated_date = now(), + updated_by = $6 + WHERE key_id = $1 + `; + + const updateValues = [ + keyId, + keyData.companyCode, + keyData.menuName, + keyData.langKey, + keyData.description, + keyData.updatedBy || "system", + ]; + + await this.client.query(updateQuery, updateValues); + + logger.info("다국어 키 수정 완료", { keyId }); + } catch (error) { + logger.error("다국어 키 수정 중 오류 발생:", error); + throw new Error( + `다국어 키 수정 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 키 삭제 + */ + async deleteLangKey(keyId: number): Promise { + try { + logger.info("다국어 키 삭제 시작", { keyId }); + + // 기존 키 확인 + const checkQuery = ` + SELECT key_id FROM multi_lang_key_master WHERE key_id = $1 + `; + const checkResult = await this.client.query(checkQuery, [keyId]); + + if (checkResult.rows.length === 0) { + throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); + } + + // 트랜잭션 시작 + await this.client.query("BEGIN"); + + try { + // 관련된 다국어 텍스트 삭제 + const deleteTextsQuery = ` + DELETE FROM multi_lang_text WHERE key_id = $1 + `; + await this.client.query(deleteTextsQuery, [keyId]); + + // 다국어 키 삭제 + const deleteKeyQuery = ` + DELETE FROM multi_lang_key_master WHERE key_id = $1 + `; + await this.client.query(deleteKeyQuery, [keyId]); + + // 트랜잭션 커밋 + await this.client.query("COMMIT"); + + logger.info("다국어 키 삭제 완료", { keyId }); + } catch (error) { + // 트랜잭션 롤백 + await this.client.query("ROLLBACK"); + throw error; + } + } catch (error) { + logger.error("다국어 키 삭제 중 오류 발생:", error); + throw new Error( + `다국어 키 삭제 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 키 상태 토글 + */ + async toggleLangKey(keyId: number): Promise { + try { + logger.info("다국어 키 상태 토글 시작", { keyId }); + + // 현재 상태 조회 + const currentQuery = ` + SELECT is_active FROM multi_lang_key_master WHERE key_id = $1 + `; + const currentResult = await this.client.query(currentQuery, [keyId]); + + if (currentResult.rows.length === 0) { + throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); + } + + const currentStatus = currentResult.rows[0].is_active; + const newStatus = currentStatus === "Y" ? "N" : "Y"; + + // 상태 업데이트 + const updateQuery = ` + UPDATE multi_lang_key_master + SET is_active = $2, updated_date = now(), updated_by = 'system' + WHERE key_id = $1 + `; + + await this.client.query(updateQuery, [keyId, newStatus]); + + const result = newStatus === "Y" ? "활성화" : "비활성화"; + logger.info("다국어 키 상태 토글 완료", { keyId, result }); + + return result; + } catch (error) { + logger.error("다국어 키 상태 토글 중 오류 발생:", error); + throw new Error( + `다국어 키 상태 토글 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 다국어 텍스트 저장/수정 + */ + async saveLangTexts( + keyId: number, + textData: SaveLangTextsRequest + ): Promise { + try { + logger.info("다국어 텍스트 저장 시작", { + keyId, + textCount: textData.texts.length, + }); + + // 기존 키 확인 + const checkQuery = ` + SELECT key_id FROM multi_lang_key_master WHERE key_id = $1 + `; + const checkResult = await this.client.query(checkQuery, [keyId]); + + if (checkResult.rows.length === 0) { + throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); + } + + // 트랜잭션 시작 + await this.client.query("BEGIN"); + + try { + // 기존 텍스트 삭제 + const deleteTextsQuery = ` + DELETE FROM multi_lang_text WHERE key_id = $1 + `; + await this.client.query(deleteTextsQuery, [keyId]); + + // 새로운 텍스트 삽입 + for (const text of textData.texts) { + const insertTextQuery = ` + INSERT INTO multi_lang_text ( + key_id, lang_code, lang_text, is_active, + created_date, created_by, updated_date, updated_by + ) VALUES ($1, $2, $3, $4, now(), $5, now(), $6) + `; + + const insertValues = [ + keyId, + text.langCode, + text.langText, + text.isActive || "Y", + text.createdBy || "system", + text.updatedBy || "system", + ]; + + await this.client.query(insertTextQuery, insertValues); + } + + // 트랜잭션 커밋 + await this.client.query("COMMIT"); + + logger.info("다국어 텍스트 저장 완료", { + keyId, + savedCount: textData.texts.length, + }); + } catch (error) { + // 트랜잭션 롤백 + await this.client.query("ROLLBACK"); + throw error; + } + } catch (error) { + logger.error("다국어 텍스트 저장 중 오류 발생:", error); + throw new Error( + `다국어 텍스트 저장 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 사용자별 다국어 텍스트 조회 + */ + async getUserText(params: GetUserTextParams): Promise { + try { + logger.info("사용자별 다국어 텍스트 조회 시작", { params }); + + const query = ` + SELECT t.lang_text as "langText" + FROM multi_lang_key_master km + JOIN multi_lang_text t ON km.key_id = t.key_id + WHERE km.company_code = $1 + AND km.menu_name = $2 + AND km.lang_key = $3 + AND t.lang_code = $4 + AND km.is_active = 'Y' + AND t.is_active = 'Y' + LIMIT 1 + `; + + const result = await this.client.query(query, [ + params.companyCode, + params.menuCode, + params.langKey, + params.userLang, + ]); + + if (result.rows.length === 0) { + logger.warn("사용자별 다국어 텍스트를 찾을 수 없음", { params }); + return params.langKey; // 기본값으로 키 반환 + } + + const langText = result.rows[0].langText; + logger.info("사용자별 다국어 텍스트 조회 완료", { params, langText }); + + return langText; + } catch (error) { + logger.error("사용자별 다국어 텍스트 조회 중 오류 발생:", error); + throw new Error( + `사용자별 다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 특정 키의 다국어 텍스트 조회 + */ + async getLangText( + companyCode: string, + langKey: string, + langCode: string + ): Promise { + try { + logger.info("특정 키의 다국어 텍스트 조회 시작", { + companyCode, + langKey, + langCode, + }); + + const query = ` + SELECT t.lang_text as "langText" + FROM multi_lang_text t + JOIN multi_lang_key_master k ON t.key_id = k.key_id + WHERE k.company_code = $1 + AND k.lang_key = $2 + AND t.lang_code = $3 + AND t.is_active = 'Y' + AND k.is_active = 'Y' + `; + + const result = await this.client.query(query, [ + companyCode, + langKey, + langCode, + ]); + + if (result.rows.length === 0) { + logger.warn("특정 키의 다국어 텍스트를 찾을 수 없음", { + companyCode, + langKey, + langCode, + }); + return langKey; // 기본값으로 키 반환 + } + + const langText = result.rows[0].langText; + logger.info("특정 키의 다국어 텍스트 조회 완료", { + companyCode, + langKey, + langCode, + langText, + }); + + return langText; + } catch (error) { + logger.error("특정 키의 다국어 텍스트 조회 중 오류 발생:", error); + throw new Error( + `특정 키의 다국어 텍스트 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 배치 번역 조회 + */ + async getBatchTranslations( + params: BatchTranslationRequest + ): Promise> { + try { + logger.info("배치 번역 조회 시작", { + companyCode: params.companyCode, + menuCode: params.menuCode, + userLang: params.userLang, + keyCount: params.langKeys.length, + }); + + if (params.langKeys.length === 0) { + return {}; + } + + // 모든 키에 대한 마스터 정보를 한번에 조회 + const langKeyMastersQuery = ` + SELECT key_id, lang_key, company_code + FROM multi_lang_key_master + WHERE lang_key = ANY($1::varchar[]) + AND (company_code = $2::varchar OR company_code = '*') + ORDER BY + CASE WHEN company_code = $2::varchar THEN 1 ELSE 2 END, + lang_key, + company_code + `; + + const langKeyMasters = await this.client.query(langKeyMastersQuery, [ + params.langKeys, + params.companyCode, + ]); + + if (langKeyMasters.rows.length === 0) { + logger.warn("배치 번역: 언어키 마스터를 찾을 수 없음", { params }); + return this.createDefaultTranslations(params.langKeys); + } + + // 찾은 키들의 ID 목록 + const keyIds = langKeyMasters.rows.map((row) => row.key_id); + const foundKeys = langKeyMasters.rows.map((row) => row.lang_key); + + // 누락된 키들 (기본값으로 설정) + const missingKeys = params.langKeys.filter( + (key) => !foundKeys.includes(key) + ); + const result: Record = {}; + + // 기본값으로 누락된 키들 설정 + missingKeys.forEach((key) => { + result[key] = key; + }); + + // 실제 번역 텍스트 조회 + if (keyIds.length > 0) { + const textsQuery = ` + SELECT t.key_id, t.lang_text, km.lang_key + FROM multi_lang_text t + JOIN multi_lang_key_master km ON t.key_id = km.key_id + WHERE t.key_id = ANY($1::int[]) + AND t.lang_code = $2 + AND t.is_active = 'Y' + `; + + const texts = await this.client.query(textsQuery, [ + keyIds, + params.userLang, + ]); + + // 결과 매핑 + texts.rows.forEach((row) => { + result[row.lang_key] = row.lang_text; + }); + } + + logger.info("배치 번역 조회 완료", { + totalKeys: params.langKeys.length, + foundKeys: foundKeys.length, + missingKeys: missingKeys.length, + resultKeys: Object.keys(result).length, + }); + + return result; + } catch (error) { + logger.error("배치 번역 조회 중 오류 발생:", error); + throw new Error( + `배치 번역 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + } + + /** + * 기본 번역 생성 (키를 그대로 반환) + */ + private createDefaultTranslations( + langKeys: string[] + ): Record { + const result: Record = {}; + langKeys.forEach((key) => { + result[key] = key; + }); + return result; + } +} diff --git a/backend-node/src/types/multilang.ts b/backend-node/src/types/multilang.ts new file mode 100644 index 00000000..8ad8adb6 --- /dev/null +++ b/backend-node/src/types/multilang.ts @@ -0,0 +1,130 @@ +export interface Language { + langCode: string; + langName: string; + langNative: string; + isActive: string; + sortOrder?: number; + createdDate?: Date; + createdBy?: string; + updatedDate?: Date; + updatedBy?: string; +} + +export interface LangKey { + keyId?: number; + companyCode: string; + menuName?: string; + langKey: string; + description?: string; + isActive: string; + createdDate?: Date; + createdBy?: string; + updatedDate?: Date; + updatedBy?: string; +} + +export interface LangText { + textId?: number; + keyId: number; + langCode: string; + langText: string; + isActive: string; + createdDate?: Date; + createdBy?: string; + updatedDate?: Date; + updatedBy?: string; +} + +export interface LangKeyWithTexts extends LangKey { + texts: LangText[]; +} + +export interface CreateLanguageRequest { + langCode: string; + langName: string; + langNative: string; + isActive?: string; + sortOrder?: number; + createdBy?: string; + updatedBy?: string; +} + +export interface UpdateLanguageRequest { + langName?: string; + langNative?: string; + isActive?: string; + sortOrder?: number; + updatedBy?: string; +} + +export interface CreateLangKeyRequest { + companyCode: string; + menuName?: string; + langKey: string; + description?: string; + isActive?: string; + createdBy?: string; + updatedBy?: string; +} + +export interface UpdateLangKeyRequest { + companyCode?: string; + menuName?: string; + langKey?: string; + description?: string; + updatedBy?: string; +} + +export interface SaveLangTextsRequest { + texts: Array<{ + langCode: string; + langText: string; + isActive?: string; + createdBy?: string; + updatedBy?: string; + }>; +} + +export interface GetLangKeysParams { + companyCode?: string; + menuCode?: string; + keyType?: string; + searchText?: string; + page?: number; + limit?: number; +} + +export interface GetUserTextParams { + companyCode: string; + menuCode: string; + langKey: string; + userLang: string; +} + +export interface BatchTranslationRequest { + companyCode: string; + menuCode?: string; + userLang: string; + langKeys: string[]; +} + +export interface TranslationCacheEntry { + data: Record; + timestamp: number; +} + +export interface ApiResponse { + success: boolean; + message?: string; + data?: T; + error?: { + code: string; + details?: any; + }; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} diff --git a/frontend/components/admin/MultiLang.tsx b/frontend/components/admin/MultiLang.tsx index aefe6a37..db587e6a 100644 --- a/frontend/components/admin/MultiLang.tsx +++ b/frontend/components/admin/MultiLang.tsx @@ -65,7 +65,7 @@ export default function MultiLangPage() { const fetchCompanies = async () => { try { console.log("회사 목록 조회 시작"); - const response = await apiClient.get("/api/admin/companies"); + const response = await apiClient.get("/admin/companies"); console.log("회사 목록 응답 데이터:", response.data); const data = response.data; @@ -87,7 +87,7 @@ export default function MultiLangPage() { // 언어 목록 조회 const fetchLanguages = async () => { try { - const response = await apiClient.get("/api/admin/multilang/languages"); + const response = await apiClient.get("/multilang/languages"); const data = response.data; if (data.success) { setLanguages(data.data); @@ -100,7 +100,7 @@ export default function MultiLangPage() { // 다국어 키 목록 조회 const fetchLangKeys = async () => { try { - const response = await apiClient.get("/api/admin/multilang/keys"); + const response = await apiClient.get("/multilang/keys"); const data = response.data; if (data.success) { console.log("✅ 전체 키 목록 로드:", data.data.length, "개"); @@ -147,7 +147,7 @@ export default function MultiLangPage() { const fetchLangTexts = async (keyId: number) => { try { console.log("다국어 텍스트 조회 시작: keyId =", keyId); - const response = await apiClient.get(`/api/admin/multilang/keys/${keyId}/texts`); + const response = await apiClient.get(`/multilang/keys/${keyId}/texts`); const data = response.data; console.log("다국어 텍스트 조회 응답:", data); if (data.success) { @@ -203,7 +203,18 @@ export default function MultiLangPage() { if (!selectedKey) return; try { - const response = await apiClient.post(`/api/admin/multilang/keys/${selectedKey.keyId}/texts`, editingTexts); + // 백엔드가 기대하는 형식으로 데이터 변환 + const requestData = { + texts: editingTexts.map((text) => ({ + langCode: text.langCode, + langText: text.langText, + isActive: text.isActive || "Y", + createdBy: user?.userId || "system", + updatedBy: user?.userId || "system", + })), + }; + + const response = await apiClient.post(`/multilang/keys/${selectedKey.keyId}/texts`, requestData); const data = response.data; if (data.success) { alert("저장되었습니다."); @@ -245,9 +256,9 @@ export default function MultiLangPage() { let response; if (editingLanguage) { - response = await apiClient.put(`/api/admin/multilang/languages/${editingLanguage.langCode}`, requestData); + response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData); } else { - response = await apiClient.post("/api/admin/multilang/languages", requestData); + response = await apiClient.post("/multilang/languages", requestData); } const result = response.data; @@ -282,17 +293,11 @@ export default function MultiLangPage() { try { const deletePromises = Array.from(selectedLanguages).map((langCode) => - fetch(`${API_BASE_URL}/multilang/languages/${langCode}`, { - method: "DELETE", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }), + apiClient.delete(`/admin/multilang/languages/${langCode}`), ); const responses = await Promise.all(deletePromises); - const failedDeletes = responses.filter((response) => !response.ok); + const failedDeletes = responses.filter((response) => !response.data.success); if (failedDeletes.length === 0) { alert("선택된 언어가 삭제되었습니다."); @@ -344,9 +349,9 @@ export default function MultiLangPage() { let response; if (editingKey) { - response = await apiClient.put(`/api/admin/multilang/keys/${editingKey.keyId}`, requestData); + response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData); } else { - response = await apiClient.post("/api/admin/multilang/keys", requestData); + response = await apiClient.post("/multilang/keys", requestData); } const data = response.data; @@ -383,7 +388,7 @@ export default function MultiLangPage() { // 키 상태 토글 const handleToggleStatus = async (keyId: number) => { try { - const response = await apiClient.put(`/api/admin/multilang/keys/${keyId}/toggle`); + const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`); const data = response.data; if (data.success) { alert(`키가 ${data.data}되었습니다.`); @@ -400,7 +405,7 @@ export default function MultiLangPage() { // 언어 상태 토글 const handleToggleLanguageStatus = async (langCode: string) => { try { - const response = await apiClient.put(`/api/admin/multilang/languages/${langCode}/toggle`); + const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`); const data = response.data; if (data.success) { alert(`언어가 ${data.data}되었습니다.`); @@ -440,9 +445,7 @@ export default function MultiLangPage() { } try { - const deletePromises = Array.from(selectedKeys).map((keyId) => - apiClient.delete(`/api/admin/multilang/keys/${keyId}`), - ); + const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`)); const responses = await Promise.all(deletePromises); const allSuccess = responses.every((response) => response.data.success); @@ -472,7 +475,7 @@ export default function MultiLangPage() { } try { - const response = await apiClient.delete(`/api/admin/multilang/keys/${keyId}`); + const response = await apiClient.delete(`/multilang/keys/${keyId}`); const data = response.data; if (data.success) { alert("언어 키가 영구적으로 삭제되었습니다."); diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index f126df67..d4ef4206 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -1,7 +1,7 @@ import axios, { AxiosResponse, AxiosError } from "axios"; // API 기본 URL 설정 -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api"; // JWT 토큰 관리 유틸리티 const TokenManager = {