import { Request, Response } from "express"; import { logger } from "../utils/logger"; import { AuthenticatedRequest } from "../types/auth"; import { ApiResponse } from "../types/common"; import { Client } from "pg"; import { query, queryOne } from "../database/db"; import config from "../config/environment"; import { AdminService } from "../services/adminService"; import { EncryptUtil } from "../utils/encryptUtil"; import { FileSystemManager } from "../utils/fileSystemManager"; /** * 관리자 메뉴 목록 조회 */ export async function getAdminMenus( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("=== 관리자 메뉴 목록 조회 시작 ==="); // 현재 로그인한 사용자의 정보 가져오기 const userId = req.user?.userId; const userCompanyCode = req.user?.companyCode || "ILSHIN"; const userType = req.user?.userType; const userLang = (req.query.userLang as string) || "ko"; const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가 logger.info(`사용자 ID: ${userId}`); logger.info(`사용자 회사 코드: ${userCompanyCode}`); logger.info(`사용자 유형: ${userType}`); logger.info(`사용자 로케일: ${userLang}`); logger.info(`메뉴 타입: ${menuType || "전체"}`); const paramMap = { userId, userCompanyCode, userType, userLang, menuType, // menuType 추가 }; const menuList = await AdminService.getAdminMenuList(paramMap); logger.info( `관리자 메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"}, 회사: ${userCompanyCode})` ); if (menuList.length > 0) { logger.info("첫 번째 메뉴:", menuList[0]); } const response: ApiResponse = { success: true, message: "관리자 메뉴 목록 조회 성공", data: menuList, }; res.status(200).json(response); } catch (error) { logger.error("관리자 메뉴 목록 조회 중 오류 발생:", error); const response: ApiResponse = { success: false, message: "관리자 메뉴 목록 조회 중 오류가 발생했습니다.", error: { code: "ADMIN_MENU_LIST_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 사용자 메뉴 목록 조회 */ export async function getUserMenus( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("=== 사용자 메뉴 목록 조회 시작 ==="); // 현재 로그인한 사용자의 정보 가져오기 const userId = req.user?.userId; const userCompanyCode = req.user?.companyCode || "ILSHIN"; const userType = req.user?.userType; const userLang = (req.query.userLang as string) || "ko"; logger.info(`사용자 ID: ${userId}`); logger.info(`사용자 회사 코드: ${userCompanyCode}`); logger.info(`사용자 유형: ${userType}`); logger.info(`사용자 로케일: ${userLang}`); const paramMap = { userId, userCompanyCode, userType, userLang, }; const menuList = await AdminService.getUserMenuList(paramMap); logger.info( `사용자 메뉴 조회 결과: ${menuList.length}개 (회사: ${userCompanyCode})` ); if (menuList.length > 0) { logger.info("첫 번째 메뉴:", menuList[0]); } const response: ApiResponse = { success: true, message: "사용자 메뉴 목록 조회 성공", data: menuList, }; res.status(200).json(response); } catch (error) { logger.error("사용자 메뉴 목록 조회 중 오류 발생:", error); const response: ApiResponse = { success: false, message: "사용자 메뉴 목록 조회 중 오류가 발생했습니다.", error: { code: "USER_MENU_LIST_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * 메뉴 정보 조회 */ export async function getMenuInfo( req: AuthenticatedRequest, res: Response ): Promise { try { const { menuId } = req.params; logger.info(`=== 메뉴 정보 조회 시작 - menuId: ${menuId} ===`); const menuInfo = await AdminService.getMenuInfo(menuId); if (!menuInfo) { const response: ApiResponse = { success: false, message: "메뉴를 찾을 수 없습니다.", error: { code: "MENU_NOT_FOUND", details: `Menu ID: ${menuId}`, }, }; res.status(404).json(response); return; } const response: ApiResponse = { success: true, message: "메뉴 정보 조회 성공", data: menuInfo, }; res.status(200).json(response); } catch (error) { logger.error("메뉴 정보 조회 중 오류 발생:", error); const response: ApiResponse = { success: false, message: "메뉴 정보 조회 중 오류가 발생했습니다.", error: { code: "MENU_INFO_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } } /** * GET /api/admin/users * 사용자 목록 조회 API * 기존 Java AdminController.getUserList() 포팅 */ export const getUserList = async (req: AuthenticatedRequest, res: Response) => { try { logger.info("사용자 목록 조회 요청", { query: req.query, user: req.user, }); const { page = 1, countPerPage = 20, search, searchField, searchValue, search_sabun, search_companyName, search_deptName, search_positionName, search_userId, search_userName, search_tel, search_email, deptCode, status, companyCode, // 회사 코드 필터 추가 size, // countPerPage 대신 사용 가능 } = req.query; // Raw Query를 사용한 사용자 목록 조회 let searchType = "none"; let whereConditions: string[] = []; let queryParams: any[] = []; let paramIndex = 1; // 회사 코드 필터 (권한 그룹 멤버 관리 시 사용) if (companyCode && typeof companyCode === "string" && companyCode.trim()) { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(companyCode.trim()); paramIndex++; logger.info("회사 코드 필터 적용", { companyCode }); } // 검색 조건 처리 if (search && typeof search === "string" && search.trim()) { // 통합 검색 searchType = "unified"; const searchTerm = search.trim(); whereConditions.push(`( sabun ILIKE $${paramIndex} OR user_type_name ILIKE $${paramIndex} OR dept_name ILIKE $${paramIndex} OR position_name ILIKE $${paramIndex} OR user_id ILIKE $${paramIndex} OR user_name ILIKE $${paramIndex} OR tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex} OR email ILIKE $${paramIndex} )`); queryParams.push(`%${searchTerm}%`); paramIndex++; logger.info("통합 검색 실행", { searchTerm }); } else if (searchField && searchValue) { // 단일 필드 검색 searchType = "single"; const fieldMap: { [key: string]: string } = { sabun: "sabun", companyName: "user_type_name", deptName: "dept_name", positionName: "position_name", userId: "user_id", userName: "user_name", tel: "tel", cellPhone: "cell_phone", email: "email", }; if (fieldMap[searchField as string]) { if (searchField === "tel") { whereConditions.push( `(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})` ); queryParams.push(`%${searchValue}%`); paramIndex++; } else { whereConditions.push( `${fieldMap[searchField as string]} ILIKE $${paramIndex}` ); queryParams.push(`%${searchValue}%`); paramIndex++; } logger.info("단일 필드 검색 실행", { searchField, searchValue }); } } else { // 고급 검색 (개별 필드별 AND 조건) const advancedSearchFields = [ { param: search_sabun, field: "sabun" }, { param: search_companyName, field: "user_type_name" }, { param: search_deptName, field: "dept_name" }, { param: search_positionName, field: "position_name" }, { param: search_userId, field: "user_id" }, { param: search_userName, field: "user_name" }, { param: search_email, field: "email" }, ]; let hasAdvancedSearch = false; for (const { param, field } of advancedSearchFields) { if (param && typeof param === "string" && param.trim()) { whereConditions.push(`${field} ILIKE $${paramIndex}`); queryParams.push(`%${param.trim()}%`); paramIndex++; hasAdvancedSearch = true; } } // 전화번호 검색 if (search_tel && typeof search_tel === "string" && search_tel.trim()) { whereConditions.push( `(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})` ); queryParams.push(`%${search_tel.trim()}%`); paramIndex++; hasAdvancedSearch = true; } if (hasAdvancedSearch) { searchType = "advanced"; logger.info("고급 검색 실행", { search_sabun, search_companyName, search_deptName, search_positionName, search_userId, search_userName, search_tel, search_email, }); } } // 현재 로그인한 사용자의 회사 코드 필터 (슈퍼관리자가 아닌 경우) if (req.user && req.user.companyCode !== "*" && !companyCode) { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(req.user.companyCode); paramIndex++; logger.info("사용자 회사 코드 필터 적용", { companyCode: req.user.companyCode, }); } // 기존 필터들 if (deptCode) { whereConditions.push(`dept_code = $${paramIndex}`); queryParams.push(deptCode); paramIndex++; } if (status) { whereConditions.push(`status = $${paramIndex}`); queryParams.push(status); paramIndex++; } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; // 총 개수 조회 const countQuery = ` SELECT COUNT(*) as total FROM user_info ${whereClause} `; const countResult = await query<{ total: string }>(countQuery, queryParams); const totalCount = parseInt(countResult[0]?.total || "0", 10); // 사용자 목록 조회 const limit = size ? Number(size) : Number(countPerPage); const offset = (Number(page) - 1) * limit; const usersQuery = ` SELECT sabun, user_id, user_name, user_name_eng, dept_code, dept_name, position_code, position_name, email, tel, cell_phone, user_type, user_type_name, regdate, status, company_code, locale FROM user_info ${whereClause} ORDER BY regdate DESC, user_name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; const users = await query(usersQuery, [...queryParams, limit, offset]); // 응답 데이터 가공 const processedUsers = users.map((user) => ({ userId: user.user_id, userName: user.user_name, userNameEng: user.user_name_eng || null, sabun: user.sabun || null, deptCode: user.dept_code || null, deptName: user.dept_name || null, positionCode: user.position_code || null, positionName: user.position_name || null, email: user.email || null, tel: user.tel || null, cellPhone: user.cell_phone || null, userType: user.user_type || null, userTypeName: user.user_type_name || null, status: user.status || "active", companyCode: user.company_code || null, locale: user.locale || null, regDate: user.regdate ? new Date(user.regdate).toISOString().split("T")[0] : null, })); const response = { success: true, data: processedUsers, total: totalCount, searchType, pagination: { page: Number(page), limit: limit, totalPages: Math.ceil(totalCount / limit), }, message: "사용자 목록 조회 성공", }; logger.info("사용자 목록 조회 성공", { totalCount, returnedCount: processedUsers.length, searchType, currentPage: Number(page), limit: limit, companyCode: companyCode || "all", }); res.status(200).json(response); } catch (error) { logger.error("사용자 목록 조회 실패", { error }); res.status(500).json({ success: false, message: "사용자 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * GET /api/admin/user-locale * 사용자 로케일 조회 API */ export const getUserLocale = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { logger.info("사용자 로케일 조회 요청", { query: req.query, user: req.user, }); if (!req.user?.userId) { res.status(400).json({ success: false, message: "사용자 정보가 없습니다.", }); return; } // Raw Query로 사용자 로케일 조회 const userInfo = await queryOne<{ locale: string }>( "SELECT locale FROM user_info WHERE user_id = $1", [req.user.userId] ); let userLocale = "en"; // 기본값 if (userInfo?.locale) { userLocale = userInfo.locale; logger.info("데이터베이스에서 사용자 로케일 조회 성공", { userId: req.user.userId, locale: userLocale, }); } else { logger.info("사용자 로케일이 설정되지 않음, 기본값 사용", { userId: req.user.userId, defaultLocale: userLocale, }); } const response = { success: true, data: userLocale, message: "사용자 로케일 조회 성공", }; logger.info("사용자 로케일 조회 성공", { userLocale, userId: req.user.userId, fromDatabase: !!userInfo?.locale, }); res.status(200).json(response); } catch (error) { logger.error("사용자 로케일 조회 실패", { error }); res.status(500).json({ success: false, message: "사용자 로케일 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * 사용자 로케일 설정 */ export const setUserLocale = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { logger.info("사용자 로케일 설정 요청", { body: req.body, user: req.user, }); if (!req.user?.userId) { res.status(400).json({ success: false, message: "사용자 정보가 없습니다.", }); return; } const { locale } = req.body; if (!locale || !["ko", "en", "ja", "zh"].includes(locale)) { res.status(400).json({ success: false, message: "유효하지 않은 로케일입니다. (ko, en, ja, zh 중 선택)", }); return; } // Raw Query로 사용자 로케일 저장 await query("UPDATE user_info SET locale = $1 WHERE user_id = $2", [ locale, req.user.userId, ]); logger.info("사용자 로케일을 데이터베이스에 저장 완료", { locale, userId: req.user.userId, }); const response = { success: true, data: locale, message: "사용자 로케일 설정 성공", }; logger.info("사용자 로케일 설정 성공", { locale, userId: req.user.userId, }); res.status(200).json(response); } catch (error) { logger.error("사용자 로케일 설정 실패", { error }); res.status(500).json({ success: false, message: "사용자 로케일 설정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * GET /api/admin/companies * 회사 목록 조회 API * 기존 Java AdminController의 회사 목록 조회 기능 포팅 */ export const getCompanyList = async ( req: AuthenticatedRequest, res: Response ) => { try { logger.info("회사 목록 조회 요청", { query: req.query, user: req.user, }); // Raw Query로 회사 목록 조회 const companies = await query( `SELECT company_code, company_name, status, writer, regdate FROM company_mng WHERE status = 'active' OR status IS NULL ORDER BY company_name ASC` ); // 프론트엔드에서 기대하는 응답 형식으로 변환 const response = { success: true, data: companies.map((company) => ({ company_code: company.company_code, company_name: company.company_name, status: company.status || "active", writer: company.writer, regdate: company.regdate ? new Date(company.regdate).toISOString() : new Date().toISOString(), data_type: "company", })), message: "회사 목록 조회 성공", }; logger.info("회사 목록 조회 성공", { totalCount: companies.length, companies: companies.map((c) => ({ code: c.company_code, name: c.company_name, })), }); res.status(200).json(response); } catch (error) { logger.error("회사 목록 조회 실패", { error }); res.status(500).json({ success: false, message: "회사 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * 다국어 언어 목록 조회 (더미 데이터) */ export async function getLanguageList( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("다국어 언어 목록 조회 요청"); // 더미 데이터 반환 const languages = [ { langCode: "KR", langName: "한국어", langNative: "한국어", isActive: "Y", }, { langCode: "EN", langName: "English", langNative: "English", isActive: "Y", }, { langCode: "JP", langName: "日本語", langNative: "日本語", isActive: "Y", }, { langCode: "CN", langName: "中文", langNative: "中文", isActive: "Y" }, ]; const response: ApiResponse = { success: true, message: "언어 목록 조회 성공", data: languages, }; res.status(200).json(response); } catch (error) { logger.error("언어 목록 조회 실패:", error); res.status(500).json({ success: false, message: "언어 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 키 목록 조회 */ export async function getLangKeyList( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("다국어 키 목록 조회 요청", { query: req.query, user: req.user, }); // Raw Query로 다국어 키 목록 조회 const result = await query( `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 ORDER BY company_code ASC, menu_name ASC, lang_key ASC` ); const langKeys = result.map((row) => ({ keyId: row.key_id, companyCode: row.company_code, menuName: row.menu_name, langKey: row.lang_key, description: row.description, isActive: row.is_active, createdDate: row.created_date ? new Date(row.created_date).toISOString() : null, createdBy: row.created_by, updatedDate: row.updated_date ? new Date(row.updated_date).toISOString() : null, updatedBy: row.updated_by, })); // 프론트엔드에서 기대하는 응답 형식으로 변환 const response: ApiResponse = { success: true, message: "다국어 키 목록 조회 성공", data: langKeys, }; logger.info("다국어 키 목록 조회 성공", { totalCount: langKeys.length, response: response, }); res.status(200).json(response); } catch (error) { logger.error("다국어 키 목록 조회 실패:", error); res.status(500).json({ success: false, message: "다국어 키 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 텍스트 목록 조회 (더미 데이터) */ export async function getLangTextList( req: AuthenticatedRequest, res: Response ): Promise { try { const { keyId } = req.params; logger.info(`다국어 텍스트 목록 조회 요청: keyId = ${keyId}`); // 더미 데이터 반환 const langTexts = [ { textId: 1, keyId: parseInt(keyId), langCode: "KR", langText: "사용자 관리", isActive: "Y", }, { textId: 2, keyId: parseInt(keyId), langCode: "EN", langText: "User Management", isActive: "Y", }, { textId: 3, keyId: parseInt(keyId), langCode: "JP", langText: "ユーザー管理", isActive: "Y", }, ]; const response: ApiResponse = { success: true, message: "다국어 텍스트 목록 조회 성공", data: langTexts, }; res.status(200).json(response); } catch (error) { logger.error("다국어 텍스트 목록 조회 실패:", error); res.status(500).json({ success: false, message: "다국어 텍스트 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 텍스트 저장 (더미 데이터) */ export async function saveLangTexts( req: AuthenticatedRequest, res: Response ): Promise { try { const { keyId } = req.params; const textData = req.body; logger.info(`다국어 텍스트 저장 요청: keyId = ${keyId}`, { textData }); // 더미 응답 const response: ApiResponse = { success: true, message: "다국어 텍스트 저장 성공", data: { savedCount: textData.length }, }; res.status(200).json(response); } catch (error) { logger.error("다국어 텍스트 저장 실패:", error); res.status(500).json({ success: false, message: "다국어 텍스트 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 키 저장 (더미 데이터) */ export async function saveLangKey( req: AuthenticatedRequest, res: Response ): Promise { try { const keyData = req.body; logger.info("다국어 키 저장 요청", { keyData }); // 더미 응답 const response: ApiResponse = { success: true, message: "다국어 키 저장 성공", data: { keyId: Math.floor(Math.random() * 1000) + 1 }, }; res.status(200).json(response); } catch (error) { logger.error("다국어 키 저장 실패:", error); res.status(500).json({ success: false, message: "다국어 키 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 키 수정 (더미 데이터) */ export async function updateLangKey( req: AuthenticatedRequest, res: Response ): Promise { try { const { keyId } = req.params; const keyData = req.body; logger.info(`다국어 키 수정 요청: keyId = ${keyId}`, { keyData }); // 더미 응답 const response: ApiResponse = { success: true, message: "다국어 키 수정 성공", data: { keyId: parseInt(keyId) }, }; res.status(200).json(response); } catch (error) { logger.error("다국어 키 수정 실패:", error); res.status(500).json({ success: false, message: "다국어 키 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 키 삭제 (더미 데이터) */ export async function deleteLangKey( req: AuthenticatedRequest, res: Response ): Promise { try { const { keyId } = req.params; logger.info(`다국어 키 삭제 요청: keyId = ${keyId}`); // 더미 응답 const response: ApiResponse = { success: true, message: "다국어 키 삭제 성공", data: { deletedKeyId: parseInt(keyId) }, }; res.status(200).json(response); } catch (error) { logger.error("다국어 키 삭제 실패:", error); res.status(500).json({ success: false, message: "다국어 키 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 다국어 키 상태 토글 (더미 데이터) */ export async function toggleLangKeyStatus( req: AuthenticatedRequest, res: Response ): Promise { try { const { keyId } = req.params; logger.info(`다국어 키 상태 토글 요청: keyId = ${keyId}`); // 더미 응답 const response: ApiResponse = { success: true, message: "다국어 키 상태 토글 성공", data: "활성화", }; res.status(200).json(response); } catch (error) { logger.error("다국어 키 상태 토글 실패:", error); res.status(500).json({ success: false, message: "다국어 키 상태 토글 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 언어 저장 (더미 데이터) */ export async function saveLanguage( req: AuthenticatedRequest, res: Response ): Promise { try { const langData = req.body; logger.info("언어 저장 요청", { langData }); // 더미 응답 const response: ApiResponse = { success: true, message: "언어 저장 성공", data: { langCode: langData.langCode }, }; res.status(200).json(response); } catch (error) { logger.error("언어 저장 실패:", error); res.status(500).json({ success: false, message: "언어 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 언어 수정 (더미 데이터) */ export async function updateLanguage( req: AuthenticatedRequest, res: Response ): Promise { try { const { langCode } = req.params; const langData = req.body; logger.info(`언어 수정 요청: langCode = ${langCode}`, { langData }); // 더미 응답 const response: ApiResponse = { success: true, message: "언어 수정 성공", data: { langCode }, }; res.status(200).json(response); } catch (error) { logger.error("언어 수정 실패:", error); res.status(500).json({ success: false, message: "언어 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 언어 상태 토글 (더미 데이터) */ export async function toggleLanguageStatus( req: AuthenticatedRequest, res: Response ): Promise { try { const { langCode } = req.params; logger.info(`언어 상태 토글 요청: langCode = ${langCode}`); // 더미 응답 const response: ApiResponse = { success: true, message: "언어 상태 토글 성공", data: "활성화", }; res.status(200).json(response); } catch (error) { logger.error("언어 상태 토글 실패:", error); res.status(500).json({ success: false, message: "언어 상태 토글 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 메뉴 저장 (추가/수정) */ export async function saveMenu( req: AuthenticatedRequest, res: Response ): Promise { try { const menuData = req.body; logger.info("메뉴 저장 요청", { menuData, user: req.user }); // 사용자의 company_code 확인 if (!req.user?.companyCode) { res.status(400).json({ success: false, message: "사용자의 회사 코드를 찾을 수 없습니다.", error: "Missing company_code", }); return; } // Raw Query를 사용한 메뉴 저장 const objid = Date.now(); // 고유 ID 생성 const companyCode = req.user.companyCode; const [savedMenu] = await query( `INSERT INTO menu_info ( objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, menu_desc, writer, regdate, status, system_name, company_code, lang_key, lang_key_desc ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`, [ objid, menuData.menuType ? Number(menuData.menuType) : null, menuData.parentObjId ? Number(menuData.parentObjId) : null, menuData.menuNameKor, menuData.menuNameEng || null, menuData.seq ? Number(menuData.seq) : null, menuData.menuUrl || null, menuData.menuDesc || null, req.user?.userId || "admin", new Date(), menuData.status || "active", menuData.systemName || null, companyCode, menuData.langKey || null, menuData.langKeyDesc || null, ] ); logger.info("메뉴 저장 성공", { savedMenu }); const response: ApiResponse = { success: true, message: "메뉴가 성공적으로 저장되었습니다.", data: { objid: savedMenu.objid.toString(), // BigInt를 문자열로 변환 menuNameKor: savedMenu.menu_name_kor, menuNameEng: savedMenu.menu_name_eng, menuUrl: savedMenu.menu_url, menuDesc: savedMenu.menu_desc, status: savedMenu.status, writer: savedMenu.writer, regdate: new Date(savedMenu.regdate).toISOString(), }, }; res.status(200).json(response); } catch (error) { logger.error("메뉴 저장 실패:", error); res.status(500).json({ success: false, message: "메뉴 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 메뉴 수정 */ export async function updateMenu( req: AuthenticatedRequest, res: Response ): Promise { try { const { menuId } = req.params; const menuData = req.body; logger.info(`메뉴 수정 요청: menuId = ${menuId}`, { menuData, user: req.user, }); // 사용자의 company_code 확인 if (!req.user?.companyCode) { res.status(400).json({ success: false, message: "사용자의 회사 코드를 찾을 수 없습니다.", error: "Missing company_code", }); return; } const companyCode = req.user.companyCode; // Raw Query를 사용한 메뉴 수정 const [updatedMenu] = await query( `UPDATE menu_info SET menu_type = $1, parent_obj_id = $2, menu_name_kor = $3, menu_name_eng = $4, seq = $5, menu_url = $6, menu_desc = $7, status = $8, system_name = $9, company_code = $10, lang_key = $11, lang_key_desc = $12 WHERE objid = $13 RETURNING *`, [ menuData.menuType ? Number(menuData.menuType) : null, menuData.parentObjId ? Number(menuData.parentObjId) : null, menuData.menuNameKor, menuData.menuNameEng || null, menuData.seq ? Number(menuData.seq) : null, menuData.menuUrl || null, menuData.menuDesc || null, menuData.status || "active", menuData.systemName || null, companyCode, menuData.langKey || null, menuData.langKeyDesc || null, Number(menuId), ] ); logger.info("메뉴 수정 성공", { updatedMenu }); const response: ApiResponse = { success: true, message: "메뉴가 성공적으로 수정되었습니다.", data: { objid: updatedMenu.objid.toString(), menuNameKor: updatedMenu.menu_name_kor, menuNameEng: updatedMenu.menu_name_eng, menuUrl: updatedMenu.menu_url, menuDesc: updatedMenu.menu_desc, status: updatedMenu.status, writer: updatedMenu.writer, regdate: new Date(updatedMenu.regdate).toISOString(), }, }; res.status(200).json(response); } catch (error) { logger.error("메뉴 수정 실패:", error); res.status(500).json({ success: false, message: "메뉴 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 메뉴 삭제 */ export async function deleteMenu( req: AuthenticatedRequest, res: Response ): Promise { try { const { menuId } = req.params; logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user }); // Raw Query를 사용한 메뉴 삭제 const [deletedMenu] = await query( `DELETE FROM menu_info WHERE objid = $1 RETURNING *`, [Number(menuId)] ); logger.info("메뉴 삭제 성공", { deletedMenu }); const response: ApiResponse = { success: true, message: "메뉴가 성공적으로 삭제되었습니다.", data: { objid: deletedMenu.objid.toString(), menuNameKor: deletedMenu.menu_name_kor, menuNameEng: deletedMenu.menu_name_eng, menuUrl: deletedMenu.menu_url, menuDesc: deletedMenu.menu_desc, status: deletedMenu.status, writer: deletedMenu.writer, regdate: new Date(deletedMenu.regdate).toISOString(), }, }; res.status(200).json(response); } catch (error) { logger.error("메뉴 삭제 실패:", error); res.status(500).json({ success: false, message: "메뉴 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 메뉴 일괄 삭제 */ export async function deleteMenusBatch( req: AuthenticatedRequest, res: Response ): Promise { try { const menuIds = req.body as string[]; logger.info("메뉴 일괄 삭제 요청", { menuIds, user: req.user }); if (!Array.isArray(menuIds) || menuIds.length === 0) { res.status(400).json({ success: false, message: "삭제할 메뉴 ID 목록이 필요합니다.", }); return; } // Raw Query를 사용한 메뉴 일괄 삭제 let deletedCount = 0; let failedCount = 0; const deletedMenus: any[] = []; const failedMenuIds: string[] = []; // 각 메뉴 ID에 대해 삭제 시도 for (const menuId of menuIds) { try { const result = await query( `DELETE FROM menu_info WHERE objid = $1 RETURNING *`, [Number(menuId)] ); if (result.length > 0) { deletedCount++; deletedMenus.push({ ...result[0], objid: result[0].objid.toString(), }); } else { failedCount++; failedMenuIds.push(menuId); } } catch (error) { logger.error(`메뉴 삭제 실패 (ID: ${menuId}):`, error); failedCount++; failedMenuIds.push(menuId); } } logger.info("메뉴 일괄 삭제 완료", { total: menuIds.length, deletedCount, failedCount, deletedMenus, failedMenuIds, }); const response: ApiResponse = { success: true, message: `메뉴 일괄 삭제 완료: ${deletedCount}개 삭제, ${failedCount}개 실패`, data: { deletedCount, failedCount, total: menuIds.length, deletedMenus, failedMenuIds, }, }; res.status(200).json(response); } catch (error) { logger.error("메뉴 일괄 삭제 실패:", error); res.status(500).json({ success: false, message: "메뉴 일괄 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * 회사 목록 조회 (실제 데이터베이스에서) */ export async function getCompanyListFromDB( req: AuthenticatedRequest, res: Response ): Promise { try { logger.info("회사 목록 조회 요청 (Raw Query)", { user: req.user }); // Raw Query로 회사 목록 조회 const companies = await query( `SELECT company_code, company_name, writer, regdate, status FROM company_mng ORDER BY regdate DESC` ); logger.info("회사 목록 조회 성공 (Raw Query)", { count: companies.length }); const response: ApiResponse = { success: true, message: "회사 목록 조회 성공", data: companies, total: companies.length, }; res.status(200).json(response); } catch (error) { logger.error("회사 목록 조회 실패 (Raw Query):", error); res.status(500).json({ success: false, message: "회사 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } /** * GET /api/admin/departments * 부서 목록 조회 API * 기존 Java AdminController의 부서 목록 조회 기능 포팅 */ export const getDepartmentList = async ( req: AuthenticatedRequest, res: Response ) => { try { logger.info("부서 목록 조회 요청", { query: req.query, user: req.user, }); const { companyCode, status, search } = req.query; // Raw Query를 사용한 부서 목록 조회 let whereConditions: string[] = []; let queryParams: any[] = []; let paramIndex = 1; // 회사 코드 필터 if (companyCode) { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(companyCode); paramIndex++; } // 상태 필터 if (status) { whereConditions.push(`status = $${paramIndex}`); queryParams.push(status); paramIndex++; } // 검색 조건 if (search && typeof search === "string" && search.trim()) { whereConditions.push(`( dept_name ILIKE $${paramIndex} OR dept_code ILIKE $${paramIndex} OR location_name ILIKE $${paramIndex} )`); queryParams.push(`%${search.trim()}%`); paramIndex++; } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; const departments = await query( `SELECT dept_code, parent_dept_code, dept_name, master_sabun, master_user_id, location, location_name, regdate, data_type, status, sales_yn, company_code, company_name FROM dept_info ${whereClause} ORDER BY parent_dept_code ASC NULLS FIRST, dept_name ASC`, queryParams ); // 부서 트리 구조 생성 const deptMap = new Map(); const rootDepartments: any[] = []; // 모든 부서를 맵에 저장 departments.forEach((dept) => { deptMap.set(dept.dept_code, { deptCode: dept.dept_code, deptName: dept.dept_name, parentDeptCode: dept.parent_dept_code, masterSabun: dept.master_sabun, masterUserId: dept.master_user_id, location: dept.location, locationName: dept.location_name, regdate: dept.regdate ? new Date(dept.regdate).toISOString() : null, dataType: dept.data_type, status: dept.status || "active", salesYn: dept.sales_yn, companyCode: dept.company_code, companyName: dept.company_name, children: [], }); }); // 부서 트리 구조 생성 departments.forEach((dept) => { const deptNode = deptMap.get(dept.dept_code); if (dept.parent_dept_code && deptMap.has(dept.parent_dept_code)) { // 상위 부서가 있으면 children에 추가 const parentDept = deptMap.get(dept.parent_dept_code); parentDept.children.push(deptNode); } else { // 상위 부서가 없으면 루트 부서로 추가 rootDepartments.push(deptNode); } }); const response = { success: true, data: { departments: rootDepartments, flatList: departments.map((dept) => ({ deptCode: dept.dept_code, deptName: dept.dept_name, parentDeptCode: dept.parent_dept_code, masterSabun: dept.master_sabun, masterUserId: dept.master_user_id, location: dept.location, locationName: dept.location_name, regdate: dept.regdate ? new Date(dept.regdate).toISOString() : null, dataType: dept.data_type, status: dept.status || "active", salesYn: dept.sales_yn, companyCode: dept.company_code, companyName: dept.company_name, })), }, message: "부서 목록 조회 성공", total: departments.length, }; logger.info("부서 목록 조회 성공", { totalCount: departments.length, rootCount: rootDepartments.length, }); res.status(200).json(response); } catch (error) { logger.error("부서 목록 조회 실패", { error }); res.status(500).json({ success: false, message: "부서 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * GET /api/admin/users/:userId * 사용자 상세 조회 API * 기존 Java AdminController의 사용자 상세 조회 기능 포팅 */ export const getUserInfo = async (req: AuthenticatedRequest, res: Response) => { try { const { userId } = req.params; logger.info(`사용자 상세 조회 요청 - userId: ${userId}`, { user: req.user, }); if (!userId) { res.status(400).json({ success: false, message: "사용자 ID가 필요합니다.", error: { code: "USER_ID_REQUIRED", details: "userId parameter is required", }, }); return; } // Raw Query를 사용한 사용자 상세 정보 조회 const user = await queryOne( `SELECT * FROM user_info WHERE user_id = $1`, [userId] ); if (!user) { res.status(404).json({ success: false, message: "사용자를 찾을 수 없습니다.", error: { code: "USER_NOT_FOUND", details: `User ID: ${userId}`, }, }); return; } // 부서 정보 별도 조회 const deptInfo = user.dept_code ? await queryOne( `SELECT dept_name, parent_dept_code, location, location_name, sales_yn, company_name FROM dept_info WHERE dept_code = $1`, [user.dept_code] ) : null; // 응답 데이터 가공 const userInfo = { sabun: user.sabun, userId: user.user_id, userName: user.user_name, userNameEng: user.user_name_eng, userNameCn: user.user_name_cn, deptCode: user.dept_code, deptName: user.dept_name, positionCode: user.position_code, positionName: user.position_name, email: user.email, tel: user.tel, cellPhone: user.cell_phone, userType: user.user_type, userTypeName: user.user_type_name, regdate: user.regdate ? new Date(user.regdate).toISOString() : null, status: user.status || "active", endDate: user.end_date ? new Date(user.end_date).toISOString() : null, faxNo: user.fax_no, partnerObjid: user.partner_objid, rank: user.rank, photo: user.photo ? `data:image/jpeg;base64,${Buffer.from(user.photo).toString("base64")}` : null, locale: user.locale, companyCode: user.company_code, dataType: user.data_type, // 부서 정보 deptInfo: { deptCode: user.dept_code, deptName: deptInfo?.dept_name, parentDeptCode: deptInfo?.parent_dept_code, location: deptInfo?.location, locationName: deptInfo?.location_name, salesYn: deptInfo?.sales_yn, companyName: deptInfo?.company_name, }, }; const response = { success: true, data: userInfo, message: "사용자 상세 정보 조회 성공", }; logger.info("사용자 상세 정보 조회 성공", { userId, userName: user.user_name, }); res.status(200).json(response); } catch (error) { logger.error("사용자 상세 정보 조회 실패", { error, userId: req.params.userId, }); res.status(500).json({ success: false, message: "사용자 상세 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * POST /api/admin/users/check-duplicate * 사용자 ID 중복 체크 API * 기존 Java AdminController의 checkDuplicateUserId 기능 포팅 */ export const checkDuplicateUserId = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const { userId } = req.body; logger.info(`사용자 ID 중복 체크 요청 - userId: ${userId}`, { user: req.user, }); if (!userId) { res.status(400).json({ success: false, message: "사용자 ID가 필요합니다.", error: { code: "USER_ID_REQUIRED", details: "userId is required", }, }); return; } // Raw Query로 사용자 ID 중복 체크 const existingUser = await queryOne( `SELECT user_id FROM user_info WHERE user_id = $1`, [userId] ); const isDuplicate = !!existingUser; const count = isDuplicate ? 1 : 0; const response = { success: true, data: { isDuplicate, count, message: isDuplicate ? "이미 사용 중인 사용자 ID입니다." : "사용 가능한 사용자 ID입니다.", }, message: "사용자 ID 중복 체크 완료", }; logger.info("사용자 ID 중복 체크 완료", { userId, isDuplicate, count, }); res.status(200).json(response); } catch (error) { logger.error("사용자 ID 중복 체크 실패", { error, userId: req.body?.userId, }); res.status(500).json({ success: false, message: "사용자 ID 중복 체크 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * POST /api/admin/users * 사용자 등록/수정 API * 기존 Java AdminController의 saveUserInfo 기능 포팅 */ /** * GET /api/admin/users/:userId/history * 사용자 변경이력 조회 API * 기존 Java AdminController.getUserHistory() 포팅 */ export const getUserHistory = async ( req: AuthenticatedRequest, res: Response ) => { try { const { userId } = req.params; const { page = 1, countPerPage = 10 } = req.query; logger.info(`사용자 변경이력 조회 요청 - userId: ${userId}`, { page, countPerPage, user: req.user, }); if (!userId) { res.status(400).json({ success: false, message: "사용자 ID가 필요합니다.", error: { code: "USER_ID_REQUIRED", details: "userId 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 currentPage = Number(page); const pageSize = Number(countPerPage); const pageStart = (currentPage - 1) * pageSize + 1; const pageEnd = currentPage * pageSize; // 전체 건수 조회 쿼리 (기존 backend와 동일한 로직) const countQuery = ` SELECT CEIL(TOTAL_CNT::float / $1)::integer AS MAX_PAGE_SIZE, TOTAL_CNT FROM ( SELECT COUNT(1) AS TOTAL_CNT FROM user_info_history WHERE user_id = $2 ) A `; const countResult = await client.query(countQuery, [pageSize, userId]); const countData = countResult.rows[0] || { total_cnt: 0, max_page_size: 1, }; // 변경이력 목록 조회 쿼리 (기존 backend와 동일한 로직) const historyQuery = ` SELECT A.* FROM ( SELECT A.*, ROW_NUMBER() OVER (ORDER BY RM DESC) AS RNUM FROM ( SELECT T.*, ROW_NUMBER() OVER (ORDER BY regdate) AS RM, (SELECT user_name FROM user_info UI WHERE T.writer = UI.user_id) AS writer_name, TO_CHAR(T.regdate, 'YYYY-MM-DD HH24:MI:SS') AS reg_date_title FROM user_info_history T WHERE user_id = $1 ) A WHERE 1=1 ) A WHERE 1=1 AND RNUM::integer <= $2 AND RNUM::integer >= $3 ORDER BY RM DESC `; const historyResult = await client.query(historyQuery, [ userId, pageEnd, pageStart, ]); // 응답 데이터 가공 const historyList = historyResult.rows.map((row) => ({ sabun: row.sabun || "", userId: row.user_id || "", userName: row.user_name || "", deptCode: row.dept_code || "", deptName: row.dept_name || "", userTypeName: row.user_type_name || "", historyType: row.history_type || "", writer: row.writer || "", writerName: row.writer_name || "", regDate: row.regdate, regDateTitle: row.reg_date_title || "", status: row.status || "", rowNum: row.rnum, })); logger.info( `사용자 변경이력 조회 완료 - userId: ${userId}, 조회건수: ${historyList.length}, 전체: ${countData.total_cnt}` ); const response: ApiResponse = { success: true, data: historyList, total: Number(countData.total_cnt), pagination: { page: currentPage, limit: pageSize, total: Number(countData.total_cnt), totalPages: Number(countData.max_page_size), }, }; res.status(200).json(response); } finally { await client.end(); } } catch (error) { logger.error("사용자 변경이력 조회 중 오류 발생", error); const response: ApiResponse = { success: false, message: "사용자 변경이력 조회 중 오류가 발생했습니다.", error: { code: "USER_HISTORY_FETCH_ERROR", details: error instanceof Error ? error.message : "Unknown error", }, }; res.status(500).json(response); } }; /** * PATCH /api/admin/users/:userId/status * 사용자 상태 변경 API (부분 수정) * 기존 Java AdminController.changeUserStatus() 포팅 */ export const changeUserStatus = async ( req: AuthenticatedRequest, res: Response ) => { try { const { userId } = req.params; const { status } = req.body; logger.info("사용자 상태 변경 요청", { userId, status, user: req.user }); // 필수 파라미터 검증 if (!userId || !status) { res.status(400).json({ result: false, msg: "사용자 ID와 상태는 필수입니다.", }); return; } // 상태 값 검증 if (!["active", "inactive"].includes(status)) { res.status(400).json({ result: false, msg: "유효하지 않은 상태값입니다. (active, inactive만 허용)", }); return; } // Raw Query를 사용한 사용자 상태 변경 // 1. 사용자 존재 여부 확인 const currentUser = await queryOne( `SELECT user_id, user_name, status FROM user_info WHERE user_id = $1`, [userId] ); if (!currentUser) { res.status(404).json({ result: false, msg: "사용자를 찾을 수 없습니다.", }); return; } // 2. 상태 변경 실행 // active/inactive에 따른 END_DATE 처리 const endDate = status === "inactive" ? new Date() : null; const updateResult = await query( `UPDATE user_info SET status = $1, end_date = $2 WHERE user_id = $3 RETURNING *`, [status, endDate, userId] ); if (updateResult.length > 0) { // 사용자 이력 저장은 user_info_history 테이블이 @@ignore 상태이므로 생략 logger.info("사용자 상태 변경 성공", { userId, oldStatus: currentUser.status, newStatus: status, updatedBy: req.user?.userId, }); res.json({ result: true, msg: `사용자 상태가 ${status === "active" ? "활성" : "비활성"}으로 변경되었습니다.`, }); } else { res.status(400).json({ result: false, msg: "사용자 상태 변경에 실패했습니다.", }); } } catch (error) { logger.error("사용자 상태 변경 중 오류 발생", { error, userId: req.params.userId, status: req.body.status, }); res.status(500).json({ result: false, msg: "시스템 오류가 발생했습니다.", }); } }; export const saveUser = async (req: AuthenticatedRequest, res: Response) => { try { const userData = req.body; const isUpdate = req.method === "PUT"; // PUT 요청이면 수정 logger.info("사용자 저장 요청", { userData, user: req.user, isUpdate, method: req.method, }); // 필수 필드 검증 let requiredFields = ["userId", "userName"]; // 신규 등록 시에만 비밀번호 필수 if (!isUpdate) { requiredFields.push("userPassword"); } for (const field of requiredFields) { if (!userData[field] || userData[field].trim() === "") { res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.`, error: { code: "REQUIRED_FIELD_MISSING", details: `Required field: ${field}`, }, }); return; } } // 비밀번호 암호화 (비밀번호가 제공된 경우에만) let encryptedPassword = null; if (userData.userPassword) { encryptedPassword = await EncryptUtil.encrypt(userData.userPassword); } // Raw Query를 사용한 사용자 저장 (upsert with ON CONFLICT) const updatePasswordClause = encryptedPassword ? "user_password = $4," : ""; const [savedUser] = await query( `INSERT INTO user_info ( user_id, user_name, user_name_eng, user_password, dept_code, dept_name, position_code, position_name, email, tel, cell_phone, user_type, user_type_name, sabun, company_code, status, locale, regdate ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (user_id) DO UPDATE SET user_name = $2, user_name_eng = $3, ${updatePasswordClause} dept_code = $5, dept_name = $6, position_code = $7, position_name = $8, email = $9, tel = $10, cell_phone = $11, user_type = $12, user_type_name = $13, sabun = $14, company_code = $15, status = $16, locale = $17 RETURNING *`, [ userData.userId, userData.userName, userData.userNameEng || null, encryptedPassword || "", // 빈 문자열로 넣되, UPDATE에서는 조건부로 제외 userData.deptCode || null, userData.deptName || null, userData.positionCode || null, userData.positionName || null, userData.email || null, userData.tel || null, userData.cellPhone || null, userData.userType || null, userData.userTypeName || null, userData.sabun || null, userData.companyCode || null, userData.status || "active", userData.locale || null, new Date(), ] ); // 기존 사용자인지 새 사용자인지 확인 (regdate로 판단) const isExistingUser = savedUser.regdate && new Date(savedUser.regdate).getTime() < Date.now() - 1000; logger.info( isExistingUser ? "사용자 정보 수정 완료" : "새 사용자 등록 완료", { userId: userData.userId, } ); const response = { success: true, result: true, message: isExistingUser ? "사용자 정보가 수정되었습니다." : "사용자가 등록되었습니다.", data: { userId: userData.userId, isUpdate: isExistingUser, }, }; res.status(200).json(response); } catch (error) { logger.error("사용자 저장 실패", { error, userData: req.body }); res.status(500).json({ success: false, result: false, message: "사용자 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * POST /api/admin/companies * 회사 등록 API * 기존 Java AdminController의 회사 등록 기능 포팅 */ export const createCompany = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { logger.info("회사 등록 요청", { body: req.body, user: req.user, }); const { company_name } = req.body; // 필수 입력값 검증 if (!company_name || !company_name.trim()) { res.status(400).json({ success: false, message: "회사명을 입력해주세요.", errorCode: "COMPANY_NAME_REQUIRED", }); return; } // Raw Query로 회사명 중복 체크 const existingCompany = await queryOne( `SELECT company_code FROM company_mng WHERE company_name = $1`, [company_name.trim()] ); if (existingCompany) { res.status(400).json({ success: false, message: "이미 등록된 회사명입니다.", errorCode: "COMPANY_NAME_DUPLICATE", }); return; } // PostgreSQL 클라이언트 생성 (복잡한 코드 생성 쿼리용) const client = new Client({ connectionString: process.env.DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/ilshin", }); await client.connect(); try { // 회사 코드 생성 (COMPANY_1, COMPANY_2, ...) const codeQuery = ` SELECT COALESCE(MAX(CAST(SUBSTRING(company_code FROM 9) AS INTEGER)), 0) + 1 as next_number FROM company_mng WHERE company_code LIKE 'COMPANY_%' `; const codeResult = await client.query(codeQuery); const nextNumber = codeResult.rows[0].next_number; const companyCode = `COMPANY_${nextNumber}`; // 회사 정보 저장 const insertQuery = ` INSERT INTO company_mng ( company_code, company_name, writer, regdate, status ) VALUES ($1, $2, $3, $4, $5) RETURNING * `; const writer = req.user ? `${req.user.userName}(${req.user.userId})` : "시스템"; const insertValues = [ companyCode, company_name.trim(), writer, new Date(), "active", ]; const insertResult = await client.query(insertQuery, insertValues); const createdCompany = insertResult.rows[0]; // 회사 폴더 초기화 (파일 시스템) try { FileSystemManager.initializeCompanyFolder(createdCompany.company_code); logger.info("회사 폴더 초기화 완료", { companyCode: createdCompany.company_code, }); } catch (folderError) { logger.warn("회사 폴더 초기화 실패 (회사 등록은 성공)", { companyCode: createdCompany.company_code, error: folderError, }); } logger.info("회사 등록 성공", { companyCode: createdCompany.company_code, companyName: createdCompany.company_name, writer: createdCompany.writer, }); const response = { success: true, message: "회사가 성공적으로 등록되었습니다.", data: { company_code: createdCompany.company_code, company_name: createdCompany.company_name, writer: createdCompany.writer, regdate: createdCompany.regdate, status: createdCompany.status, }, }; res.status(201).json(response); } finally { await client.end(); } } catch (error) { logger.error("회사 등록 실패", { error, body: req.body }); res.status(500).json({ success: false, message: "회사 등록 중 오류가 발생했습니다.", errorCode: "COMPANY_CREATE_ERROR", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * PUT /api/admin/companies/:companyCode * 회사 정보 수정 API */ export const updateCompany = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const { companyCode } = req.params; const { company_name, status } = req.body; logger.info("회사 정보 수정 요청", { companyCode, body: req.body, user: req.user, }); // 필수 입력값 검증 if (!company_name || !company_name.trim()) { res.status(400).json({ success: false, message: "회사명을 입력해주세요.", errorCode: "COMPANY_NAME_REQUIRED", }); return; } // Raw Query로 회사명 중복 체크 (자기 자신 제외) const duplicateCompany = await queryOne( `SELECT company_code FROM company_mng WHERE company_name = $1 AND company_code != $2`, [company_name.trim(), companyCode] ); if (duplicateCompany) { res.status(400).json({ success: false, message: "이미 등록된 회사명입니다.", errorCode: "COMPANY_NAME_DUPLICATE", }); return; } // Raw Query로 회사 정보 수정 const result = await query( `UPDATE company_mng SET company_name = $1, status = $2 WHERE company_code = $3 RETURNING *`, [company_name.trim(), status || "active", companyCode] ); if (result.length === 0) { res.status(404).json({ success: false, message: "해당 회사를 찾을 수 없습니다.", errorCode: "COMPANY_NOT_FOUND", }); return; } const updatedCompany = result[0]; logger.info("회사 정보 수정 성공", { companyCode: updatedCompany.company_code, companyName: updatedCompany.company_name, status: updatedCompany.status, }); const response = { success: true, message: "회사 정보가 수정되었습니다.", data: { company_code: updatedCompany.company_code, company_name: updatedCompany.company_name, writer: updatedCompany.writer, regdate: updatedCompany.regdate, status: updatedCompany.status, }, }; res.status(200).json(response); } catch (error) { logger.error("회사 정보 수정 실패", { error, body: req.body }); res.status(500).json({ success: false, message: "회사 정보 수정 중 오류가 발생했습니다.", errorCode: "COMPANY_UPDATE_ERROR", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * DELETE /api/admin/companies/:companyCode * 회사 삭제 API */ export const deleteCompany = async ( req: AuthenticatedRequest, res: Response ): Promise => { try { const { companyCode } = req.params; logger.info("회사 삭제 요청", { companyCode, user: req.user, }); // Raw Query로 회사 삭제 const result = await query( `DELETE FROM company_mng WHERE company_code = $1 RETURNING company_code, company_name`, [companyCode] ); if (result.length === 0) { res.status(404).json({ success: false, message: "해당 회사를 찾을 수 없습니다.", errorCode: "COMPANY_NOT_FOUND", }); return; } const deletedCompany = result[0]; logger.info("회사 삭제 성공", { companyCode: deletedCompany.company_code, companyName: deletedCompany.company_name, }); const response = { success: true, message: "회사가 삭제되었습니다.", data: { company_code: deletedCompany.company_code, company_name: deletedCompany.company_name, }, }; res.status(200).json(response); } catch (error) { logger.error("회사 삭제 실패", { error }); res.status(500).json({ success: false, message: "회사 삭제 중 오류가 발생했습니다.", errorCode: "COMPANY_DELETE_ERROR", error: error instanceof Error ? error.message : "Unknown error", }); } }; /** * POST /api/admin/users/reset-password * 사용자 비밀번호 초기화 API * 기존 Java AdminController.resetUserPassword() 포팅 */ export const updateProfile = async ( req: AuthenticatedRequest, res: Response ) => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ result: false, error: { code: "TOKEN_MISSING", details: "인증 토큰이 필요합니다.", }, }); return; } const { userName, userNameEng, userNameCn, email, tel, cellPhone, photo, locale, } = req.body; // 업데이트할 필드와 값 준비 const updateFields: string[] = []; const updateValues: any[] = []; let paramIndex = 1; if (userName !== undefined) { updateFields.push(`user_name = $${paramIndex}`); updateValues.push(userName); paramIndex++; } if (userNameEng !== undefined) { updateFields.push(`user_name_eng = $${paramIndex}`); updateValues.push(userNameEng); paramIndex++; } if (userNameCn !== undefined) { updateFields.push(`user_name_cn = $${paramIndex}`); updateValues.push(userNameCn); paramIndex++; } if (email !== undefined) { updateFields.push(`email = $${paramIndex}`); updateValues.push(email); paramIndex++; } if (tel !== undefined) { updateFields.push(`tel = $${paramIndex}`); updateValues.push(tel); paramIndex++; } if (cellPhone !== undefined) { updateFields.push(`cell_phone = $${paramIndex}`); updateValues.push(cellPhone); paramIndex++; } // photo 데이터 처리 (Base64를 Buffer로 변환하여 저장) if (photo !== undefined) { if (photo && typeof photo === "string") { try { // Base64 헤더 제거 (data:image/jpeg;base64, 등) const base64Data = photo.replace(/^data:image\/[a-z]+;base64,/, ""); // Base64를 Buffer로 변환 updateFields.push(`photo = $${paramIndex}`); updateValues.push(Buffer.from(base64Data, "base64")); paramIndex++; } catch (error) { console.error("Base64 이미지 처리 오류:", error); updateFields.push(`photo = $${paramIndex}`); updateValues.push(null); paramIndex++; } } else { updateFields.push(`photo = $${paramIndex}`); updateValues.push(null); paramIndex++; } } if (locale !== undefined) { updateFields.push(`locale = $${paramIndex}`); updateValues.push(locale); paramIndex++; } // 업데이트할 데이터가 없으면 에러 if (updateFields.length === 0) { res.status(400).json({ result: false, error: { code: "NO_DATA", details: "업데이트할 데이터가 없습니다.", }, }); return; } // Raw Query로 데이터베이스 업데이트 updateValues.push(userId); await query( `UPDATE user_info SET ${updateFields.join(", ")} WHERE user_id = $${paramIndex}`, updateValues ); // 업데이트된 사용자 정보 조회 const updatedUser = await queryOne( `SELECT user_id, user_name, user_name_eng, user_name_cn, dept_code, dept_name, position_code, position_name, email, tel, cell_phone, user_type, user_type_name, photo, locale FROM user_info WHERE user_id = $1`, [userId] ); // photo가 Buffer 타입인 경우 Base64로 변환 const responseData = { ...updatedUser, photo: updatedUser?.photo ? `data:image/jpeg;base64,${Buffer.from(updatedUser.photo).toString("base64")}` : null, }; res.json({ result: true, message: "프로필이 성공적으로 업데이트되었습니다.", data: responseData, }); } catch (error) { console.error("프로필 업데이트 오류:", error); res.status(500).json({ result: false, error: { code: "UPDATE_FAILED", details: "프로필 업데이트 중 오류가 발생했습니다.", }, }); } }; export const resetUserPassword = async ( req: AuthenticatedRequest, res: Response ): Promise => { const { userId, newPassword } = req.body; logger.info("비밀번호 초기화 요청", { userId, user: req.user }); // 입력값 검증 if (!userId || !userId.trim()) { res.status(400).json({ result: false, msg: "사용자 ID가 필요합니다.", }); return; } if (!newPassword || !newPassword.trim()) { res.status(400).json({ success: false, result: false, message: "새 비밀번호가 필요합니다.", msg: "새 비밀번호가 필요합니다.", }); return; } // 비밀번호 길이 검증 (최소 4자) if (newPassword.length < 4) { res.status(400).json({ success: false, result: false, message: "비밀번호는 최소 4자 이상이어야 합니다.", msg: "비밀번호는 최소 4자 이상이어야 합니다.", }); return; } try { // 1. Raw Query로 사용자 존재 여부 확인 const currentUser = await queryOne( `SELECT user_id, user_name FROM user_info WHERE user_id = $1`, [userId] ); if (!currentUser) { res.status(404).json({ success: false, result: false, message: "사용자를 찾을 수 없습니다.", msg: "사용자를 찾을 수 없습니다.", }); return; } // 2. 비밀번호 암호화 (기존 Java 로직과 동일) let encryptedPassword: string; try { // EncryptUtil과 동일한 암호화 사용 const crypto = require("crypto"); const keyName = "ILJIAESSECRETKEY"; const algorithm = "aes-128-ecb"; // AES-128-ECB 암호화 const cipher = crypto.createCipher(algorithm, keyName); let encrypted = cipher.update(newPassword, "utf8", "hex"); encrypted += cipher.final("hex"); encryptedPassword = encrypted.toUpperCase(); } catch (encryptError) { logger.error("비밀번호 암호화 중 오류 발생", { error: encryptError, userId, }); res.status(500).json({ success: false, result: false, message: "비밀번호 암호화 중 오류가 발생했습니다.", msg: "비밀번호 암호화 중 오류가 발생했습니다.", }); return; } // 3. Raw Query로 비밀번호 업데이트 실행 const updateResult = await query( `UPDATE user_info SET user_password = $1 WHERE user_id = $2 RETURNING *`, [encryptedPassword, userId] ); if (updateResult.length > 0) { // 이력 저장은 user_info_history 테이블이 @@ignore 상태이므로 생략 logger.info("비밀번호 초기화 성공", { userId, updatedBy: req.user?.userId, }); res.json({ success: true, result: true, message: "비밀번호가 성공적으로 초기화되었습니다.", msg: "비밀번호가 성공적으로 초기화되었습니다.", }); } else { res.status(400).json({ success: false, result: false, message: "사용자 정보를 찾을 수 없거나 비밀번호 변경에 실패했습니다.", msg: "사용자 정보를 찾을 수 없거나 비밀번호 변경에 실패했습니다.", }); } } catch (error) { logger.error("비밀번호 초기화 중 오류 발생", { error, userId, }); res.status(500).json({ success: false, result: false, message: "비밀번호 초기화 중 시스템 오류가 발생했습니다.", msg: "비밀번호 초기화 중 시스템 오류가 발생했습니다.", }); } };