Merge remote-tracking branch 'origin/main' into ksh
This commit is contained in:
@@ -527,6 +527,53 @@ export const deleteColumnMappingsByColumn = async (req: AuthenticatedRequest, re
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 코드로 라벨 조회
|
||||
*
|
||||
* POST /api/table-categories/labels-by-codes
|
||||
*
|
||||
* Body:
|
||||
* - valueCodes: 카테고리 코드 배열 (예: ["CATEGORY_767659DCUF", "CATEGORY_8292565608"])
|
||||
*
|
||||
* Response:
|
||||
* - { [code]: label } 형태의 매핑 객체
|
||||
*/
|
||||
export const getCategoryLabelsByCodes = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { valueCodes } = req.body;
|
||||
|
||||
if (!valueCodes || !Array.isArray(valueCodes) || valueCodes.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("카테고리 코드로 라벨 조회", {
|
||||
valueCodes,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
const labels = await tableCategoryValueService.getCategoryLabelsByCodes(
|
||||
valueCodes,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: labels,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`카테고리 라벨 조회 실패: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 라벨 조회 중 오류가 발생했습니다",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2레벨 메뉴 목록 조회
|
||||
*
|
||||
|
||||
@@ -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