Merge remote-tracking branch 'origin/main' into ksh

This commit is contained in:
SeongHyun Kim
2025-12-09 09:24:33 +09:00
122 changed files with 10018 additions and 1710 deletions

View File

@@ -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레벨 메뉴 목록 조회
*

View File

@@ -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}
`;

View 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 || "통계 조회 중 오류가 발생했습니다.",
});
}
}
}