2025-12-08 16:01:59 +09:00
|
|
|
/**
|
|
|
|
|
* 세금계산서 컨트롤러
|
|
|
|
|
* 세금계산서 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,
|
2025-12-08 16:18:44 +09:00
|
|
|
cost_type,
|
2025-12-08 16:01:59 +09:00
|
|
|
} = 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,
|
2025-12-08 16:18:44 +09:00
|
|
|
cost_type: cost_type as any,
|
2025-12-08 16:01:59 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 || "통계 조회 중 오류가 발생했습니다.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 16:18:44 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 비용 유형별 통계 조회
|
|
|
|
|
* 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 || "통계 조회 중 오류가 발생했습니다.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 16:01:59 +09:00
|
|
|
}
|
|
|
|
|
|