Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
File diff suppressed because it is too large
Load Diff
212
backend-node/src/controllers/approvalProxyController.ts
Normal file
212
backend-node/src/controllers/approvalProxyController.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
// ============================================================
|
||||
// 대결 위임 설정 (Approval Proxy Settings) CRUD
|
||||
// ============================================================
|
||||
|
||||
export class ApprovalProxyController {
|
||||
// 대결 위임 목록 조회 (user_info JOIN으로 이름/부서 포함)
|
||||
static async getProxySettings(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT ps.*,
|
||||
u1.user_name AS original_user_name, u1.dept_name AS original_dept_name,
|
||||
u2.user_name AS proxy_user_name, u2.dept_name AS proxy_dept_name
|
||||
FROM approval_proxy_settings ps
|
||||
LEFT JOIN user_info u1 ON ps.original_user_id = u1.user_id AND ps.company_code = u1.company_code
|
||||
LEFT JOIN user_info u2 ON ps.proxy_user_id = u2.user_id AND ps.company_code = u2.company_code
|
||||
WHERE ps.company_code = $1
|
||||
ORDER BY ps.created_at DESC`,
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 생성 (기간 중복 체크 포함)
|
||||
static async createProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { original_user_id, proxy_user_id, start_date, end_date, reason, is_active = "Y" } = req.body;
|
||||
|
||||
if (!original_user_id || !proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자는 필수입니다." });
|
||||
}
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ success: false, message: "시작일과 종료일은 필수입니다." });
|
||||
}
|
||||
if (original_user_id === proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자가 동일할 수 없습니다." });
|
||||
}
|
||||
|
||||
// 같은 기간 중복 체크 (daterange 오버랩)
|
||||
const overlap = await queryOne<any>(
|
||||
`SELECT COUNT(*) AS cnt FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND is_active = 'Y'
|
||||
AND daterange(start_date, end_date, '[]') && daterange($2::date, $3::date, '[]')
|
||||
AND company_code = $4`,
|
||||
[original_user_id, start_date, end_date, companyCode]
|
||||
);
|
||||
|
||||
if (overlap && parseInt(overlap.cnt) > 0) {
|
||||
return res.status(400).json({ success: false, message: "해당 기간에 이미 대결 설정이 존재합니다." });
|
||||
}
|
||||
|
||||
const [row] = await query<any>(
|
||||
`INSERT INTO approval_proxy_settings
|
||||
(original_user_id, proxy_user_id, start_date, end_date, reason, is_active, company_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[original_user_id, proxy_user_id, start_date, end_date, reason || null, is_active, companyCode]
|
||||
);
|
||||
|
||||
return res.status(201).json({ success: true, data: row, message: "대결 위임이 생성되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 생성 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 수정
|
||||
static async updateProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const existing = await queryOne<any>(
|
||||
"SELECT id FROM approval_proxy_settings WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
const { proxy_user_id, start_date, end_date, reason, is_active } = req.body;
|
||||
|
||||
const fields: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (proxy_user_id !== undefined) { fields.push(`proxy_user_id = $${idx++}`); params.push(proxy_user_id); }
|
||||
if (start_date !== undefined) { fields.push(`start_date = $${idx++}`); params.push(start_date); }
|
||||
if (end_date !== undefined) { fields.push(`end_date = $${idx++}`); params.push(end_date); }
|
||||
if (reason !== undefined) { fields.push(`reason = $${idx++}`); params.push(reason); }
|
||||
if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); }
|
||||
|
||||
if (fields.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "수정할 필드가 없습니다." });
|
||||
}
|
||||
|
||||
fields.push(`updated_at = NOW()`);
|
||||
params.push(id, companyCode);
|
||||
|
||||
const [row] = await query<any>(
|
||||
`UPDATE approval_proxy_settings SET ${fields.join(", ")}
|
||||
WHERE id = $${idx++} AND company_code = $${idx++}
|
||||
RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: row, message: "대결 위임이 수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 수정 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 삭제
|
||||
static async deleteProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await query<any>(
|
||||
"DELETE FROM approval_proxy_settings WHERE id = $1 AND company_code = $2 RETURNING id",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
return res.json({ success: true, message: "대결 위임이 삭제되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 삭제 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 사용자의 현재 활성 대결자 조회
|
||||
static async checkActiveProxy(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ success: false, message: "userId 파라미터는 필수입니다." });
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT ps.*, u.user_name AS proxy_user_name
|
||||
FROM approval_proxy_settings ps
|
||||
LEFT JOIN user_info u ON ps.proxy_user_id = u.user_id AND ps.company_code = u.company_code
|
||||
WHERE ps.original_user_id = $1 AND ps.is_active = 'Y'
|
||||
AND ps.start_date <= CURRENT_DATE AND ps.end_date >= CURRENT_DATE
|
||||
AND ps.company_code = $2`,
|
||||
[userId, companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("활성 대결자 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "활성 대결자 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
275
backend-node/src/controllers/systemNoticeController.ts
Normal file
275
backend-node/src/controllers/systemNoticeController.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { logger } from "../utils/logger";
|
||||
import { query } from "../database/db";
|
||||
|
||||
/**
|
||||
* GET /api/system-notices
|
||||
* 공지사항 목록 조회
|
||||
* - 최고 관리자(*): 전체 조회
|
||||
* - 일반 회사: 자신의 company_code 데이터만 조회
|
||||
* - is_active 필터 옵션 지원
|
||||
*/
|
||||
export const getSystemNotices = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { is_active } = req.query;
|
||||
|
||||
logger.info("공지사항 목록 조회 요청", { companyCode, is_active });
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 최고 관리자가 아닌 경우 company_code 필터링
|
||||
if (companyCode !== "*") {
|
||||
conditions.push(`company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// is_active 필터 (true/false 문자열 처리)
|
||||
if (is_active !== undefined && is_active !== "") {
|
||||
const activeValue = is_active === "true" || is_active === "1";
|
||||
conditions.push(`is_active = $${paramIndex}`);
|
||||
params.push(activeValue);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT
|
||||
id,
|
||||
company_code,
|
||||
title,
|
||||
content,
|
||||
is_active,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM system_notice
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
logger.info("공지사항 목록 조회 성공", {
|
||||
companyCode,
|
||||
count: rows.length,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: rows,
|
||||
total: rows.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공지사항 목록 조회 실패", { error });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "공지사항 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/system-notices
|
||||
* 공지사항 등록
|
||||
* - company_code는 req.user.companyCode에서 자동 추출 (클라이언트 입력 신뢰 금지)
|
||||
*/
|
||||
export const createSystemNotice = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { title, content, is_active = true } = req.body;
|
||||
|
||||
logger.info("공지사항 등록 요청", { companyCode, userId, title });
|
||||
|
||||
if (!title || !title.trim()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "제목을 입력해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "내용을 입력해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const [created] = await query<any>(
|
||||
`INSERT INTO system_notice (company_code, title, content, is_active, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[companyCode, title.trim(), content.trim(), is_active, userId]
|
||||
);
|
||||
|
||||
logger.info("공지사항 등록 성공", {
|
||||
id: created.id,
|
||||
companyCode,
|
||||
title: created.title,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: created,
|
||||
message: "공지사항이 등록되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공지사항 등록 실패", { error });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "공지사항 등록 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* PUT /api/system-notices/:id
|
||||
* 공지사항 수정
|
||||
* - WHERE id=$1 AND company_code=$2 로 타 회사 데이터 수정 차단
|
||||
* - 최고 관리자는 company_code 조건 없이 id만으로 수정 가능
|
||||
*/
|
||||
export const updateSystemNotice = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
const { title, content, is_active } = req.body;
|
||||
|
||||
logger.info("공지사항 수정 요청", { id, companyCode });
|
||||
|
||||
if (!title || !title.trim()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "제목을 입력해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "내용을 입력해주세요.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let result: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: id만으로 수정
|
||||
result = await query<any>(
|
||||
`UPDATE system_notice
|
||||
SET title = $1, content = $2, is_active = $3, updated_at = NOW()
|
||||
WHERE id = $4
|
||||
RETURNING *`,
|
||||
[title.trim(), content.trim(), is_active ?? true, id]
|
||||
);
|
||||
} else {
|
||||
// 일반 회사: company_code 추가 조건으로 타 회사 데이터 수정 차단
|
||||
result = await query<any>(
|
||||
`UPDATE system_notice
|
||||
SET title = $1, content = $2, is_active = $3, updated_at = NOW()
|
||||
WHERE id = $4 AND company_code = $5
|
||||
RETURNING *`,
|
||||
[title.trim(), content.trim(), is_active ?? true, id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "공지사항을 찾을 수 없거나 수정 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("공지사항 수정 성공", { id, companyCode });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result[0],
|
||||
message: "공지사항이 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공지사항 수정 실패", { error, id: req.params.id });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "공지사항 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE /api/system-notices/:id
|
||||
* 공지사항 삭제
|
||||
* - WHERE id=$1 AND company_code=$2 로 타 회사 데이터 삭제 차단
|
||||
* - 최고 관리자는 company_code 조건 없이 id만으로 삭제 가능
|
||||
*/
|
||||
export const deleteSystemNotice = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
logger.info("공지사항 삭제 요청", { id, companyCode });
|
||||
|
||||
let result: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: id만으로 삭제
|
||||
result = await query<any>(
|
||||
`DELETE FROM system_notice WHERE id = $1 RETURNING id`,
|
||||
[id]
|
||||
);
|
||||
} else {
|
||||
// 일반 회사: company_code 추가 조건으로 타 회사 데이터 삭제 차단
|
||||
result = await query<any>(
|
||||
`DELETE FROM system_notice WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "공지사항을 찾을 수 없거나 삭제 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("공지사항 삭제 성공", { id, companyCode });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "공지사항이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공지사항 삭제 실패", { error, id: req.params.id });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "공지사항 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user