ERP-node/backend-node/src/controllers/adminController.ts

2559 lines
69 KiB
TypeScript

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 { PrismaClient } from "@prisma/client";
import config from "../config/environment";
import { AdminService } from "../services/adminService";
import { EncryptUtil } from "../utils/encryptUtil";
const prisma = new PrismaClient();
/**
* 관리자 메뉴 목록 조회
*/
export async function getAdminMenus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("=== 관리자 메뉴 목록 조회 시작 ===");
// 현재 로그인한 사용자의 회사 코드와 로케일 가져오기
const userCompanyCode = req.user?.companyCode || "ILSHIN";
const userLang = (req.query.userLang as string) || "ko";
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
logger.info(`사용자 로케일: ${userLang}`);
const paramMap = {
userCompanyCode,
userLang,
SYSTEM_NAME: "PLM",
};
const menuList = await AdminService.getAdminMenuList(paramMap);
logger.info(`관리자 메뉴 조회 결과: ${menuList.length}`);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", menuList[0]);
}
const response: ApiResponse<any[]> = {
success: true,
message: "관리자 메뉴 목록 조회 성공",
data: menuList,
};
res.status(200).json(response);
} catch (error) {
logger.error("관리자 메뉴 목록 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
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<void> {
try {
logger.info("=== 사용자 메뉴 목록 조회 시작 ===");
// 현재 로그인한 사용자의 회사 코드와 로케일 가져오기
const userCompanyCode = req.user?.companyCode || "ILSHIN";
const userLang = (req.query.userLang as string) || "ko";
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
logger.info(`사용자 로케일: ${userLang}`);
const paramMap = {
userCompanyCode,
userLang,
SYSTEM_NAME: "PLM",
};
const menuList = await AdminService.getUserMenuList(paramMap);
logger.info(`사용자 메뉴 조회 결과: ${menuList.length}`);
if (menuList.length > 0) {
logger.info("첫 번째 메뉴:", menuList[0]);
}
const response: ApiResponse<any[]> = {
success: true,
message: "사용자 메뉴 목록 조회 성공",
data: menuList,
};
res.status(200).json(response);
} catch (error) {
logger.error("사용자 메뉴 목록 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
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<void> {
try {
const { menuId } = req.params;
logger.info(`=== 메뉴 정보 조회 시작 - menuId: ${menuId} ===`);
const menuInfo = await AdminService.getMenuInfo(menuId);
if (!menuInfo) {
const response: ApiResponse<null> = {
success: false,
message: "메뉴를 찾을 수 없습니다.",
error: {
code: "MENU_NOT_FOUND",
details: `Menu ID: ${menuId}`,
},
};
res.status(404).json(response);
return;
}
const response: ApiResponse<any> = {
success: true,
message: "메뉴 정보 조회 성공",
data: menuInfo,
};
res.status(200).json(response);
} catch (error) {
logger.error("메뉴 정보 조회 중 오류 발생:", error);
const response: ApiResponse<null> = {
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,
} = req.query;
// Prisma ORM을 사용한 사용자 목록 조회
let whereConditions: any = {};
let searchType = "none";
// 검색 조건 처리
if (search && typeof search === "string" && search.trim()) {
// 통합 검색
searchType = "unified";
const searchTerm = search.trim();
whereConditions.OR = [
{ sabun: { contains: searchTerm, mode: "insensitive" } },
{ user_type_name: { contains: searchTerm, mode: "insensitive" } },
{ dept_name: { contains: searchTerm, mode: "insensitive" } },
{ position_name: { contains: searchTerm, mode: "insensitive" } },
{ user_id: { contains: searchTerm, mode: "insensitive" } },
{ user_name: { contains: searchTerm, mode: "insensitive" } },
{ tel: { contains: searchTerm, mode: "insensitive" } },
{ cell_phone: { contains: searchTerm, mode: "insensitive" } },
{ email: { contains: searchTerm, mode: "insensitive" } },
];
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.OR = [
{ tel: { contains: searchValue as string, mode: "insensitive" } },
{
cell_phone: {
contains: searchValue as string,
mode: "insensitive",
},
},
];
} else {
whereConditions[fieldMap[searchField as string]] = {
contains: searchValue as string,
mode: "insensitive",
};
}
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[field] = {
contains: param.trim(),
mode: "insensitive",
};
hasAdvancedSearch = true;
}
}
// 전화번호 검색
if (search_tel && typeof search_tel === "string" && search_tel.trim()) {
whereConditions.OR = [
{ tel: { contains: search_tel.trim(), mode: "insensitive" } },
{ cell_phone: { contains: search_tel.trim(), mode: "insensitive" } },
];
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 (deptCode) {
whereConditions.dept_code = deptCode as string;
}
if (status) {
whereConditions.status = status as string;
}
// 총 개수 조회
const totalCount = await prisma.user_info.count({
where: whereConditions,
});
// 사용자 목록 조회
const users = await prisma.user_info.findMany({
where: whereConditions,
orderBy: [{ regdate: "desc" }, { user_name: "asc" }],
skip: (Number(page) - 1) * Number(countPerPage),
take: Number(countPerPage),
select: {
sabun: true,
user_id: true,
user_name: true,
user_name_eng: 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,
regdate: true,
status: true,
company_code: true,
locale: true,
},
});
// 응답 데이터 가공
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 ? user.regdate.toISOString().split("T")[0] : null,
}));
const response = {
success: true,
data: processedUsers,
total: totalCount,
searchType,
pagination: {
page: Number(page),
limit: Number(countPerPage),
totalPages: Math.ceil(totalCount / Number(countPerPage)),
},
message: "사용자 목록 조회 성공",
};
logger.info("사용자 목록 조회 성공", {
totalCount,
returnedCount: processedUsers.length,
searchType,
currentPage: Number(page),
countPerPage: Number(countPerPage),
});
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<void> => {
try {
logger.info("사용자 로케일 조회 요청", {
query: req.query,
user: req.user,
});
if (!req.user?.userId) {
res.status(400).json({
success: false,
message: "사용자 정보가 없습니다.",
});
return;
}
// 데이터베이스에서 사용자 로케일 조회
const userInfo = await prisma.user_info.findFirst({
where: {
user_id: req.user.userId,
},
select: {
locale: true,
},
});
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<void> => {
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;
}
// 데이터베이스에 사용자 로케일 저장
await prisma.user_info.update({
where: {
user_id: req.user.userId,
},
data: {
locale: locale,
},
});
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,
});
// Prisma ORM을 사용한 회사 목록 조회
const companies = await prisma.company_mng.findMany({
where: {
OR: [{ status: "active" }, { status: null }],
},
orderBy: {
company_name: "asc",
},
select: {
company_code: true,
company_name: true,
status: true,
writer: true,
regdate: true,
},
});
// 프론트엔드에서 기대하는 응답 형식으로 변환
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
? 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<void> {
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<any[]> = {
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<void> {
try {
logger.info("다국어 키 목록 조회 요청", {
query: req.query,
user: req.user,
});
// Prisma ORM을 사용한 다국어 키 목록 조회
const result = await prisma.multi_lang_key_master.findMany({
orderBy: [
{ company_code: "asc" },
{ menu_name: "asc" },
{ lang_key: "asc" },
],
select: {
key_id: true,
company_code: true,
menu_name: true,
lang_key: true,
description: true,
is_active: true,
created_date: true,
created_by: true,
updated_date: true,
updated_by: true,
},
});
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?.toISOString(),
createdBy: row.created_by,
updatedDate: row.updated_date?.toISOString(),
updatedBy: row.updated_by,
}));
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response: ApiResponse<any[]> = {
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<void> {
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<any[]> = {
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<void> {
try {
const { keyId } = req.params;
const textData = req.body;
logger.info(`다국어 텍스트 저장 요청: keyId = ${keyId}`, { textData });
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const keyData = req.body;
logger.info("다국어 키 저장 요청", { keyData });
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const { keyId } = req.params;
const keyData = req.body;
logger.info(`다국어 키 수정 요청: keyId = ${keyId}`, { keyData });
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const { keyId } = req.params;
logger.info(`다국어 키 삭제 요청: keyId = ${keyId}`);
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const { keyId } = req.params;
logger.info(`다국어 키 상태 토글 요청: keyId = ${keyId}`);
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const langData = req.body;
logger.info("언어 저장 요청", { langData });
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const { langCode } = req.params;
const langData = req.body;
logger.info(`언어 수정 요청: langCode = ${langCode}`, { langData });
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const { langCode } = req.params;
logger.info(`언어 상태 토글 요청: langCode = ${langCode}`);
// 더미 응답
const response: ApiResponse<any> = {
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<void> {
try {
const menuData = req.body;
logger.info("메뉴 저장 요청", { menuData, user: req.user });
// Prisma ORM을 사용한 메뉴 저장
const savedMenu = await prisma.menu_info.create({
data: {
objid: Date.now(), // 고유 ID 생성
menu_type: menuData.menuType ? Number(menuData.menuType) : null,
parent_obj_id: menuData.parentObjId
? Number(menuData.parentObjId)
: null,
menu_name_kor: menuData.menuNameKor,
menu_name_eng: menuData.menuNameEng || null,
seq: menuData.seq ? Number(menuData.seq) : null,
menu_url: menuData.menuUrl || null,
menu_desc: menuData.menuDesc || null,
writer: req.user?.userId || "admin",
regdate: new Date(),
status: menuData.status || "active",
system_name: menuData.systemName || "PLM",
company_code: menuData.companyCode || "*",
lang_key: menuData.langKey || null,
lang_key_desc: menuData.langKeyDesc || null,
},
});
logger.info("메뉴 저장 성공", { savedMenu });
const response: ApiResponse<any> = {
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: savedMenu.regdate,
},
};
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<void> {
try {
const { menuId } = req.params;
const menuData = req.body;
logger.info(`메뉴 수정 요청: menuId = ${menuId}`, {
menuData,
user: req.user,
});
// Prisma ORM을 사용한 메뉴 수정
const updatedMenu = await prisma.menu_info.update({
where: {
objid: Number(menuId),
},
data: {
menu_type: menuData.menuType ? Number(menuData.menuType) : null,
parent_obj_id: menuData.parentObjId
? Number(menuData.parentObjId)
: null,
menu_name_kor: menuData.menuNameKor,
menu_name_eng: menuData.menuNameEng || null,
seq: menuData.seq ? Number(menuData.seq) : null,
menu_url: menuData.menuUrl || null,
menu_desc: menuData.menuDesc || null,
status: menuData.status || "active",
system_name: menuData.systemName || "PLM",
company_code: menuData.companyCode || "*",
lang_key: menuData.langKey || null,
lang_key_desc: menuData.langKeyDesc || null,
},
});
logger.info("메뉴 수정 성공", { updatedMenu });
const response: ApiResponse<any> = {
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: updatedMenu.regdate,
},
};
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<void> {
try {
const { menuId } = req.params;
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
// Prisma ORM을 사용한 메뉴 삭제
const deletedMenu = await prisma.menu_info.delete({
where: {
objid: Number(menuId),
},
});
logger.info("메뉴 삭제 성공", { deletedMenu });
const response: ApiResponse<any> = {
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: deletedMenu.regdate,
},
};
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<void> {
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;
}
// Prisma ORM을 사용한 메뉴 일괄 삭제
let deletedCount = 0;
let failedCount = 0;
const deletedMenus: any[] = [];
const failedMenuIds: string[] = [];
// 각 메뉴 ID에 대해 삭제 시도
for (const menuId of menuIds) {
try {
const deletedMenu = await prisma.menu_info.delete({
where: {
objid: Number(menuId),
},
});
if (deletedMenu) {
deletedCount++;
deletedMenus.push({
...deletedMenu,
objid: deletedMenu.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<any> = {
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<void> {
try {
logger.info("회사 목록 조회 요청 (Prisma)", { user: req.user });
// Prisma ORM으로 회사 목록 조회
const companies = await prisma.company_mng.findMany({
select: {
company_code: true,
company_name: true,
writer: true,
regdate: true,
status: true,
},
orderBy: {
regdate: "desc",
},
});
logger.info("회사 목록 조회 성공 (Prisma)", { count: companies.length });
const response: ApiResponse<any> = {
success: true,
message: "회사 목록 조회 성공",
data: companies,
total: companies.length,
};
res.status(200).json(response);
} catch (error) {
logger.error("회사 목록 조회 실패 (Prisma):", 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;
// Prisma ORM을 사용한 부서 목록 조회
const whereConditions: any = {};
// 회사 코드 필터
if (companyCode) {
whereConditions.company_name = companyCode;
}
// 상태 필터
if (status) {
whereConditions.status = status;
}
// 검색 조건
if (search) {
whereConditions.OR = [
{ dept_name: { contains: search as string, mode: "insensitive" } },
{ dept_code: { contains: search as string, mode: "insensitive" } },
{ location_name: { contains: search as string, mode: "insensitive" } },
];
}
const departments = await prisma.dept_info.findMany({
where: whereConditions,
orderBy: [{ parent_dept_code: "asc" }, { dept_name: "asc" }],
select: {
dept_code: true,
parent_dept_code: true,
dept_name: true,
master_sabun: true,
master_user_id: true,
location: true,
location_name: true,
regdate: true,
data_type: true,
status: true,
sales_yn: true,
company_name: true,
},
});
// 부서 트리 구조 생성
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 ? dept.regdate.toISOString() : null,
dataType: dept.data_type,
status: dept.status || "active",
salesYn: dept.sales_yn,
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 ? dept.regdate.toISOString() : null,
dataType: dept.data_type,
status: dept.status || "active",
salesYn: dept.sales_yn,
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;
}
// Prisma ORM을 사용한 사용자 상세 정보 조회
const user = await prisma.user_info.findUnique({
where: {
user_id: 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 prisma.dept_info.findUnique({
where: {
dept_code: user.dept_code,
},
select: {
dept_name: true,
parent_dept_code: true,
location: true,
location_name: true,
sales_yn: true,
company_name: true,
},
})
: 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 ? user.regdate.toISOString() : null,
status: user.status || "active",
endDate: user.end_date ? user.end_date.toISOString() : null,
faxNo: user.fax_no,
partnerObjid: user.partner_objid,
rank: user.rank,
photo: user.photo
? `data:image/jpeg;base64,${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<void> => {
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;
}
// Prisma ORM으로 사용자 ID 중복 체크
const existingUser = await prisma.user_info.findUnique({
where: {
user_id: userId,
},
select: {
user_id: true,
},
});
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<any[]> = {
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<null> = {
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;
}
// Prisma ORM을 사용한 사용자 상태 변경
// 1. 사용자 존재 여부 확인
const currentUser = await prisma.user_info.findUnique({
where: {
user_id: userId,
},
select: {
user_id: true,
user_name: true,
status: true,
},
});
if (!currentUser) {
res.status(404).json({
result: false,
msg: "사용자를 찾을 수 없습니다.",
});
return;
}
// 2. 상태 변경 데이터 준비
let updateData: any = {
status: status,
};
// active/inactive에 따른 END_DATE 처리
if (status === "inactive") {
updateData.end_date = new Date();
} else if (status === "active") {
updateData.end_date = null;
}
// 3. Prisma ORM으로 상태 변경 실행
const updateResult = await prisma.user_info.update({
where: {
user_id: userId,
},
data: updateData,
});
if (updateResult) {
// 사용자 이력 저장은 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;
logger.info("사용자 저장 요청", { userData, user: req.user });
// 필수 필드 검증
const requiredFields = ["userId", "userName", "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;
}
}
// 비밀번호 암호화
const encryptedPassword = await EncryptUtil.encrypt(userData.userPassword);
// Prisma ORM을 사용한 사용자 저장 (upsert)
const savedUser = await prisma.user_info.upsert({
where: {
user_id: userData.userId,
},
create: {
user_id: userData.userId,
user_name: userData.userName,
user_name_eng: userData.userNameEng || null,
user_password: encryptedPassword,
dept_code: userData.deptCode || null,
dept_name: userData.deptName || null,
position_code: userData.positionCode || null,
position_name: userData.positionName || null,
email: userData.email || null,
tel: userData.tel || null,
cell_phone: userData.cellPhone || null,
user_type: userData.userType || null,
user_type_name: userData.userTypeName || null,
sabun: userData.sabun || null,
company_code: userData.companyCode || null,
status: userData.status || "active",
locale: userData.locale || null,
regdate: new Date(),
},
update: {
user_name: userData.userName,
user_name_eng: userData.userNameEng || null,
user_password: encryptedPassword,
dept_code: userData.deptCode || null,
dept_name: userData.deptName || null,
position_code: userData.positionCode || null,
position_name: userData.positionName || null,
email: userData.email || null,
tel: userData.tel || null,
cell_phone: userData.cellPhone || null,
user_type: userData.userType || null,
user_type_name: userData.userTypeName || null,
sabun: userData.sabun || null,
company_code: userData.companyCode || null,
status: userData.status || "active",
locale: userData.locale || null,
},
});
// 기존 사용자인지 새 사용자인지 확인
const isUpdate =
(await prisma.user_info.count({
where: { user_id: userData.userId },
})) > 0;
logger.info(isUpdate ? "사용자 정보 수정 완료" : "새 사용자 등록 완료", {
userId: userData.userId,
});
const response = {
success: true,
result: true,
message: isUpdate
? "사용자 정보가 수정되었습니다."
: "사용자가 등록되었습니다.",
data: {
userId: userData.userId,
isUpdate,
},
};
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<void> => {
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;
}
// Prisma ORM으로 회사명 중복 체크
const existingCompany = await prisma.company_mng.findFirst({
where: {
company_name: 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];
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<void> => {
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;
}
// Prisma ORM으로 회사명 중복 체크 (자기 자신 제외)
const duplicateCompany = await prisma.company_mng.findFirst({
where: {
company_name: company_name.trim(),
company_code: {
not: companyCode,
},
},
});
if (duplicateCompany) {
res.status(400).json({
success: false,
message: "이미 등록된 회사명입니다.",
errorCode: "COMPANY_NAME_DUPLICATE",
});
return;
}
// Prisma ORM으로 회사 정보 수정
try {
const updatedCompany = await prisma.company_mng.update({
where: {
company_code: companyCode,
},
data: {
company_name: company_name.trim(),
status: status || "active",
},
});
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 (updateError: any) {
if (updateError.code === "P2025") {
// Prisma error code for "Record to update not found"
res.status(404).json({
success: false,
message: "해당 회사를 찾을 수 없습니다.",
errorCode: "COMPANY_NOT_FOUND",
});
return;
}
throw updateError;
}
} 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<void> => {
try {
const { companyCode } = req.params;
logger.info("회사 삭제 요청", {
companyCode,
user: req.user,
});
// Prisma ORM으로 회사 존재 여부 확인
const existingCompany = await prisma.company_mng.findUnique({
where: {
company_code: companyCode,
},
select: {
company_code: true,
company_name: true,
},
});
if (!existingCompany) {
res.status(404).json({
success: false,
message: "해당 회사를 찾을 수 없습니다.",
errorCode: "COMPANY_NOT_FOUND",
});
return;
}
// Prisma ORM으로 회사 삭제
await prisma.company_mng.delete({
where: {
company_code: companyCode,
},
});
logger.info("회사 삭제 성공", {
companyCode,
companyName: existingCompany.company_name,
});
const response = {
success: true,
message: "회사가 삭제되었습니다.",
data: {
company_code: companyCode,
company_name: existingCompany.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 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;
// 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로 변환
updateData.photo = Buffer.from(base64Data, "base64");
} catch (error) {
console.error("Base64 이미지 처리 오류:", error);
updateData.photo = null;
}
} else {
updateData.photo = null; // 빈 값이면 null로 설정
}
}
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,
},
});
// photo가 Buffer 타입인 경우 Base64로 변환
const responseData = {
...updatedUser,
photo: updatedUser?.photo
? `data:image/jpeg;base64,${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<void> => {
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. Prisma ORM으로 사용자 존재 여부 확인
const currentUser = await prisma.user_info.findUnique({
where: {
user_id: userId,
},
select: {
user_id: true,
user_name: true,
},
});
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. Prisma ORM으로 비밀번호 업데이트 실행
const updateResult = await prisma.user_info.update({
where: {
user_id: userId,
},
data: {
user_password: encryptedPassword,
},
});
if (updateResult) {
// 이력 저장은 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: "비밀번호 초기화 중 시스템 오류가 발생했습니다.",
});
}
};