From 00ce90a9f0ecb4feab9fd2c614c834a426c8c80d Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Wed, 27 Aug 2025 17:32:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/adminController.ts | 106 +++++++++++++++++- backend-node/src/routes/adminRoutes.ts | 2 + frontend/hooks/useProfile.ts | 60 +++++----- 3 files changed, 132 insertions(+), 36 deletions(-) diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 2ebfcb28..667637c8 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -3,10 +3,13 @@ import { logger } from "../utils/logger"; import { AuthenticatedRequest } from "../types/auth"; import { ApiResponse } from "../types/common"; import { Client } from "pg"; +import { PrismaClient } from "@prisma/client"; import config from "../config/environment"; import { AdminService } from "../services/adminService"; import { EncryptUtil } from "../utils/encryptUtil"; +const prisma = new PrismaClient(); + /** * 관리자 메뉴 목록 조회 */ @@ -347,10 +350,9 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => { const countParams = [...queryParams]; // 총 개수 조회를 위해 기존 쿼리를 COUNT로 변환 - const countQuery = query.replace( - /SELECT[\s\S]*?FROM/i, - "SELECT COUNT(*) as total FROM" - ).replace(/ORDER BY.*$/i, ""); + const countQuery = query + .replace(/SELECT[\s\S]*?FROM/i, "SELECT COUNT(*) as total FROM") + .replace(/ORDER BY.*$/i, ""); const countResult = await client.query(countQuery, countParams); const totalCount = parseInt(countResult.rows[0].total); @@ -2686,6 +2688,102 @@ export const deleteCompany = async ( * 사용자 비밀번호 초기화 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 updateData: any = {}; + if (userName !== undefined) updateData.user_name = userName; + if (userNameEng !== undefined) updateData.user_name_eng = userNameEng; + if (userNameCn !== undefined) updateData.user_name_cn = userNameCn; + if (email !== undefined) updateData.email = email; + if (tel !== undefined) updateData.tel = tel; + if (cellPhone !== undefined) updateData.cell_phone = cellPhone; + if (photo !== undefined) updateData.photo = photo; + if (locale !== undefined) updateData.locale = locale; + + // 업데이트할 데이터가 없으면 에러 + if (Object.keys(updateData).length === 0) { + res.status(400).json({ + result: false, + error: { + code: "NO_DATA", + details: "업데이트할 데이터가 없습니다.", + }, + }); + return; + } + + // 데이터베이스 업데이트 + await prisma.user_info.update({ + where: { user_id: userId }, + data: updateData, + }); + + // 업데이트된 사용자 정보 조회 + const updatedUser = await prisma.user_info.findUnique({ + where: { user_id: userId }, + select: { + user_id: true, + user_name: true, + user_name_eng: true, + user_name_cn: true, + dept_code: true, + dept_name: true, + position_code: true, + position_name: true, + email: true, + tel: true, + cell_phone: true, + user_type: true, + user_type_name: true, + photo: true, + locale: true, + }, + }); + + res.json({ + result: true, + message: "프로필이 성공적으로 업데이트되었습니다.", + data: updatedUser, + }); + } catch (error) { + console.error("프로필 업데이트 오류:", error); + res.status(500).json({ + result: false, + error: { + code: "UPDATE_FAILED", + details: "프로필 업데이트 중 오류가 발생했습니다.", + }, + }); + } +}; + export const resetUserPassword = async ( req: AuthenticatedRequest, res: Response diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index afdfb284..895b96e9 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -12,6 +12,7 @@ import { getUserHistory, // 사용자 변경이력 조회 changeUserStatus, // 사용자 상태 변경 resetUserPassword, // 사용자 비밀번호 초기화 + updateProfile, // 프로필 수정 getDepartmentList, // 부서 목록 조회 checkDuplicateUserId, // 사용자 ID 중복 체크 saveUser, // 사용자 등록/수정 @@ -45,6 +46,7 @@ router.get("/users/:userId", getUserInfo); // 사용자 상세 조회 router.get("/users/:userId/history", getUserHistory); // 사용자 변경이력 조회 router.patch("/users/:userId/status", changeUserStatus); // 사용자 상태 변경 router.post("/users", saveUser); // 사용자 등록/수정 +router.put("/profile", updateProfile); // 프로필 수정 router.post("/users/check-duplicate", checkDuplicateUserId); // 사용자 ID 중복 체크 router.post("/users/reset-password", resetUserPassword); // 사용자 비밀번호 초기화 diff --git a/frontend/hooks/useProfile.ts b/frontend/hooks/useProfile.ts index ed9f0528..adfe8d26 100644 --- a/frontend/hooks/useProfile.ts +++ b/frontend/hooks/useProfile.ts @@ -3,6 +3,7 @@ import { useState, useCallback } from "react"; import { ProfileFormData, ProfileModalState } from "@/types/profile"; import { LAYOUT_CONFIG, MESSAGES } from "@/constants/layout"; +import { apiCall } from "@/lib/api/client"; // 알림 모달 상태 타입 interface AlertModalState { @@ -181,48 +182,43 @@ export const useProfile = (user: any, refreshUserData: () => Promise) => { photoData = modalState.selectedImage; } - // 사용자 정보 저장 - const userSaveData = { - userId: user.userId, + // 사용자 정보 저장 데이터 준비 + const updateData = { userName: modalState.formData.userName, email: modalState.formData.email, - deptName: modalState.formData.deptName, - positionName: modalState.formData.positionName, locale: modalState.formData.locale, - photo: photoData, + photo: photoData !== user.photo ? photoData : undefined, // 변경된 경우만 전송 }; - console.log("사용자 정보 저장 요청:", userSaveData); + console.log("프로필 업데이트 요청:", updateData); - const userResponse = await fetch(`${LAYOUT_CONFIG.API_BASE_URL}${LAYOUT_CONFIG.ENDPOINTS.USER_SAVE}`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userSaveData), - }); + // API 호출 (JWT 토큰 자동 포함) + const response = await apiCall("PUT", "/admin/profile", updateData); - if (userResponse.ok) { - const userResult = await userResponse.json(); - console.log("사용자 정보 저장 응답:", userResult); + console.log("프로필 업데이트 응답:", response); - if (userResult.result) { - // 성공: 세션 정보 새로고침 - await refreshUserData(); - setModalState((prev) => ({ - ...prev, - selectedFile: null, - isOpen: false, - })); - showAlert("저장 완료", MESSAGES.PROFILE_SAVE_SUCCESS, "success"); - } else { - throw new Error(userResult.msg || "사용자 정보 저장 실패"); + if (response.result) { + // locale이 변경된 경우 전역 변수와 localStorage 업데이트 + if (modalState.formData.locale && modalState.formData.locale !== user.locale) { + if (typeof window !== "undefined") { + // 전역 변수 업데이트 + (window as any).__GLOBAL_USER_LANG = modalState.formData.locale; + // localStorage 업데이트 + localStorage.setItem("userLocale", modalState.formData.locale); + console.log("🌍 사용자 locale 업데이트:", modalState.formData.locale); + } } + + // 성공: 사용자 정보 새로고침 + await refreshUserData(); + setModalState((prev) => ({ + ...prev, + selectedFile: null, + isOpen: false, + })); + showAlert("저장 완료", "프로필이 성공적으로 업데이트되었습니다.", "success"); } else { - const errorText = await userResponse.text(); - console.error("API 응답 오류:", errorText); - throw new Error(`사용자 정보 저장 실패: ${userResponse.status} ${userResponse.statusText}`); + throw new Error(response.message || "프로필 업데이트 실패"); } } catch (error) { console.error("프로필 저장 실패:", error);