- Integrated client IP address retrieval in the audit logging functionality across multiple controllers, including admin, common code, department, flow, screen, and table management. - Updated the `auditLogService` to include a new method for obtaining the client's IP address, ensuring accurate logging of user actions. - This enhancement improves traceability and accountability by capturing the source of requests, thereby strengthening the overall logging mechanism within the application.
581 lines
15 KiB
TypeScript
581 lines
15 KiB
TypeScript
import { Response } from "express";
|
|
import { logger } from "../utils/logger";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import { ApiResponse } from "../types/common";
|
|
import { query, queryOne } from "../database/db";
|
|
import { auditLogService, getClientIp } from "../services/auditLogService";
|
|
|
|
/**
|
|
* 부서 목록 조회 (회사별)
|
|
*/
|
|
export async function getDepartments(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { companyCode } = req.params;
|
|
const userCompanyCode = req.user?.companyCode;
|
|
|
|
logger.info("부서 목록 조회", { companyCode, userCompanyCode });
|
|
|
|
// 최고 관리자가 아니면 자신의 회사만 조회 가능
|
|
if (userCompanyCode !== "*" && userCompanyCode !== companyCode) {
|
|
res.status(403).json({
|
|
success: false,
|
|
message: "해당 회사의 부서를 조회할 권한이 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 부서 목록 조회 (부서원 수 포함)
|
|
const departments = await query<any>(`
|
|
SELECT
|
|
d.dept_code,
|
|
d.dept_name,
|
|
d.company_code,
|
|
d.parent_dept_code,
|
|
COUNT(DISTINCT ud.user_id) as member_count
|
|
FROM dept_info d
|
|
LEFT JOIN user_dept ud ON d.dept_code = ud.dept_code
|
|
WHERE d.company_code = $1
|
|
GROUP BY d.dept_code, d.dept_name, d.company_code, d.parent_dept_code
|
|
ORDER BY d.dept_name
|
|
`, [companyCode]);
|
|
|
|
// 응답 형식 변환
|
|
const formattedDepartments = departments.map((dept) => ({
|
|
dept_code: dept.dept_code,
|
|
dept_name: dept.dept_name,
|
|
company_code: dept.company_code,
|
|
parent_dept_code: dept.parent_dept_code,
|
|
memberCount: parseInt(dept.member_count || "0"),
|
|
}));
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: formattedDepartments,
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서 목록 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서 목록 조회 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서 상세 조회
|
|
*/
|
|
export async function getDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode } = req.params;
|
|
|
|
const department = await queryOne<any>(`
|
|
SELECT
|
|
dept_code,
|
|
dept_name,
|
|
company_code,
|
|
parent_dept_code
|
|
FROM dept_info
|
|
WHERE dept_code = $1
|
|
`, [deptCode]);
|
|
|
|
if (!department) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "부서를 찾을 수 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: department,
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서 상세 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서 조회 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서 생성
|
|
*/
|
|
export async function createDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { companyCode } = req.params;
|
|
const { dept_name, parent_dept_code } = req.body;
|
|
|
|
if (!dept_name || !dept_name.trim()) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "부서명을 입력해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 같은 회사 내 중복 부서명 확인
|
|
const duplicate = await queryOne<any>(`
|
|
SELECT dept_code, dept_name
|
|
FROM dept_info
|
|
WHERE company_code = $1 AND dept_name = $2
|
|
`, [companyCode, dept_name.trim()]);
|
|
|
|
if (duplicate) {
|
|
res.status(409).json({
|
|
success: false,
|
|
message: `"${dept_name}" 부서가 이미 존재합니다.`,
|
|
isDuplicate: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 회사 이름 조회
|
|
const company = await queryOne<any>(`
|
|
SELECT company_name FROM company_mng WHERE company_code = $1
|
|
`, [companyCode]);
|
|
|
|
const companyName = company?.company_name || companyCode;
|
|
|
|
// 부서 코드 생성 (전역 카운트: DEPT_1, DEPT_2, ...)
|
|
const codeResult = await queryOne<any>(`
|
|
SELECT COALESCE(MAX(CAST(SUBSTRING(dept_code FROM 6) AS INTEGER)), 0) + 1 as next_number
|
|
FROM dept_info
|
|
WHERE dept_code ~ '^DEPT_[0-9]+$'
|
|
`);
|
|
|
|
const nextNumber = codeResult?.next_number || 1;
|
|
const deptCode = `DEPT_${nextNumber}`;
|
|
|
|
// 부서 생성
|
|
const result = await query<any>(`
|
|
INSERT INTO dept_info (
|
|
dept_code,
|
|
dept_name,
|
|
company_code,
|
|
company_name,
|
|
parent_dept_code,
|
|
status,
|
|
regdate
|
|
) VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
|
RETURNING *
|
|
`, [
|
|
deptCode,
|
|
dept_name.trim(),
|
|
companyCode,
|
|
companyName,
|
|
parent_dept_code || null,
|
|
'active',
|
|
]);
|
|
|
|
logger.info("부서 생성 성공", { deptCode, dept_name });
|
|
|
|
auditLogService.log({
|
|
companyCode: companyCode || req.user?.companyCode || "",
|
|
userId: req.user?.userId || "",
|
|
userName: req.user?.userName || "",
|
|
action: "CREATE",
|
|
resourceType: "DATA",
|
|
resourceId: deptCode,
|
|
resourceName: dept_name.trim(),
|
|
tableName: "dept_info",
|
|
summary: `부서 "${dept_name.trim()}" 생성`,
|
|
changes: { after: { deptCode, deptName: dept_name.trim(), parentDeptCode: parent_dept_code } },
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: "부서가 생성되었습니다.",
|
|
data: result[0],
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서 생성 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서 생성 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서 수정
|
|
*/
|
|
export async function updateDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode } = req.params;
|
|
const { dept_name, parent_dept_code } = req.body;
|
|
|
|
if (!dept_name || !dept_name.trim()) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "부서명을 입력해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = await query<any>(`
|
|
UPDATE dept_info
|
|
SET
|
|
dept_name = $1,
|
|
parent_dept_code = $2
|
|
WHERE dept_code = $3
|
|
RETURNING *
|
|
`, [dept_name.trim(), parent_dept_code || null, deptCode]);
|
|
|
|
if (result.length === 0) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "부서를 찾을 수 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("부서 수정 성공", { deptCode });
|
|
|
|
auditLogService.log({
|
|
companyCode: req.user?.companyCode || "",
|
|
userId: req.user?.userId || "",
|
|
userName: req.user?.userName || "",
|
|
action: "UPDATE",
|
|
resourceType: "DATA",
|
|
resourceId: deptCode,
|
|
resourceName: dept_name.trim(),
|
|
tableName: "dept_info",
|
|
summary: `부서 "${dept_name.trim()}" 수정`,
|
|
changes: { after: { deptName: dept_name.trim(), parentDeptCode: parent_dept_code } },
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "부서가 수정되었습니다.",
|
|
data: result[0],
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서 수정 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서 수정 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서 삭제
|
|
*/
|
|
export async function deleteDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode } = req.params;
|
|
|
|
// 하위 부서 확인
|
|
const hasChildren = await queryOne<any>(`
|
|
SELECT COUNT(*) as count
|
|
FROM dept_info
|
|
WHERE parent_dept_code = $1
|
|
`, [deptCode]);
|
|
|
|
if (parseInt(hasChildren?.count || "0") > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "하위 부서가 있는 부서는 삭제할 수 없습니다. 먼저 하위 부서를 삭제해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 부서원 삭제 (부서 삭제 전에 먼저 삭제)
|
|
const deletedMembers = await query<any>(`
|
|
DELETE FROM user_dept
|
|
WHERE dept_code = $1
|
|
RETURNING user_id
|
|
`, [deptCode]);
|
|
|
|
const memberCount = deletedMembers.length;
|
|
|
|
// 부서 삭제
|
|
const result = await query<any>(`
|
|
DELETE FROM dept_info
|
|
WHERE dept_code = $1
|
|
RETURNING dept_code, dept_name
|
|
`, [deptCode]);
|
|
|
|
if (result.length === 0) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "부서를 찾을 수 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("부서 삭제 성공", {
|
|
deptCode,
|
|
deptName: result[0].dept_name,
|
|
deletedMemberCount: memberCount
|
|
});
|
|
|
|
auditLogService.log({
|
|
companyCode: req.user?.companyCode || "",
|
|
userId: req.user?.userId || "",
|
|
userName: req.user?.userName || "",
|
|
action: "DELETE",
|
|
resourceType: "DATA",
|
|
resourceId: deptCode,
|
|
resourceName: result[0].dept_name,
|
|
tableName: "dept_info",
|
|
summary: `부서 "${result[0].dept_name}" 삭제${memberCount > 0 ? ` (부서원 ${memberCount}명 제외)` : ""}`,
|
|
changes: { before: { deptCode, deptName: result[0].dept_name } },
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: memberCount > 0
|
|
? `부서가 삭제되었습니다. (부서원 ${memberCount}명 제외됨)`
|
|
: "부서가 삭제되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서 삭제 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서 삭제 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서원 목록 조회
|
|
*/
|
|
export async function getDepartmentMembers(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode } = req.params;
|
|
|
|
const members = await query<any>(`
|
|
SELECT
|
|
u.user_id,
|
|
u.user_name,
|
|
u.email,
|
|
u.tel as phone,
|
|
u.cell_phone,
|
|
u.position_name,
|
|
ud.dept_code,
|
|
d.dept_name,
|
|
ud.is_primary
|
|
FROM user_dept ud
|
|
JOIN user_info u ON ud.user_id = u.user_id
|
|
JOIN dept_info d ON ud.dept_code = d.dept_code
|
|
WHERE ud.dept_code = $1
|
|
ORDER BY ud.is_primary DESC, u.user_name
|
|
`, [deptCode]);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: members,
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서원 목록 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서원 목록 조회 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용자 검색 (부서원 추가용)
|
|
*/
|
|
export async function searchUsers(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { companyCode } = req.params;
|
|
const { search } = req.query;
|
|
|
|
if (!search || typeof search !== 'string') {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "검색어를 입력해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 사용자 검색 (ID 또는 이름)
|
|
const users = await query<any>(`
|
|
SELECT
|
|
user_id,
|
|
user_name,
|
|
email,
|
|
position_name,
|
|
company_code
|
|
FROM user_info
|
|
WHERE company_code = $1
|
|
AND (
|
|
user_id ILIKE $2 OR
|
|
user_name ILIKE $2
|
|
)
|
|
ORDER BY user_name
|
|
LIMIT 20
|
|
`, [companyCode, `%${search}%`]);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: users,
|
|
});
|
|
} catch (error) {
|
|
logger.error("사용자 검색 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "사용자 검색 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서원 추가
|
|
*/
|
|
export async function addDepartmentMember(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode } = req.params;
|
|
const { user_id } = req.body;
|
|
|
|
if (!user_id) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "사용자 ID를 입력해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 사용자 존재 확인
|
|
const user = await queryOne<any>(`
|
|
SELECT user_id, user_name
|
|
FROM user_info
|
|
WHERE user_id = $1
|
|
`, [user_id]);
|
|
|
|
if (!user) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "사용자를 찾을 수 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 이미 부서원인지 확인
|
|
const existing = await queryOne<any>(`
|
|
SELECT *
|
|
FROM user_dept
|
|
WHERE user_id = $1 AND dept_code = $2
|
|
`, [user_id, deptCode]);
|
|
|
|
if (existing) {
|
|
res.status(409).json({
|
|
success: false,
|
|
message: "이미 해당 부서의 부서원입니다.",
|
|
isDuplicate: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 주 부서가 있는지 확인
|
|
const hasPrimary = await queryOne<any>(`
|
|
SELECT *
|
|
FROM user_dept
|
|
WHERE user_id = $1 AND is_primary = true
|
|
`, [user_id]);
|
|
|
|
// 부서원 추가
|
|
await query<any>(`
|
|
INSERT INTO user_dept (user_id, dept_code, is_primary, created_at)
|
|
VALUES ($1, $2, $3, NOW())
|
|
`, [user_id, deptCode, !hasPrimary]);
|
|
|
|
logger.info("부서원 추가 성공", { user_id, deptCode });
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: "부서원이 추가되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서원 추가 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서원 추가 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서원 제거
|
|
*/
|
|
export async function removeDepartmentMember(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode, userId } = req.params;
|
|
|
|
const result = await query<any>(`
|
|
DELETE FROM user_dept
|
|
WHERE user_id = $1 AND dept_code = $2
|
|
RETURNING *
|
|
`, [userId, deptCode]);
|
|
|
|
if (result.length === 0) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "해당 부서원을 찾을 수 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("부서원 제거 성공", { userId, deptCode });
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "부서원이 제거되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
logger.error("부서원 제거 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "부서원 제거 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 주 부서 설정
|
|
*/
|
|
export async function setPrimaryDepartment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { deptCode, userId } = req.params;
|
|
|
|
// 다른 부서의 주 부서 해제
|
|
await query<any>(`
|
|
UPDATE user_dept
|
|
SET is_primary = false
|
|
WHERE user_id = $1
|
|
`, [userId]);
|
|
|
|
// 해당 부서를 주 부서로 설정
|
|
await query<any>(`
|
|
UPDATE user_dept
|
|
SET is_primary = true
|
|
WHERE user_id = $1 AND dept_code = $2
|
|
`, [userId, deptCode]);
|
|
|
|
logger.info("주 부서 설정 성공", { userId, deptCode });
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "주 부서가 설정되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
logger.error("주 부서 설정 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "주 부서 설정 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|