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

3801 lines
107 KiB
TypeScript
Raw Normal View History

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, getPool } from "../database/db";
2025-08-26 09:56:45 +09:00
import config from "../config/environment";
2025-08-21 09:41:46 +09:00
import { AdminService } from "../services/adminService";
2025-08-25 13:12:17 +09:00
import { EncryptUtil } from "../utils/encryptUtil";
2025-09-05 14:52:10 +09:00
import { FileSystemManager } from "../utils/fileSystemManager";
import { validateBusinessNumber } from "../utils/businessNumberValidator";
import { MenuCopyService } from "../services/menuCopyService";
2025-08-21 09:41:46 +09:00
/**
*
*/
export async function getAdminMenus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
2025-10-27 16:58:43 +09:00
logger.info("=== 관리자 메뉴 목록 조회 시작 ===");
2025-08-21 09:41:46 +09:00
2025-10-27 16:58:43 +09:00
// 현재 로그인한 사용자의 정보 가져오기
const userId = req.user?.userId;
2025-08-21 09:41:46 +09:00
const userCompanyCode = req.user?.companyCode || "ILSHIN";
2025-10-27 16:58:43 +09:00
const userType = req.user?.userType;
2025-08-21 09:41:46 +09:00
const userLang = (req.query.userLang as string) || "ko";
2025-10-13 19:18:01 +09:00
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
const includeInactive = req.query.includeInactive === "true"; // includeInactive 파라미터 추가
2025-08-21 09:41:46 +09:00
2025-10-27 16:58:43 +09:00
logger.info(`사용자 ID: ${userId}`);
2025-08-21 09:41:46 +09:00
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
2025-10-27 16:58:43 +09:00
logger.info(`사용자 유형: ${userType}`);
2025-08-21 09:41:46 +09:00
logger.info(`사용자 로케일: ${userLang}`);
2025-10-13 19:18:01 +09:00
logger.info(`메뉴 타입: ${menuType || "전체"}`);
logger.info(`비활성 메뉴 포함: ${includeInactive}`);
2025-08-21 09:41:46 +09:00
const paramMap = {
2025-10-27 16:58:43 +09:00
userId,
2025-08-21 09:41:46 +09:00
userCompanyCode,
2025-10-27 16:58:43 +09:00
userType,
2025-08-21 09:41:46 +09:00
userLang,
menuType, // includeInactive와 관계없이 menuType 유지 (관리자/사용자 구분)
includeInactive, // includeInactive 추가
2025-08-21 09:41:46 +09:00
};
const menuList = await AdminService.getAdminMenuList(paramMap);
2025-10-13 19:18:01 +09:00
logger.info(
2025-10-27 16:58:43 +09:00
`관리자 메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"}, 회사: ${userCompanyCode})`
2025-10-13 19:18:01 +09:00
);
2025-08-21 09:41:46 +09:00
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("=== 사용자 메뉴 목록 조회 시작 ===");
2025-10-27 16:58:43 +09:00
// 현재 로그인한 사용자의 정보 가져오기
const userId = req.user?.userId;
2025-08-21 09:41:46 +09:00
const userCompanyCode = req.user?.companyCode || "ILSHIN";
2025-10-27 16:58:43 +09:00
const userType = req.user?.userType;
2025-08-21 09:41:46 +09:00
const userLang = (req.query.userLang as string) || "ko";
2025-10-27 16:58:43 +09:00
logger.info(`사용자 ID: ${userId}`);
2025-08-21 09:41:46 +09:00
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
2025-10-27 16:58:43 +09:00
logger.info(`사용자 유형: ${userType}`);
2025-08-21 09:41:46 +09:00
logger.info(`사용자 로케일: ${userLang}`);
const paramMap = {
2025-10-27 16:58:43 +09:00
userId,
2025-08-21 09:41:46 +09:00
userCompanyCode,
2025-10-27 16:58:43 +09:00
userType,
2025-08-21 09:41:46 +09:00
userLang,
};
const menuList = await AdminService.getUserMenuList(paramMap);
2025-10-27 16:58:43 +09:00
logger.info(
`사용자 메뉴 조회 결과: ${menuList.length}개 (회사: ${userCompanyCode})`
);
2025-08-21 09:41:46 +09:00
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,
});
2025-08-26 14:23:22 +09:00
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,
2025-10-27 16:40:59 +09:00
companyCode, // 회사 코드 필터 추가
size, // countPerPage 대신 사용 가능
2025-08-26 14:23:22 +09:00
} = req.query;
// Raw Query를 사용한 사용자 목록 조회
2025-09-01 11:00:38 +09:00
let searchType = "none";
let whereConditions: string[] = [];
let queryParams: any[] = [];
let paramIndex = 1;
2025-09-01 11:00:38 +09:00
2025-10-27 16:40:59 +09:00
// 회사 코드 필터 (권한 그룹 멤버 관리 시 사용)
if (companyCode && typeof companyCode === "string" && companyCode.trim()) {
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(companyCode.trim());
paramIndex++;
logger.info("회사 코드 필터 적용", { companyCode });
}
// 최고 관리자 필터링 (회사 관리자와 일반 사용자는 최고 관리자를 볼 수 없음)
if (req.user && req.user.companyCode !== "*") {
// 최고 관리자가 아닌 경우, company_code가 "*"인 사용자는 제외
whereConditions.push(`company_code != '*'`);
logger.info("최고 관리자 필터링 적용", {
userCompanyCode: req.user.companyCode,
});
}
2025-09-01 11:00:38 +09:00
// 검색 조건 처리
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++;
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
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",
};
2025-08-26 14:23:22 +09:00
2025-09-01 11:00:38 +09:00
if (fieldMap[searchField as string]) {
if (searchField === "tel") {
whereConditions.push(
`(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})`
);
queryParams.push(`%${searchValue}%`);
paramIndex++;
2025-09-01 11:00:38 +09:00
} else {
whereConditions.push(
`${fieldMap[searchField as string]} ILIKE $${paramIndex}`
);
queryParams.push(`%${searchValue}%`);
paramIndex++;
2025-08-26 14:23:22 +09:00
}
2025-09-01 11:00:38 +09:00
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" },
];
2025-08-26 14:23:22 +09:00
2025-09-01 11:00:38 +09:00
let hasAdvancedSearch = false;
2025-08-26 14:23:22 +09:00
2025-09-01 11:00:38 +09:00
for (const { param, field } of advancedSearchFields) {
if (param && typeof param === "string" && param.trim()) {
whereConditions.push(`${field} ILIKE $${paramIndex}`);
queryParams.push(`%${param.trim()}%`);
paramIndex++;
2025-08-26 14:23:22 +09:00
hasAdvancedSearch = true;
}
2025-08-25 13:12:17 +09:00
}
2025-09-01 11:00:38 +09:00
// 전화번호 검색
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++;
2025-09-01 11:00:38 +09:00
hasAdvancedSearch = true;
2025-08-25 13:12:17 +09:00
}
2025-09-01 11:00:38 +09:00
if (hasAdvancedSearch) {
searchType = "advanced";
logger.info("고급 검색 실행", {
search_sabun,
search_companyName,
search_deptName,
search_positionName,
search_userId,
search_userName,
search_tel,
search_email,
});
2025-08-25 13:12:17 +09:00
}
2025-09-01 11:00:38 +09:00
}
2025-08-25 13:12:17 +09:00
2025-10-27 16:40:59 +09:00
// 현재 로그인한 사용자의 회사 코드 필터 (슈퍼관리자가 아닌 경우)
if (req.user && req.user.companyCode !== "*" && !companyCode) {
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(req.user.companyCode);
paramIndex++;
logger.info("사용자 회사 코드 필터 적용", {
companyCode: req.user.companyCode,
});
}
2025-09-01 11:00:38 +09:00
// 기존 필터들
if (deptCode) {
whereConditions.push(`dept_code = $${paramIndex}`);
queryParams.push(deptCode);
paramIndex++;
2025-09-01 11:00:38 +09:00
}
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
if (status) {
whereConditions.push(`status = $${paramIndex}`);
queryParams.push(status);
paramIndex++;
2025-09-01 11:00:38 +09:00
}
2025-08-25 13:12:17 +09:00
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
2025-09-01 11:00:38 +09:00
// 총 개수 조회
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);
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
// 사용자 목록 조회
2025-10-27 16:40:59 +09:00
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}
`;
2025-10-27 16:40:59 +09:00
const users = await query<any>(usersQuery, [...queryParams, limit, offset]);
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
// 응답 데이터 가공
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,
2025-09-01 11:00:38 +09:00
}));
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
const response = {
success: true,
data: processedUsers,
total: totalCount,
searchType,
pagination: {
page: Number(page),
2025-10-27 16:40:59 +09:00
limit: limit,
totalPages: Math.ceil(totalCount / limit),
2025-09-01 11:00:38 +09:00
},
message: "사용자 목록 조회 성공",
};
2025-09-01 11:00:38 +09:00
logger.info("사용자 목록 조회 성공", {
totalCount,
returnedCount: processedUsers.length,
searchType,
currentPage: Number(page),
2025-10-27 16:40:59 +09:00
limit: limit,
companyCode: companyCode || "all",
2025-09-01 11:00:38 +09:00
});
2025-09-01 11:00:38 +09:00
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;
}
// 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<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;
}
// 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",
});
}
};
/**
2025-08-25 13:12:17 +09:00
* GET /api/admin/companies
* API
2025-08-25 13:12:17 +09:00
* 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<any>(
` SELECT
company_code,
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
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,
})),
});
2025-08-25 13:12:17 +09:00
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",
});
}
}
/**
2025-08-25 15:12:31 +09:00
*
*/
export async function getLangKeyList(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("다국어 키 목록 조회 요청", {
query: req.query,
user: req.user,
});
// Raw Query로 다국어 키 목록 조회
const result = await query<any>(
`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,
}));
2025-08-25 15:12:31 +09:00
// 프론트엔드에서 기대하는 응답 형식으로 변환
const response: ApiResponse<any[]> = {
success: true,
message: "다국어 키 목록 조회 성공",
data: langKeys,
};
2025-08-25 15:12:31 +09:00
logger.info("다국어 키 목록 조회 성공", {
totalCount: langKeys.length,
response: response,
});
2025-08-25 15:12:31 +09:00
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 });
2025-10-13 15:06:48 +09:00
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
let requestCompanyCode = menuData.companyCode || menuData.company_code;
// "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용
2025-11-24 17:24:47 +09:00
if (
requestCompanyCode === "none" ||
requestCompanyCode === "" ||
!requestCompanyCode
) {
requestCompanyCode = undefined;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 생성 가능
if (requestCompanyCode === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 생성할 수 있습니다.",
error: "Unauthorized to create common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 생성 가능
// requestCompanyCode가 undefined면 사용자 회사 코드 사용 (권한 체크 통과)
if (requestCompanyCode && requestCompanyCode !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 생성할 권한이 없습니다.",
error: "Unauthorized to create menu for this company",
});
return;
}
}
// Raw Query를 사용한 메뉴 저장
const objid = Date.now(); // 고유 ID 생성
const companyCode = requestCompanyCode || userCompanyCode;
// menu_url이 비어있으면 screen_code도 null로 설정
const menuUrl = menuData.menuUrl || null;
const screenCode = menuUrl ? menuData.screenCode || null : null;
const [savedMenu] = await query<any>(
`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, screen_code
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
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,
menuUrl,
menuData.menuDesc || null,
req.user?.userId || "admin",
new Date(),
menuData.status || "active",
menuData.systemName || null,
companyCode,
menuData.langKey || null,
menuData.langKeyDesc || null,
screenCode,
]
);
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: 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<void> {
try {
const { menuId } = req.params;
const menuData = req.body;
logger.info(`메뉴 수정 요청: menuId = ${menuId}`, {
menuData,
user: req.user,
});
2025-10-13 15:06:48 +09:00
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 수정하려는 메뉴 조회
const currentMenu = await queryOne<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 수정 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 수정할 수 있습니다.",
error: "Unauthorized to update common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 수정 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 수정할 권한이 없습니다.",
error: "Unauthorized to update menu for this company",
});
return;
}
}
2025-11-24 17:24:47 +09:00
const requestCompanyCode =
menuData.companyCode || menuData.company_code || currentMenu.company_code;
// company_code 변경 시도하는 경우 권한 체크
if (requestCompanyCode !== currentMenu.company_code) {
// 공통 메뉴로 변경하려는 경우 최고 관리자만 가능
if (requestCompanyCode === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴로 변경할 권한이 없습니다.",
error: "Unauthorized to change to common menu",
});
return;
}
}
// 회사 관리자는 자기 회사로만 변경 가능
2025-11-24 17:24:47 +09:00
else if (
userCompanyCode !== "*" &&
requestCompanyCode !== userCompanyCode
) {
res.status(403).json({
success: false,
message: "해당 회사로 변경할 권한이 없습니다.",
error: "Unauthorized to change company",
});
return;
}
}
const companyCode = requestCompanyCode;
// menu_url이 비어있으면 screen_code도 null로 설정
const menuUrl = menuData.menuUrl || null;
const screenCode = menuUrl ? menuData.screenCode || null : null;
// Raw Query를 사용한 메뉴 수정
const [updatedMenu] = await query<any>(
`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,
screen_code = $13
WHERE objid = $14
RETURNING *`,
[
menuData.menuType ? Number(menuData.menuType) : null,
menuData.parentObjId ? Number(menuData.parentObjId) : null,
menuData.menuNameKor,
menuData.menuNameEng || null,
menuData.seq ? Number(menuData.seq) : null,
menuUrl,
menuData.menuDesc || null,
menuData.status || "active",
menuData.systemName || null,
companyCode,
menuData.langKey || null,
menuData.langKeyDesc || null,
screenCode,
Number(menuId),
]
);
// menu_url이 비어있으면 화면 할당도 해제 (screen_menu_assignments의 is_active를 'N'으로)
if (!menuUrl) {
await query(
`UPDATE screen_menu_assignments
2025-11-24 10:02:56 +09:00
SET is_active = 'N'
WHERE menu_objid = $1 AND company_code = $2`,
[Number(menuId), companyCode]
);
logger.info("화면 할당 비활성화", { menuId, companyCode });
}
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: 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<void> {
try {
const { menuId } = req.params;
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 삭제하려는 메뉴 조회
const currentMenu = await queryOne<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 삭제 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
error: "Unauthorized to delete common menu",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴를 삭제할 권한이 없습니다.",
error: "Unauthorized to delete menu for this company",
});
return;
}
}
2025-12-03 16:02:09 +09:00
// 외래키 제약 조건이 있는 관련 테이블 데이터 먼저 정리
const menuObjid = Number(menuId);
// 1. category_column_mapping에서 menu_objid를 NULL로 설정
await query(
`UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]
);
// 2. code_category에서 menu_objid를 NULL로 설정
await query(
`UPDATE code_category SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]
);
// 3. code_info에서 menu_objid를 NULL로 설정
await query(
`UPDATE code_info SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]
);
// 4. numbering_rules에서 menu_objid를 NULL로 설정
await query(
`UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]
);
// 5. rel_menu_auth에서 관련 권한 삭제
await query(
`DELETE FROM rel_menu_auth WHERE menu_objid = $1`,
[menuObjid]
);
// 6. screen_menu_assignments에서 관련 할당 삭제
await query(
`DELETE FROM screen_menu_assignments WHERE menu_objid = $1`,
[menuObjid]
);
logger.info("메뉴 관련 데이터 정리 완료", { menuObjid });
// Raw Query를 사용한 메뉴 삭제
const [deletedMenu] = await query<any>(
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
2025-12-03 16:02:09 +09:00
[menuObjid]
);
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: 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<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;
}
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 삭제하려는 메뉴들의 company_code 확인
const menusToDelete = await query<any>(
`SELECT objid, company_code FROM menu_info WHERE objid = ANY($1::bigint[])`,
[menuIds.map((id) => Number(id))]
);
// 권한 체크: 공통 메뉴 포함 여부 확인
2025-11-24 17:24:47 +09:00
const hasCommonMenu = menusToDelete.some(
(menu: any) => menu.company_code === "*"
);
if (
hasCommonMenu &&
(userCompanyCode !== "*" || userType !== "SUPER_ADMIN")
) {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
error: "Unauthorized to delete common menu",
});
return;
}
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
if (userCompanyCode !== "*") {
const unauthorizedMenus = menusToDelete.filter(
2025-11-24 17:24:47 +09:00
(menu: any) =>
menu.company_code !== userCompanyCode && menu.company_code !== "*"
);
if (unauthorizedMenus.length > 0) {
res.status(403).json({
success: false,
message: "다른 회사의 메뉴를 삭제할 권한이 없습니다.",
error: "Unauthorized to delete menus for other companies",
});
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<any>(
`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<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 toggleMenuStatus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuId } = req.params;
logger.info(`메뉴 상태 토글 요청: menuId = ${menuId}`, { user: req.user });
// 사용자의 company_code 확인
if (!req.user?.companyCode) {
res.status(400).json({
success: false,
message: "사용자의 회사 코드를 찾을 수 없습니다.",
error: "Missing company_code",
});
return;
}
const userCompanyCode = req.user.companyCode;
const userType = req.user.userType;
// 현재 상태 및 회사 코드 조회
const currentMenu = await queryOne<any>(
`SELECT objid, status, company_code FROM menu_info WHERE objid = $1`,
[Number(menuId)]
);
if (!currentMenu) {
res.status(404).json({
success: false,
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
error: "Menu not found",
});
return;
}
// 공통 메뉴(company_code = '*')는 최고 관리자만 상태 변경 가능
if (currentMenu.company_code === "*") {
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
res.status(403).json({
success: false,
message: "공통 메뉴는 최고 관리자만 상태를 변경할 수 있습니다.",
error: "Unauthorized to toggle common menu status",
});
return;
}
} else if (userCompanyCode !== "*") {
// 회사 관리자는 자기 회사 메뉴만 상태 변경 가능
if (currentMenu.company_code !== userCompanyCode) {
res.status(403).json({
success: false,
message: "해당 회사의 메뉴 상태를 변경할 권한이 없습니다.",
error: "Unauthorized to toggle menu status for this company",
});
return;
}
}
// 상태 토글 (active <-> inactive)
const currentStatus = currentMenu.status;
const newStatus = currentStatus === "active" ? "inactive" : "active";
// 상태 업데이트
const [updatedMenu] = await query<any>(
`UPDATE menu_info SET status = $1 WHERE objid = $2 RETURNING *`,
[newStatus, Number(menuId)]
);
logger.info("메뉴 상태 토글 성공", {
menuId,
oldStatus: currentStatus,
newStatus,
});
const result = newStatus === "active" ? "활성화" : "비활성화";
const response: ApiResponse<string> = {
success: true,
message: `메뉴가 ${result}되었습니다.`,
data: result,
};
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",
errorCode: "MENU_TOGGLE_ERROR",
});
}
}
/**
* ( )
*/
export async function getCompanyListFromDB(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
logger.info("회사 목록 조회 요청 (Raw Query)", { user: req.user });
// Raw Query로 회사 목록 조회
const companies = await query<any>(
` SELECT
company_code,
company_name,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
writer,
regdate,
status
FROM company_mng
ORDER BY regdate DESC`
);
logger.info("회사 목록 조회 성공 (Raw Query)", { count: companies.length });
const response: ApiResponse<any> = {
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",
});
}
}
2025-08-25 13:12:17 +09:00
/**
* 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;
2025-08-25 13:12:17 +09:00
// 회사 코드 필터
if (companyCode) {
2025-10-27 16:40:59 +09:00
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(companyCode);
paramIndex++;
}
2025-08-25 13:12:17 +09:00
// 상태 필터
if (status) {
whereConditions.push(`status = $${paramIndex}`);
queryParams.push(status);
paramIndex++;
}
2025-08-25 13:12:17 +09:00
// 검색 조건
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++;
}
2025-08-25 13:12:17 +09:00
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const departments = await query<any>(
`SELECT
dept_code,
parent_dept_code,
dept_name,
master_sabun,
master_user_id,
location,
location_name,
regdate,
data_type,
status,
sales_yn,
2025-10-27 16:40:59 +09:00
company_code,
company_name
FROM dept_info
${whereClause}
ORDER BY parent_dept_code ASC NULLS FIRST, dept_name ASC`,
queryParams
);
2025-08-25 13:12:17 +09:00
// 부서 트리 구조 생성
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,
2025-10-27 16:40:59 +09:00
companyCode: dept.company_code,
companyName: dept.company_name,
children: [],
});
});
2025-08-25 13:12:17 +09:00
// 부서 트리 구조 생성
departments.forEach((dept) => {
const deptNode = deptMap.get(dept.dept_code);
2025-08-25 13:12:17 +09:00
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);
}
});
2025-08-25 13:12:17 +09:00
const response = {
success: true,
data: {
departments: rootDepartments,
flatList: departments.map((dept) => ({
2025-08-25 13:12:17 +09:00
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,
2025-08-25 13:12:17 +09:00
dataType: dept.data_type,
status: dept.status || "active",
salesYn: dept.sales_yn,
2025-10-27 16:40:59 +09:00
companyCode: dept.company_code,
2025-08-25 13:12:17 +09:00
companyName: dept.company_name,
})),
},
message: "부서 목록 조회 성공",
total: departments.length,
};
2025-08-25 13:12:17 +09:00
logger.info("부서 목록 조회 성공", {
totalCount: departments.length,
rootCount: rootDepartments.length,
});
2025-08-25 13:12:17 +09:00
res.status(200).json(response);
2025-08-25 13:12:17 +09:00
} 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<any>(
`SELECT * FROM user_info WHERE user_id = $1`,
[userId]
);
2025-08-25 13:12:17 +09:00
if (!user) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
error: {
code: "USER_NOT_FOUND",
details: `User ID: ${userId}`,
},
});
return;
}
2025-08-25 13:12:17 +09:00
// 부서 정보 별도 조회
const deptInfo = user.dept_code
? await queryOne<any>(
`SELECT
dept_name,
parent_dept_code,
location,
location_name,
sales_yn,
company_name
FROM dept_info
WHERE dept_code = $1`,
[user.dept_code]
)
: null;
2025-08-25 13:12:17 +09:00
// 응답 데이터 가공
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
2025-09-26 18:51:27 +09:00
? `data:image/jpeg;base64,${Buffer.from(user.photo).toString("base64")}`
: null,
locale: user.locale,
companyCode: user.company_code,
dataType: user.data_type,
// 부서 정보
deptInfo: {
2025-08-25 13:12:17 +09:00
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,
},
};
2025-08-25 13:12:17 +09:00
const response = {
success: true,
data: userInfo,
message: "사용자 상세 정보 조회 성공",
};
2025-08-25 13:12:17 +09:00
logger.info("사용자 상세 정보 조회 성공", {
userId,
userName: user.user_name,
});
2025-08-25 13:12:17 +09:00
res.status(200).json(response);
2025-08-25 13:12:17 +09:00
} 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
2025-09-01 11:00:38 +09:00
): Promise<void> => {
2025-08-25 13:12:17 +09:00
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<any>(
`SELECT user_id FROM user_info WHERE user_id = $1`,
[userId]
);
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
const isDuplicate = !!existingUser;
const count = isDuplicate ? 1 : 0;
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
const response = {
success: true,
data: {
2025-08-25 13:12:17 +09:00
isDuplicate,
count,
2025-09-01 11:00:38 +09:00
message: isDuplicate
? "이미 사용 중인 사용자 ID입니다."
: "사용 가능한 사용자 ID입니다.",
},
message: "사용자 ID 중복 체크 완료",
};
2025-08-25 13:12:17 +09:00
2025-09-01 11:00:38 +09:00
logger.info("사용자 ID 중복 체크 완료", {
userId,
isDuplicate,
count,
});
res.status(200).json(response);
2025-08-25 13:12:17 +09:00
} 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
*/
2025-08-25 18:30:07 +09:00
/**
* 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);
}
};
2025-08-26 09:56:45 +09:00
/**
* 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<any>(
`SELECT user_id, user_name, status FROM user_info WHERE user_id = $1`,
[userId]
);
2025-08-26 09:56:45 +09:00
if (!currentUser) {
res.status(404).json({
result: false,
msg: "사용자를 찾을 수 없습니다.",
2025-09-01 11:00:38 +09:00
});
return;
}
2025-08-26 09:56:45 +09:00
// 2. 상태 변경 실행
// active/inactive에 따른 END_DATE 처리
const endDate = status === "inactive" ? new Date() : null;
2025-08-26 09:56:45 +09:00
const updateResult = await query<any>(
`UPDATE user_info
SET status = $1, end_date = $2
WHERE user_id = $3
RETURNING *`,
[status, endDate, userId]
);
2025-08-26 09:56:45 +09:00
if (updateResult.length > 0) {
// 사용자 이력 저장은 user_info_history 테이블이 @@ignore 상태이므로 생략
2025-08-26 09:56:45 +09:00
logger.info("사용자 상태 변경 성공", {
userId,
oldStatus: currentUser.status,
newStatus: status,
updatedBy: req.user?.userId,
});
2025-08-26 09:56:45 +09:00
res.json({
result: true,
msg: `사용자 상태가 ${status === "active" ? "활성" : "비활성"}으로 변경되었습니다.`,
});
} else {
res.status(400).json({
result: false,
msg: "사용자 상태 변경에 실패했습니다.",
});
2025-08-26 09:56:45 +09:00
}
} catch (error) {
logger.error("사용자 상태 변경 중 오류 발생", {
error,
userId: req.params.userId,
status: req.body.status,
});
res.status(500).json({
result: false,
msg: "시스템 오류가 발생했습니다.",
});
}
};
2025-08-25 13:12:17 +09:00
export const saveUser = async (req: AuthenticatedRequest, res: Response) => {
try {
const userData = req.body;
2025-10-27 16:40:59 +09:00
const isUpdate = req.method === "PUT"; // PUT 요청이면 수정
logger.info("사용자 저장 요청", {
userData,
user: req.user,
isUpdate,
method: req.method,
});
2025-08-25 13:12:17 +09:00
// 필수 필드 검증
2025-10-27 16:40:59 +09:00
let requiredFields = ["userId", "userName"];
// 신규 등록 시에만 비밀번호 필수
if (!isUpdate) {
requiredFields.push("userPassword");
}
2025-08-25 13:12:17 +09:00
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;
}
}
2025-10-27 16:40:59 +09:00
// 비밀번호 암호화 (비밀번호가 제공된 경우에만)
let encryptedPassword = null;
if (userData.userPassword) {
encryptedPassword = await EncryptUtil.encrypt(userData.userPassword);
}
2025-08-25 13:12:17 +09:00
// Raw Query를 사용한 사용자 저장 (upsert with ON CONFLICT)
2025-10-27 16:40:59 +09:00
const updatePasswordClause = encryptedPassword ? "user_password = $4," : "";
const [savedUser] = await query<any>(
`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,
2025-10-27 16:40:59 +09:00
${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,
2025-10-27 16:40:59 +09:00
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(),
]
);
2025-08-25 13:12:17 +09:00
// 기존 사용자인지 새 사용자인지 확인 (regdate로 판단)
2025-10-27 16:40:59 +09:00
const isExistingUser =
savedUser.regdate &&
new Date(savedUser.regdate).getTime() < Date.now() - 1000;
2025-08-25 13:12:17 +09:00
2025-10-27 16:40:59 +09:00
logger.info(
isExistingUser ? "사용자 정보 수정 완료" : "새 사용자 등록 완료",
{
userId: userData.userId,
}
);
2025-08-25 13:12:17 +09:00
const response = {
success: true,
result: true,
2025-10-27 16:40:59 +09:00
message: isExistingUser
? "사용자 정보가 수정되었습니다."
: "사용자가 등록되었습니다.",
data: {
userId: userData.userId,
2025-10-27 16:40:59 +09:00
isUpdate: isExistingUser,
},
};
2025-08-25 13:12:17 +09:00
res.status(200).json(response);
2025-08-25 13:12:17 +09:00
} 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;
}
// Raw Query로 회사명 중복 체크
const existingCompany = await queryOne<any>(
`SELECT company_code FROM company_mng WHERE company_name = $1`,
[company_name.trim()]
);
2025-09-01 11:00:38 +09:00
// 사업자등록번호 유효성 검증
const businessNumberValidation = validateBusinessNumber(
req.body.business_registration_number?.trim() || ""
);
if (!businessNumberValidation.isValid) {
res.status(400).json({
success: false,
message: businessNumberValidation.message,
errorCode: "INVALID_BUSINESS_NUMBER",
});
return;
}
// Raw Query로 사업자등록번호 중복 체크
const existingBusinessNumber = await queryOne<any>(
`SELECT company_code FROM company_mng WHERE business_registration_number = $1`,
[req.body.business_registration_number?.trim()]
);
2025-09-01 11:00:38 +09:00
if (existingCompany) {
res.status(400).json({
success: false,
message: "이미 등록된 회사명입니다.",
errorCode: "COMPANY_NAME_DUPLICATE",
});
return;
}
if (existingBusinessNumber) {
res.status(400).json({
success: false,
message: "이미 등록된 사업자등록번호입니다.",
errorCode: "DUPLICATE_BUSINESS_NUMBER",
});
return;
}
2025-09-01 11:00:38 +09:00
// 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,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
writer,
regdate,
status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`;
const writer = req.user
? `${req.user.userName}(${req.user.userId})`
: "시스템";
const insertValues = [
companyCode,
company_name.trim(),
req.body.business_registration_number?.trim() || null,
req.body.representative_name?.trim() || null,
req.body.representative_phone?.trim() || null,
req.body.email?.trim() || null,
req.body.website?.trim() || null,
req.body.address?.trim() || null,
writer,
new Date(),
"active",
];
const insertResult = await client.query(insertQuery, insertValues);
const createdCompany = insertResult.rows[0];
2025-09-05 14:52:10 +09:00
// 회사 폴더 초기화 (파일 시스템)
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",
});
}
};
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
/**
* GET /api/admin/companies/:companyCode
* API
*/
export const getCompanyByCode = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { companyCode } = req.params;
logger.info("회사 정보 조회 요청", {
companyCode,
user: req.user,
});
// Raw Query로 회사 정보 조회
const company = await queryOne<any>(
`SELECT * FROM company_mng WHERE company_code = $1`,
[companyCode]
);
if (!company) {
res.status(404).json({
success: false,
message: "해당 회사를 찾을 수 없습니다.",
errorCode: "COMPANY_NOT_FOUND",
});
return;
}
logger.info("회사 정보 조회 성공", {
companyCode: company.company_code,
companyName: company.company_name,
});
const response = {
success: true,
message: "회사 정보 조회 성공",
data: {
companyCode: company.company_code,
companyName: company.company_name,
businessRegistrationNumber: company.business_registration_number,
representativeName: company.representative_name,
representativePhone: company.representative_phone,
email: company.email,
website: company.website,
address: company.address,
status: company.status,
writer: company.writer,
regdate: company.regdate,
},
};
res.status(200).json(response);
} catch (error) {
2025-11-24 17:24:47 +09:00
logger.error("회사 정보 조회 실패", {
error,
companyCode: req.params.companyCode,
});
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
res.status(500).json({
success: false,
message: "회사 정보 조회 중 오류가 발생했습니다.",
errorCode: "COMPANY_GET_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,
business_registration_number,
representative_name,
representative_phone,
email,
website,
address,
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<any>(
`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;
}
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
if (business_registration_number && business_registration_number.trim()) {
// 유효성 검증
2025-11-24 17:24:47 +09:00
const businessNumberValidation = validateBusinessNumber(
business_registration_number.trim()
);
if (!businessNumberValidation.isValid) {
res.status(400).json({
success: false,
message: businessNumberValidation.message,
errorCode: "INVALID_BUSINESS_NUMBER",
});
return;
}
// 중복 체크
const duplicateBusinessNumber = await queryOne<any>(
`SELECT company_code FROM company_mng
WHERE business_registration_number = $1 AND company_code != $2`,
[business_registration_number.trim(), companyCode]
);
if (duplicateBusinessNumber) {
res.status(400).json({
success: false,
message: "이미 등록된 사업자등록번호입니다.",
errorCode: "DUPLICATE_BUSINESS_NUMBER",
});
return;
}
}
// Raw Query로 회사 정보 수정
const result = await query<any>(
`UPDATE company_mng
SET
company_name = $1,
business_registration_number = $2,
representative_name = $3,
representative_phone = $4,
email = $5,
website = $6,
address = $7,
status = $8
WHERE company_code = $9
RETURNING *`,
[
company_name.trim(),
business_registration_number?.trim() || null,
representative_name?.trim() || null,
representative_phone?.trim() || null,
email?.trim() || null,
website?.trim() || null,
address?.trim() || null,
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<void> => {
try {
const { companyCode } = req.params;
logger.info("회사 삭제 요청", {
companyCode,
user: req.user,
});
// Raw Query로 회사 삭제
const result = await query<any>(
`DELETE FROM company_mng
WHERE company_code = $1
RETURNING company_code, company_name`,
[companyCode]
);
if (result.length === 0) {
2025-09-01 11:00:38 +09:00
res.status(404).json({
success: false,
message: "해당 회사를 찾을 수 없습니다.",
errorCode: "COMPANY_NOT_FOUND",
});
return;
}
const deletedCompany = result[0];
2025-09-01 11:00:38 +09:00
logger.info("회사 삭제 성공", {
companyCode: deletedCompany.company_code,
companyName: deletedCompany.company_name,
2025-09-01 11:00:38 +09:00
});
2025-09-01 11:00:38 +09:00
const response = {
success: true,
message: "회사가 삭제되었습니다.",
data: {
company_code: deletedCompany.company_code,
company_name: deletedCompany.company_name,
2025-09-01 11:00:38 +09:00
},
};
2025-09-01 11:00:38 +09:00
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",
});
}
};
2025-08-26 13:42:57 +09:00
/**
* POST /api/admin/users/reset-password
* API
* Java AdminController.resetUserPassword()
*/
2025-08-27 17:32:41 +09:00
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++;
}
2025-08-28 10:05:06 +09:00
// 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++;
2025-08-28 10:05:06 +09:00
} catch (error) {
console.error("Base64 이미지 처리 오류:", error);
updateFields.push(`photo = $${paramIndex}`);
updateValues.push(null);
paramIndex++;
2025-08-28 10:05:06 +09:00
}
} else {
updateFields.push(`photo = $${paramIndex}`);
updateValues.push(null);
paramIndex++;
2025-08-28 10:05:06 +09:00
}
}
if (locale !== undefined) {
updateFields.push(`locale = $${paramIndex}`);
updateValues.push(locale);
paramIndex++;
}
2025-08-27 17:32:41 +09:00
// 업데이트할 데이터가 없으면 에러
if (updateFields.length === 0) {
2025-08-27 17:32:41 +09:00
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
);
2025-08-27 17:32:41 +09:00
// 업데이트된 사용자 정보 조회
const updatedUser = await queryOne<any>(
`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]
);
2025-08-27 17:32:41 +09:00
2025-08-28 10:05:06 +09:00
// photo가 Buffer 타입인 경우 Base64로 변환
const responseData = {
...updatedUser,
photo: updatedUser?.photo
2025-09-26 18:51:27 +09:00
? `data:image/jpeg;base64,${Buffer.from(updatedUser.photo).toString("base64")}`
2025-08-28 10:05:06 +09:00
: null,
};
2025-08-27 17:32:41 +09:00
res.json({
result: true,
message: "프로필이 성공적으로 업데이트되었습니다.",
2025-08-28 10:05:06 +09:00
data: responseData,
2025-08-27 17:32:41 +09:00
});
} catch (error) {
console.error("프로필 업데이트 오류:", error);
res.status(500).json({
result: false,
error: {
code: "UPDATE_FAILED",
details: "프로필 업데이트 중 오류가 발생했습니다.",
},
});
}
};
2025-08-26 13:42:57 +09:00
export const resetUserPassword = async (
req: AuthenticatedRequest,
res: Response
2025-09-01 11:00:38 +09:00
): Promise<void> => {
const { userId, newPassword } = req.body;
2025-08-26 13:42:57 +09:00
2025-09-01 11:00:38 +09:00
logger.info("비밀번호 초기화 요청", { userId, user: req.user });
2025-08-26 13:42:57 +09:00
2025-09-01 11:00:38 +09:00
// 입력값 검증
if (!userId || !userId.trim()) {
res.status(400).json({
result: false,
msg: "사용자 ID가 필요합니다.",
});
return;
}
2025-08-26 13:42:57 +09:00
2025-09-01 11:00:38 +09:00
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<any>(
`SELECT user_id, user_name FROM user_info WHERE user_id = $1`,
[userId]
);
2025-09-01 11:00:38 +09:00
if (!currentUser) {
res.status(404).json({
2025-08-26 13:42:57 +09:00
success: false,
result: false,
2025-09-01 11:00:38 +09:00
message: "사용자를 찾을 수 없습니다.",
msg: "사용자를 찾을 수 없습니다.",
2025-08-26 13:42:57 +09:00
});
return;
}
2025-09-01 11:00:38 +09:00
// 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({
2025-08-26 13:42:57 +09:00
success: false,
result: false,
2025-09-01 11:00:38 +09:00
message: "비밀번호 암호화 중 오류가 발생했습니다.",
msg: "비밀번호 암호화 중 오류가 발생했습니다.",
2025-08-26 13:42:57 +09:00
});
return;
}
// 3. Raw Query로 비밀번호 업데이트 실행
const updateResult = await query<any>(
`UPDATE user_info SET user_password = $1 WHERE user_id = $2 RETURNING *`,
[encryptedPassword, userId]
);
2025-08-26 13:42:57 +09:00
if (updateResult.length > 0) {
// 이력 저장은 user_info_history 테이블이 @@ignore 상태이므로 생략
2025-09-01 11:00:38 +09:00
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: "사용자 정보를 찾을 수 없거나 비밀번호 변경에 실패했습니다.",
});
2025-08-26 13:42:57 +09:00
}
} catch (error) {
logger.error("비밀번호 초기화 중 오류 발생", {
error,
2025-09-01 11:00:38 +09:00
userId,
2025-08-26 13:42:57 +09:00
});
res.status(500).json({
success: false,
result: false,
message: "비밀번호 초기화 중 시스템 오류가 발생했습니다.",
msg: "비밀번호 초기화 중 시스템 오류가 발생했습니다.",
});
}
};
/**
* ( )
*/
export async function getTableSchema(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const companyCode = req.user?.companyCode;
if (!tableName) {
res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
return;
}
logger.info("테이블 스키마 조회", { tableName, companyCode });
// information_schema에서 컬럼 정보 가져오기
const schemaQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position
`;
const columns = await query<any>(schemaQuery, [tableName]);
if (columns.length === 0) {
res.status(404).json({
success: false,
message: `테이블 '${tableName}'을 찾을 수 없습니다.`,
});
return;
}
// 컬럼 정보를 간단한 형태로 변환
const columnList = columns.map((col: any) => ({
name: col.column_name,
type: col.data_type,
nullable: col.is_nullable === "YES",
default: col.column_default,
maxLength: col.character_maximum_length,
precision: col.numeric_precision,
scale: col.numeric_scale,
}));
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);
res.json({
success: true,
message: "테이블 스키마 조회 성공",
data: {
tableName,
columns: columnList,
},
});
} catch (error) {
logger.error("테이블 스키마 조회 중 오류 발생:", error);
res.status(500).json({
success: false,
message: "테이블 스키마 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_SCHEMA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
});
}
}
/**
*
* POST /api/admin/menus/:menuObjid/copy
*/
export async function copyMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuObjid } = req.params;
const { targetCompanyCode } = req.body;
const userId = req.user!.userId;
const userCompanyCode = req.user!.companyCode;
const userType = req.user!.userType;
const isSuperAdmin = req.user!.isSuperAdmin;
logger.info(`
=== API ===
menuObjid: ${menuObjid}
targetCompanyCode: ${targetCompanyCode}
userId: ${userId}
userCompanyCode: ${userCompanyCode}
userType: ${userType}
isSuperAdmin: ${isSuperAdmin}
`);
// 권한 체크: 최고 관리자만 가능
if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
2025-11-24 17:24:47 +09:00
logger.warn(
`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`
);
res.status(403).json({
success: false,
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",
error: {
code: "FORBIDDEN",
details: "Only super admin can copy menus",
},
});
return;
}
// 필수 파라미터 검증
if (!menuObjid || !targetCompanyCode) {
res.status(400).json({
success: false,
message: "필수 파라미터가 누락되었습니다",
error: {
code: "MISSING_PARAMETERS",
details: "menuObjid and targetCompanyCode are required",
},
});
return;
}
// 화면명 변환 설정 (선택사항)
const screenNameConfig = req.body.screenNameConfig
? {
removeText: req.body.screenNameConfig.removeText,
addPrefix: req.body.screenNameConfig.addPrefix,
}
: undefined;
// 메뉴 복사 실행
const menuCopyService = new MenuCopyService();
const result = await menuCopyService.copyMenu(
parseInt(menuObjid, 10),
targetCompanyCode,
userId,
screenNameConfig
);
logger.info("✅ 메뉴 복사 API 성공");
res.json({
success: true,
message: "메뉴 복사 완료",
data: result,
});
} catch (error: any) {
logger.error("❌ 메뉴 복사 API 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 복사 중 오류가 발생했습니다",
error: {
code: "MENU_COPY_ERROR",
details: error.message || "Unknown error",
},
});
}
}
/**
* ============================================================
* + API
* ============================================================
*
* (user_info) (user_dept) .
*
* ##
* 1. user_info
* 2. user_dept +
* 3.
* 4.
*
* ##
* ```json
* {
* "userInfo": {
* "user_id": "string (필수)",
* "user_name": "string (필수)",
* "email": "string",
* "cell_phone": "string",
* "sabun": "string",
* ...
* },
* "mainDept": {
* "dept_code": "string (필수)",
* "dept_name": "string",
* "position_name": "string"
* },
* "subDepts": [
* {
* "dept_code": "string (필수)",
* "dept_name": "string",
* "position_name": "string"
* }
* ]
* }
* ```
*/
// 사원 + 부서 저장 요청 타입
interface UserWithDeptRequest {
userInfo: {
user_id: string;
user_name: string;
user_name_eng?: string;
user_password?: string;
email?: string;
tel?: string;
cell_phone?: string;
sabun?: string;
user_type?: string;
user_type_name?: string;
status?: string;
locale?: string;
// 메인 부서 정보 (user_info에도 저장)
dept_code?: string;
dept_name?: string;
position_code?: string;
position_name?: string;
};
mainDept?: {
dept_code: string;
dept_name?: string;
position_name?: string;
};
subDepts?: Array<{
dept_code: string;
dept_name?: string;
position_name?: string;
}>;
isUpdate?: boolean; // 수정 모드 여부
}
/**
* POST /api/admin/users/with-dept
* + API
*/
export const saveUserWithDept = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
const client = await getPool().connect();
try {
const { userInfo, mainDept, subDepts = [], isUpdate = false } = req.body as UserWithDeptRequest;
const companyCode = req.user?.companyCode || "*";
const currentUserId = req.user?.userId;
logger.info("사원+부서 통합 저장 요청", {
userId: userInfo?.user_id,
mainDept: mainDept?.dept_code,
subDeptsCount: subDepts.length,
isUpdate,
companyCode,
});
// 필수값 검증
if (!userInfo?.user_id || !userInfo?.user_name) {
res.status(400).json({
success: false,
message: "사용자 ID와 이름은 필수입니다.",
error: { code: "REQUIRED_FIELD_MISSING" },
});
return;
}
// 트랜잭션 시작
await client.query("BEGIN");
// 1. 기존 사용자 확인
const existingUser = await client.query(
"SELECT user_id FROM user_info WHERE user_id = $1",
[userInfo.user_id]
);
const isExistingUser = existingUser.rows.length > 0;
// 2. 비밀번호 암호화 (새 사용자이거나 비밀번호가 제공된 경우)
let encryptedPassword = null;
if (userInfo.user_password) {
encryptedPassword = await EncryptUtil.encrypt(userInfo.user_password);
}
// 3. user_info 저장 (UPSERT)
// mainDept가 있으면 user_info에도 메인 부서 정보 저장
const deptCode = mainDept?.dept_code || userInfo.dept_code || null;
const deptName = mainDept?.dept_name || userInfo.dept_name || null;
const positionName = mainDept?.position_name || userInfo.position_name || null;
if (isExistingUser) {
// 기존 사용자 수정
const updateFields: string[] = [];
const updateValues: any[] = [];
let paramIndex = 1;
// 동적으로 업데이트할 필드 구성
const fieldsToUpdate: Record<string, any> = {
user_name: userInfo.user_name,
user_name_eng: userInfo.user_name_eng,
email: userInfo.email,
tel: userInfo.tel,
cell_phone: userInfo.cell_phone,
sabun: userInfo.sabun,
user_type: userInfo.user_type,
user_type_name: userInfo.user_type_name,
status: userInfo.status || "active",
locale: userInfo.locale,
dept_code: deptCode,
dept_name: deptName,
position_code: userInfo.position_code,
position_name: positionName,
company_code: companyCode !== "*" ? companyCode : undefined,
};
// 비밀번호가 제공된 경우에만 업데이트
if (encryptedPassword) {
fieldsToUpdate.user_password = encryptedPassword;
}
for (const [key, value] of Object.entries(fieldsToUpdate)) {
if (value !== undefined) {
updateFields.push(`${key} = $${paramIndex}`);
updateValues.push(value);
paramIndex++;
}
}
if (updateFields.length > 0) {
updateValues.push(userInfo.user_id);
await client.query(
`UPDATE user_info SET ${updateFields.join(", ")} WHERE user_id = $${paramIndex}`,
updateValues
);
}
} else {
// 새 사용자 등록
await client.query(
`INSERT INTO user_info (
user_id, user_name, user_name_eng, user_password,
email, tel, cell_phone, sabun,
user_type, user_type_name, status, locale,
dept_code, dept_name, position_code, position_name,
company_code, regdate
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW())`,
[
userInfo.user_id,
userInfo.user_name,
userInfo.user_name_eng || null,
encryptedPassword || null,
userInfo.email || null,
userInfo.tel || null,
userInfo.cell_phone || null,
userInfo.sabun || null,
userInfo.user_type || null,
userInfo.user_type_name || null,
userInfo.status || "active",
userInfo.locale || null,
deptCode,
deptName,
userInfo.position_code || null,
positionName,
companyCode !== "*" ? companyCode : null,
]
);
}
// 4. user_dept 처리
if (mainDept?.dept_code || subDepts.length > 0) {
// 4-1. 기존 부서 관계 조회 (메인 부서 변경 감지용)
const existingDepts = await client.query(
"SELECT dept_code, is_primary FROM user_dept WHERE user_id = $1",
[userInfo.user_id]
);
const existingMainDept = existingDepts.rows.find((d: any) => d.is_primary === true);
// 4-2. 메인 부서가 변경된 경우, 기존 메인 부서를 겸직으로 전환
if (mainDept?.dept_code && existingMainDept && existingMainDept.dept_code !== mainDept.dept_code) {
logger.info("메인 부서 변경 감지 - 기존 메인을 겸직으로 전환", {
userId: userInfo.user_id,
oldMain: existingMainDept.dept_code,
newMain: mainDept.dept_code,
});
await client.query(
"UPDATE user_dept SET is_primary = false, updated_at = NOW() WHERE user_id = $1 AND dept_code = $2",
[userInfo.user_id, existingMainDept.dept_code]
);
}
// 4-3. 기존 겸직 부서 삭제 (메인 제외)
// 새로 입력받은 subDepts로 교체하기 위해 기존 겸직 삭제
await client.query(
"DELETE FROM user_dept WHERE user_id = $1 AND is_primary = false",
[userInfo.user_id]
);
// 4-4. 메인 부서 저장 (UPSERT)
if (mainDept?.dept_code) {
await client.query(
`INSERT INTO user_dept (user_id, dept_code, is_primary, dept_name, user_name, position_name, company_code, created_at, updated_at)
VALUES ($1, $2, true, $3, $4, $5, $6, NOW(), NOW())
ON CONFLICT (user_id, dept_code) DO UPDATE SET
is_primary = true,
dept_name = $3,
user_name = $4,
position_name = $5,
company_code = $6,
updated_at = NOW()`,
[
userInfo.user_id,
mainDept.dept_code,
mainDept.dept_name || null,
userInfo.user_name,
mainDept.position_name || null,
companyCode !== "*" ? companyCode : null,
]
);
}
// 4-5. 겸직 부서 저장
for (const subDept of subDepts) {
if (!subDept.dept_code) continue;
// 메인 부서와 같은 부서는 겸직으로 추가하지 않음
if (mainDept?.dept_code === subDept.dept_code) continue;
await client.query(
`INSERT INTO user_dept (user_id, dept_code, is_primary, dept_name, user_name, position_name, company_code, created_at, updated_at)
VALUES ($1, $2, false, $3, $4, $5, $6, NOW(), NOW())
ON CONFLICT (user_id, dept_code) DO UPDATE SET
is_primary = false,
dept_name = $3,
user_name = $4,
position_name = $5,
company_code = $6,
updated_at = NOW()`,
[
userInfo.user_id,
subDept.dept_code,
subDept.dept_name || null,
userInfo.user_name,
subDept.position_name || null,
companyCode !== "*" ? companyCode : null,
]
);
}
}
// 트랜잭션 커밋
await client.query("COMMIT");
logger.info("사원+부서 통합 저장 완료", {
userId: userInfo.user_id,
isUpdate: isExistingUser,
});
res.json({
success: true,
message: isExistingUser ? "사원 정보가 수정되었습니다." : "사원이 등록되었습니다.",
data: {
userId: userInfo.user_id,
isUpdate: isExistingUser,
},
});
} catch (error: any) {
// 트랜잭션 롤백
await client.query("ROLLBACK");
logger.error("사원+부서 통합 저장 실패", { error: error.message, body: req.body });
// 중복 키 에러 처리
if (error.code === "23505") {
res.status(400).json({
success: false,
message: "이미 존재하는 사용자 ID입니다.",
error: { code: "DUPLICATE_USER_ID" },
});
return;
}
res.status(500).json({
success: false,
message: "사원 저장 중 오류가 발생했습니다.",
error: { code: "SAVE_ERROR", details: error.message },
});
} finally {
client.release();
}
}
/**
* GET /api/admin/users/:userId/with-dept
* + API ( )
*/
export const getUserWithDept = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { userId } = req.params;
const companyCode = req.user?.companyCode || "*";
logger.info("사원+부서 조회 요청", { userId, companyCode });
// 1. user_info 조회
let userQuery = "SELECT * FROM user_info WHERE user_id = $1";
const userParams: any[] = [userId];
// 최고 관리자가 아니면 회사 필터링
if (companyCode !== "*") {
userQuery += " AND company_code = $2";
userParams.push(companyCode);
}
const userResult = await query<any>(userQuery, userParams);
if (userResult.length === 0) {
res.status(404).json({
success: false,
message: "사용자를 찾을 수 없습니다.",
error: { code: "USER_NOT_FOUND" },
});
return;
}
const userInfo = userResult[0];
// 2. user_dept 조회 (메인 + 겸직)
let deptQuery = "SELECT * FROM user_dept WHERE user_id = $1 ORDER BY is_primary DESC, created_at ASC";
const deptResult = await query<any>(deptQuery, [userId]);
const mainDept = deptResult.find((d: any) => d.is_primary === true);
const subDepts = deptResult.filter((d: any) => d.is_primary === false);
res.json({
success: true,
data: {
userInfo,
mainDept: mainDept || null,
subDepts,
},
});
} catch (error: any) {
logger.error("사원+부서 조회 실패", { error: error.message, userId: req.params.userId });
res.status(500).json({
success: false,
message: "사원 조회 중 오류가 발생했습니다.",
error: { code: "QUERY_ERROR", details: error.message },
});
}
}