535 lines
13 KiB
TypeScript
535 lines
13 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";
|
|
|
|
/**
|
|
* 부서 목록 조회 (회사별)
|
|
*/
|
|
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 });
|
|
|
|
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 });
|
|
|
|
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
|
|
});
|
|
|
|
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: "주 부서 설정 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|