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; } }