Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -3,7 +3,7 @@ import { logger } from "../utils/logger";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { ApiResponse } from "../types/common";
|
||||
import { Client } from "pg";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import { query, queryOne, getPool } from "../database/db";
|
||||
import config from "../config/environment";
|
||||
import { AdminService } from "../services/adminService";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
@@ -3406,3 +3406,395 @@ export async function copyMenu(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
* 사원 + 부서 통합 관리 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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export class TableHistoryController {
|
||||
|
||||
const whereClause = whereConditions.join(" AND ");
|
||||
|
||||
// 이력 조회 쿼리
|
||||
// 이력 조회 쿼리 (log_id로 정렬 - 시간 데이터 불일치 문제 해결)
|
||||
const historyQuery = `
|
||||
SELECT
|
||||
log_id,
|
||||
@@ -84,7 +84,7 @@ export class TableHistoryController {
|
||||
full_row_after
|
||||
FROM ${logTableName}
|
||||
WHERE ${whereClause}
|
||||
ORDER BY changed_at DESC
|
||||
ORDER BY log_id DESC
|
||||
LIMIT ${limitParam} OFFSET ${offsetParam}
|
||||
`;
|
||||
|
||||
@@ -196,7 +196,7 @@ export class TableHistoryController {
|
||||
|
||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
||||
|
||||
// 이력 조회 쿼리
|
||||
// 이력 조회 쿼리 (log_id로 정렬 - 시간 데이터 불일치 문제 해결)
|
||||
const historyQuery = `
|
||||
SELECT
|
||||
log_id,
|
||||
@@ -213,7 +213,7 @@ export class TableHistoryController {
|
||||
full_row_after
|
||||
FROM ${logTableName}
|
||||
${whereClause}
|
||||
ORDER BY changed_at DESC
|
||||
ORDER BY log_id DESC
|
||||
LIMIT ${limitParam} OFFSET ${offsetParam}
|
||||
`;
|
||||
|
||||
|
||||
365
backend-node/src/controllers/taxInvoiceController.ts
Normal file
365
backend-node/src/controllers/taxInvoiceController.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 세금계산서 컨트롤러
|
||||
* 세금계산서 API 엔드포인트 처리
|
||||
*/
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { TaxInvoiceService } from "../services/taxInvoiceService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class TaxInvoiceController {
|
||||
/**
|
||||
* 세금계산서 목록 조회
|
||||
* GET /api/tax-invoice
|
||||
*/
|
||||
static async getList(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
page = "1",
|
||||
pageSize = "20",
|
||||
invoice_type,
|
||||
invoice_status,
|
||||
start_date,
|
||||
end_date,
|
||||
search,
|
||||
buyer_name,
|
||||
cost_type,
|
||||
} = req.query;
|
||||
|
||||
const result = await TaxInvoiceService.getList(companyCode, {
|
||||
page: parseInt(page as string, 10),
|
||||
pageSize: parseInt(pageSize as string, 10),
|
||||
invoice_type: invoice_type as "sales" | "purchase" | undefined,
|
||||
invoice_status: invoice_status as string | undefined,
|
||||
start_date: start_date as string | undefined,
|
||||
end_date: end_date as string | undefined,
|
||||
search: search as string | undefined,
|
||||
buyer_name: buyer_name as string | undefined,
|
||||
cost_type: cost_type as any,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
total: result.total,
|
||||
totalPages: Math.ceil(result.total / result.pageSize),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 목록 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 상세 조회
|
||||
* GET /api/tax-invoice/:id
|
||||
*/
|
||||
static async getById(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await TaxInvoiceService.getById(id, companyCode);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ success: false, message: "세금계산서를 찾을 수 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 상세 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 생성
|
||||
* POST /api/tax-invoice
|
||||
*/
|
||||
static async create(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.invoice_type) {
|
||||
res.status(400).json({ success: false, message: "세금계산서 유형은 필수입니다." });
|
||||
return;
|
||||
}
|
||||
if (!data.invoice_date) {
|
||||
res.status(400).json({ success: false, message: "작성일자는 필수입니다." });
|
||||
return;
|
||||
}
|
||||
if (data.supply_amount === undefined || data.supply_amount === null) {
|
||||
res.status(400).json({ success: false, message: "공급가액은 필수입니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await TaxInvoiceService.create(data, companyCode, userId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "세금계산서가 생성되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 생성 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 생성 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 수정
|
||||
* PUT /api/tax-invoice/:id
|
||||
*/
|
||||
static async update(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
const result = await TaxInvoiceService.update(id, data, companyCode, userId);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ success: false, message: "세금계산서를 찾을 수 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "세금계산서가 수정되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 수정 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 수정 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 삭제
|
||||
* DELETE /api/tax-invoice/:id
|
||||
*/
|
||||
static async delete(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await TaxInvoiceService.delete(id, companyCode, userId);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({ success: false, message: "세금계산서를 찾을 수 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "세금계산서가 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 삭제 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 삭제 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 발행
|
||||
* POST /api/tax-invoice/:id/issue
|
||||
*/
|
||||
static async issue(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await TaxInvoiceService.issue(id, companyCode, userId);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "세금계산서를 찾을 수 없거나 이미 발행된 상태입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "세금계산서가 발행되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 발행 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 발행 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 취소
|
||||
* POST /api/tax-invoice/:id/cancel
|
||||
*/
|
||||
static async cancel(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { reason } = req.body;
|
||||
|
||||
const result = await TaxInvoiceService.cancel(id, companyCode, userId, reason);
|
||||
|
||||
if (!result) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "세금계산서를 찾을 수 없거나 취소할 수 없는 상태입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "세금계산서가 취소되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("세금계산서 취소 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "세금계산서 취소 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 월별 통계 조회
|
||||
* GET /api/tax-invoice/stats/monthly
|
||||
*/
|
||||
static async getMonthlyStats(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { year, month } = req.query;
|
||||
const now = new Date();
|
||||
const targetYear = year ? parseInt(year as string, 10) : now.getFullYear();
|
||||
const targetMonth = month ? parseInt(month as string, 10) : now.getMonth() + 1;
|
||||
|
||||
const result = await TaxInvoiceService.getMonthlyStats(companyCode, targetYear, targetMonth);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
period: { year: targetYear, month: targetMonth },
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("월별 통계 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "통계 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용 유형별 통계 조회
|
||||
* GET /api/tax-invoice/stats/cost-type
|
||||
*/
|
||||
static async getCostTypeStats(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { year, month } = req.query;
|
||||
const targetYear = year ? parseInt(year as string, 10) : undefined;
|
||||
const targetMonth = month ? parseInt(month as string, 10) : undefined;
|
||||
|
||||
const result = await TaxInvoiceService.getCostTypeStats(companyCode, targetYear, targetMonth);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
period: { year: targetYear, month: targetMonth },
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("비용 유형별 통계 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "통계 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user