import { query, queryOne, transaction } from "../database/db"; import { logger } from "../utils/logger"; import { Language, LangKey, LangText, CreateLanguageRequest, UpdateLanguageRequest, CreateLangKeyRequest, UpdateLangKeyRequest, SaveLangTextsRequest, GetLangKeysParams, GetUserTextParams, BatchTranslationRequest, ApiResponse, } from "../types/multilang"; export class MultiLangService { constructor() {} /** * 언어 목록 조회 */ async getLanguages(): Promise { try { logger.info("언어 목록 조회 시작"); const languages = await query<{ lang_code: string; lang_name: string; lang_native: string | null; is_active: string | null; sort_order: number | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( `SELECT lang_code, lang_name, lang_native, is_active, sort_order, created_date, created_by, updated_date, updated_by FROM language_master ORDER BY sort_order ASC, lang_code ASC` ); const mappedLanguages: Language[] = languages.map((lang) => ({ langCode: lang.lang_code, langName: lang.lang_name, langNative: lang.lang_native || "", isActive: lang.is_active || "N", sortOrder: lang.sort_order ?? undefined, createdDate: lang.created_date || undefined, createdBy: lang.created_by || undefined, updatedDate: lang.updated_date || undefined, updatedBy: lang.updated_by || undefined, })); logger.info(`언어 목록 조회 완료: ${mappedLanguages.length}개`); return mappedLanguages; } 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 existingLanguage = await queryOne<{ lang_code: string }>( `SELECT lang_code FROM language_master WHERE lang_code = $1`, [languageData.langCode] ); if (existingLanguage) { throw new Error( `이미 존재하는 언어 코드입니다: ${languageData.langCode}` ); } // 언어 생성 const createdLanguage = await queryOne<{ lang_code: string; lang_name: string; lang_native: string | null; is_active: string | null; sort_order: number | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( `INSERT INTO language_master (lang_code, lang_name, lang_native, is_active, sort_order, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, [ languageData.langCode, languageData.langName, languageData.langNative, languageData.isActive || "Y", languageData.sortOrder || 0, languageData.createdBy || "system", languageData.updatedBy || "system", ] ); logger.info("언어 생성 완료", { langCode: createdLanguage!.lang_code }); return { langCode: createdLanguage!.lang_code, langName: createdLanguage!.lang_name, langNative: createdLanguage!.lang_native || "", isActive: createdLanguage!.is_active || "N", sortOrder: createdLanguage!.sort_order ?? undefined, createdDate: createdLanguage!.created_date || undefined, createdBy: createdLanguage!.created_by || undefined, updatedDate: createdLanguage!.updated_date || undefined, updatedBy: createdLanguage!.updated_by || undefined, }; } 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 existingLanguage = await queryOne<{ lang_code: string }>( `SELECT lang_code FROM language_master WHERE lang_code = $1`, [langCode] ); if (!existingLanguage) { throw new Error(`언어를 찾을 수 없습니다: ${langCode}`); } // 동적 UPDATE 쿼리 생성 const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (languageData.langName) { updates.push(`lang_name = $${paramIndex++}`); values.push(languageData.langName); } if (languageData.langNative) { updates.push(`lang_native = $${paramIndex++}`); values.push(languageData.langNative); } if (languageData.isActive) { updates.push(`is_active = $${paramIndex++}`); values.push(languageData.isActive); } if (languageData.sortOrder !== undefined) { updates.push(`sort_order = $${paramIndex++}`); values.push(languageData.sortOrder); } updates.push(`updated_by = $${paramIndex++}`); values.push(languageData.updatedBy || "system"); values.push(langCode); // WHERE 조건용 // 언어 수정 const updatedLanguage = await queryOne<{ lang_code: string; lang_name: string; lang_native: string | null; is_active: string | null; sort_order: number | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( `UPDATE language_master SET ${updates.join(", ")} WHERE lang_code = $${paramIndex} RETURNING *`, values ); logger.info("언어 수정 완료", { langCode }); return { langCode: updatedLanguage!.lang_code, langName: updatedLanguage!.lang_name, langNative: updatedLanguage!.lang_native || "", isActive: updatedLanguage!.is_active || "N", sortOrder: updatedLanguage!.sort_order ?? undefined, createdDate: updatedLanguage!.created_date || undefined, createdBy: updatedLanguage!.created_by || undefined, updatedDate: updatedLanguage!.updated_date || undefined, updatedBy: updatedLanguage!.updated_by || undefined, }; } 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 currentLanguage = await queryOne<{ is_active: string | null }>( `SELECT is_active FROM language_master WHERE lang_code = $1`, [langCode] ); if (!currentLanguage) { throw new Error(`언어를 찾을 수 없습니다: ${langCode}`); } const newStatus = currentLanguage.is_active === "Y" ? "N" : "Y"; // 상태 업데이트 await query( `UPDATE language_master SET is_active = $1, updated_by = $2 WHERE lang_code = $3`, [newStatus, "system", langCode] ); 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 }); const whereConditions: string[] = []; const values: any[] = []; let paramIndex = 1; // 회사 코드 필터 if (params.companyCode) { whereConditions.push(`company_code = $${paramIndex++}`); values.push(params.companyCode); } // 메뉴 코드 필터 if (params.menuCode) { whereConditions.push(`menu_name = $${paramIndex++}`); values.push(params.menuCode); } // 검색 조건 (OR) if (params.searchText) { whereConditions.push( `(lang_key ILIKE $${paramIndex} OR description ILIKE $${paramIndex} OR menu_name ILIKE $${paramIndex})` ); values.push(`%${params.searchText}%`); paramIndex++; } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; const langKeys = await query<{ key_id: number; company_code: string; menu_name: string | null; lang_key: string; description: string | null; is_active: string | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( `SELECT key_id, company_code, menu_name, lang_key, description, is_active, created_date, created_by, updated_date, updated_by FROM multi_lang_key_master ${whereClause} ORDER BY company_code ASC, menu_name ASC, lang_key ASC`, values ); const mappedKeys: LangKey[] = langKeys.map((key) => ({ keyId: key.key_id, companyCode: key.company_code, menuName: key.menu_name || undefined, langKey: key.lang_key, description: key.description || undefined, isActive: key.is_active || "Y", createdDate: key.created_date || undefined, createdBy: key.created_by || undefined, updatedDate: key.updated_date || undefined, updatedBy: key.updated_by || undefined, })); logger.info(`다국어 키 목록 조회 완료: ${mappedKeys.length}개`); return mappedKeys; } 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 langTexts = await query<{ text_id: number; key_id: number; lang_code: string; lang_text: string; is_active: string | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( `SELECT text_id, key_id, lang_code, lang_text, is_active, created_date, created_by, updated_date, updated_by FROM multi_lang_text WHERE key_id = $1 AND is_active = $2 ORDER BY lang_code ASC`, [keyId, "Y"] ); const mappedTexts: LangText[] = langTexts.map((text) => ({ textId: text.text_id, keyId: text.key_id, langCode: text.lang_code, langText: text.lang_text, isActive: text.is_active || "Y", createdDate: text.created_date || undefined, createdBy: text.created_by || undefined, updatedDate: text.updated_date || undefined, updatedBy: text.updated_by || undefined, })); logger.info(`다국어 텍스트 조회 완료: ${mappedTexts.length}개`); return mappedTexts; } 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 existingKey = await queryOne<{ key_id: number }>( `SELECT key_id FROM multi_lang_key_master WHERE company_code = $1 AND lang_key = $2`, [keyData.companyCode, keyData.langKey] ); if (existingKey) { throw new Error( `동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}` ); } // 다국어 키 생성 const createdKey = await queryOne<{ key_id: number }>( `INSERT INTO multi_lang_key_master (company_code, menu_name, lang_key, description, is_active, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING key_id`, [ keyData.companyCode, keyData.menuName || null, keyData.langKey, keyData.description || null, keyData.isActive || "Y", keyData.createdBy || "system", keyData.updatedBy || "system", ] ); logger.info("다국어 키 생성 완료", { keyId: createdKey!.key_id, langKey: keyData.langKey, }); return createdKey!.key_id; } 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 existingKey = await queryOne<{ key_id: number }>( `SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`, [keyId] ); if (!existingKey) { throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); } // 중복 체크 (자신을 제외하고) if (keyData.companyCode && keyData.langKey) { const duplicateKey = await queryOne<{ key_id: number }>( `SELECT key_id FROM multi_lang_key_master WHERE company_code = $1 AND lang_key = $2 AND key_id != $3`, [keyData.companyCode, keyData.langKey, keyId] ); if (duplicateKey) { throw new Error( `동일한 회사에 이미 존재하는 언어키입니다: ${keyData.langKey}` ); } } // 동적 UPDATE 쿼리 생성 const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (keyData.companyCode) { updates.push(`company_code = $${paramIndex++}`); values.push(keyData.companyCode); } if (keyData.menuName !== undefined) { updates.push(`menu_name = $${paramIndex++}`); values.push(keyData.menuName); } if (keyData.langKey) { updates.push(`lang_key = $${paramIndex++}`); values.push(keyData.langKey); } if (keyData.description !== undefined) { updates.push(`description = $${paramIndex++}`); values.push(keyData.description); } updates.push(`updated_by = $${paramIndex++}`); values.push(keyData.updatedBy || "system"); values.push(keyId); // WHERE 조건용 // 다국어 키 수정 await query( `UPDATE multi_lang_key_master SET ${updates.join(", ")} WHERE key_id = $${paramIndex}`, values ); 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 existingKey = await queryOne<{ key_id: number }>( `SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`, [keyId] ); if (!existingKey) { throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); } // 트랜잭션으로 키와 연관된 텍스트 모두 삭제 await transaction(async (client) => { // 관련된 다국어 텍스트 삭제 await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [ keyId, ]); // 다국어 키 삭제 await client.query( `DELETE FROM multi_lang_key_master WHERE key_id = $1`, [keyId] ); }); logger.info("다국어 키 삭제 완료", { keyId }); } 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 currentKey = await queryOne<{ is_active: string | null }>( `SELECT is_active FROM multi_lang_key_master WHERE key_id = $1`, [keyId] ); if (!currentKey) { throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); } const newStatus = currentKey.is_active === "Y" ? "N" : "Y"; // 상태 업데이트 await query( `UPDATE multi_lang_key_master SET is_active = $1, updated_by = $2 WHERE key_id = $3`, [newStatus, "system", keyId] ); 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 existingKey = await queryOne<{ key_id: number }>( `SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`, [keyId] ); if (!existingKey) { throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`); } // 트랜잭션으로 기존 텍스트 삭제 후 새로 생성 await transaction(async (client) => { // 기존 텍스트 삭제 await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [ keyId, ]); // 새로운 텍스트 삽입 if (textData.texts.length > 0) { for (const text of textData.texts) { await client.query( `INSERT INTO multi_lang_text (key_id, lang_code, lang_text, is_active, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6)`, [ keyId, text.langCode, text.langText, text.isActive || "Y", text.createdBy || "system", text.updatedBy || "system", ] ); } } }); logger.info("다국어 텍스트 저장 완료", { keyId, savedCount: textData.texts.length, }); } 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 result = await queryOne<{ lang_text: string }>( `SELECT mlt.lang_text FROM multi_lang_text mlt INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id WHERE mlt.lang_code = $1 AND mlt.is_active = $2 AND mlkm.company_code = $3 AND mlkm.menu_name = $4 AND mlkm.lang_key = $5 AND mlkm.is_active = $6`, [ params.userLang, "Y", params.companyCode, params.menuCode, params.langKey, "Y", ] ); if (!result) { logger.warn("사용자별 다국어 텍스트를 찾을 수 없음", { params }); return params.langKey; // 기본값으로 키 반환 } logger.info("사용자별 다국어 텍스트 조회 완료", { params, langText: result.lang_text, }); return result.lang_text; } 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 result = await queryOne<{ lang_text: string }>( `SELECT mlt.lang_text FROM multi_lang_text mlt INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id WHERE mlt.lang_code = $1 AND mlt.is_active = $2 AND mlkm.company_code = $3 AND mlkm.lang_key = $4 AND mlkm.is_active = $5`, [langCode, "Y", companyCode, langKey, "Y"] ); if (!result) { logger.warn("특정 키의 다국어 텍스트를 찾을 수 없음", { companyCode, langKey, langCode, }); return langKey; // 기본값으로 키 반환 } logger.info("특정 키의 다국어 텍스트 조회 완료", { companyCode, langKey, langCode, langText: result.lang_text, }); return result.lang_text; } 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 placeholders = params.langKeys .map((_, i) => `$${i + 4}`) .join(", "); const translations = await query<{ lang_text: string; lang_key: string; company_code: string; }>( `SELECT mlt.lang_text, mlkm.lang_key, mlkm.company_code FROM multi_lang_text mlt INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id WHERE mlt.lang_code = $1 AND mlt.is_active = $2 AND mlkm.lang_key IN (${placeholders}) AND mlkm.company_code IN ($3, '*') AND mlkm.is_active = $2 ORDER BY mlkm.company_code ASC`, [params.userLang, "Y", params.companyCode, ...params.langKeys] ); const result: Record = {}; // 기본값으로 모든 키 설정 params.langKeys.forEach((key) => { result[key] = key; }); // 실제 번역으로 덮어쓰기 (회사별 우선) translations.forEach((translation) => { const langKey = translation.lang_key; if (params.langKeys.includes(langKey)) { result[langKey] = translation.lang_text; } }); logger.info("배치 번역 조회 완료", { totalKeys: params.langKeys.length, foundTranslations: translations.length, resultKeys: Object.keys(result).length, }); return result; } catch (error) { logger.error("배치 번역 조회 중 오류 발생:", error); throw new Error( `배치 번역 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 언어 삭제 */ async deleteLanguage(langCode: string): Promise { try { logger.info("언어 삭제 시작", { langCode }); // 기존 언어 확인 const existingLanguage = await queryOne<{ lang_code: string }>( `SELECT lang_code FROM language_master WHERE lang_code = $1`, [langCode] ); if (!existingLanguage) { throw new Error(`언어를 찾을 수 없습니다: ${langCode}`); } // 트랜잭션으로 언어와 관련 텍스트 삭제 await transaction(async (client) => { // 해당 언어의 다국어 텍스트 삭제 const deleteResult = await client.query( `DELETE FROM multi_lang_text WHERE lang_code = $1`, [langCode] ); logger.info(`삭제된 다국어 텍스트 수: ${deleteResult.rowCount}`, { langCode, }); // 언어 마스터 삭제 await client.query(`DELETE FROM language_master WHERE lang_code = $1`, [ langCode, ]); }); logger.info("언어 삭제 완료", { langCode }); } catch (error) { logger.error("언어 삭제 중 오류 발생:", error); throw new Error( `언어 삭제 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } }