세금계산서 업그레이드
This commit is contained in:
@@ -36,6 +36,7 @@ export class TaxInvoiceController {
|
||||
end_date,
|
||||
search,
|
||||
buyer_name,
|
||||
cost_type,
|
||||
} = req.query;
|
||||
|
||||
const result = await TaxInvoiceService.getList(companyCode, {
|
||||
@@ -47,6 +48,7 @@ export class TaxInvoiceController {
|
||||
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({
|
||||
@@ -327,5 +329,37 @@ export class TaxInvoiceController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용 유형별 통계 조회
|
||||
* 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 || "통계 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ router.get("/", TaxInvoiceController.getList);
|
||||
// 월별 통계 (상세 조회보다 먼저 정의해야 함)
|
||||
router.get("/stats/monthly", TaxInvoiceController.getMonthlyStats);
|
||||
|
||||
// 비용 유형별 통계
|
||||
router.get("/stats/cost-type", TaxInvoiceController.getCostTypeStats);
|
||||
|
||||
// 상세 조회
|
||||
router.get("/:id", TaxInvoiceController.getById);
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { query, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 비용 유형 타입
|
||||
export type CostType = "purchase" | "installation" | "repair" | "maintenance" | "disposal" | "other";
|
||||
|
||||
// 세금계산서 타입 정의
|
||||
export interface TaxInvoice {
|
||||
id: string;
|
||||
@@ -46,6 +49,9 @@ export interface TaxInvoice {
|
||||
// 첨부파일 (JSON 배열로 저장)
|
||||
attachments: TaxInvoiceAttachment[] | null;
|
||||
|
||||
// 비용 유형 (구매/설치/수리/유지보수/폐기/기타)
|
||||
cost_type: CostType | null;
|
||||
|
||||
created_date: string;
|
||||
updated_date: string;
|
||||
writer: string;
|
||||
@@ -99,6 +105,7 @@ export interface CreateTaxInvoiceDto {
|
||||
customer_id?: string;
|
||||
items?: CreateTaxInvoiceItemDto[];
|
||||
attachments?: TaxInvoiceAttachment[]; // 첨부파일
|
||||
cost_type?: CostType; // 비용 유형
|
||||
}
|
||||
|
||||
export interface CreateTaxInvoiceItemDto {
|
||||
@@ -121,6 +128,7 @@ export interface TaxInvoiceListParams {
|
||||
end_date?: string;
|
||||
search?: string;
|
||||
buyer_name?: string;
|
||||
cost_type?: CostType; // 비용 유형 필터
|
||||
}
|
||||
|
||||
export class TaxInvoiceService {
|
||||
@@ -169,6 +177,7 @@ export class TaxInvoiceService {
|
||||
end_date,
|
||||
search,
|
||||
buyer_name,
|
||||
cost_type,
|
||||
} = params;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
@@ -214,6 +223,12 @@ export class TaxInvoiceService {
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (cost_type) {
|
||||
conditions.push(`cost_type = $${paramIndex}`);
|
||||
values.push(cost_type);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(" AND ");
|
||||
|
||||
// 전체 개수 조회
|
||||
@@ -282,13 +297,13 @@ export class TaxInvoiceService {
|
||||
supplier_business_type, supplier_business_item,
|
||||
buyer_business_no, buyer_name, buyer_ceo_name, buyer_address, buyer_email,
|
||||
supply_amount, tax_amount, total_amount, invoice_date,
|
||||
remarks, order_id, customer_id, attachments, writer
|
||||
remarks, order_id, customer_id, attachments, cost_type, writer
|
||||
) VALUES (
|
||||
$1, $2, $3, 'draft',
|
||||
$4, $5, $6, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20, $21, $22, $23
|
||||
$19, $20, $21, $22, $23, $24
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
@@ -313,6 +328,7 @@ export class TaxInvoiceService {
|
||||
data.order_id || null,
|
||||
data.customer_id || null,
|
||||
data.attachments ? JSON.stringify(data.attachments) : null,
|
||||
data.cost_type || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
@@ -402,6 +418,7 @@ export class TaxInvoiceService {
|
||||
invoice_date = COALESCE($17, invoice_date),
|
||||
remarks = COALESCE($18, remarks),
|
||||
attachments = $19,
|
||||
cost_type = COALESCE($20, cost_type),
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
RETURNING *`,
|
||||
@@ -425,6 +442,7 @@ export class TaxInvoiceService {
|
||||
data.invoice_date,
|
||||
data.remarks,
|
||||
data.attachments ? JSON.stringify(data.attachments) : null,
|
||||
data.cost_type,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -608,5 +626,159 @@ export class TaxInvoiceService {
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용 유형별 통계 조회
|
||||
*/
|
||||
static async getCostTypeStats(
|
||||
companyCode: string,
|
||||
year?: number,
|
||||
month?: number
|
||||
): Promise<{
|
||||
by_cost_type: Array<{
|
||||
cost_type: CostType | null;
|
||||
count: number;
|
||||
supply_amount: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
}>;
|
||||
by_month: Array<{
|
||||
year_month: string;
|
||||
cost_type: CostType | null;
|
||||
count: number;
|
||||
total_amount: number;
|
||||
}>;
|
||||
summary: {
|
||||
total_count: number;
|
||||
total_amount: number;
|
||||
purchase_amount: number;
|
||||
installation_amount: number;
|
||||
repair_amount: number;
|
||||
maintenance_amount: number;
|
||||
disposal_amount: number;
|
||||
other_amount: number;
|
||||
};
|
||||
}> {
|
||||
const conditions: string[] = ["company_code = $1", "invoice_status != 'cancelled'"];
|
||||
const values: any[] = [companyCode];
|
||||
let paramIndex = 2;
|
||||
|
||||
// 연도/월 필터
|
||||
if (year && month) {
|
||||
const startDate = `${year}-${String(month).padStart(2, "0")}-01`;
|
||||
const endDate = new Date(year, month, 0).toISOString().split("T")[0];
|
||||
conditions.push(`invoice_date >= $${paramIndex} AND invoice_date <= $${paramIndex + 1}`);
|
||||
values.push(startDate, endDate);
|
||||
paramIndex += 2;
|
||||
} else if (year) {
|
||||
conditions.push(`EXTRACT(YEAR FROM invoice_date) = $${paramIndex}`);
|
||||
values.push(year);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(" AND ");
|
||||
|
||||
// 비용 유형별 집계
|
||||
const byCostType = await query<{
|
||||
cost_type: CostType | null;
|
||||
count: string;
|
||||
supply_amount: string;
|
||||
tax_amount: string;
|
||||
total_amount: string;
|
||||
}>(
|
||||
`SELECT
|
||||
cost_type,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(supply_amount), 0) as supply_amount,
|
||||
COALESCE(SUM(tax_amount), 0) as tax_amount,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount
|
||||
FROM tax_invoice
|
||||
WHERE ${whereClause}
|
||||
GROUP BY cost_type
|
||||
ORDER BY total_amount DESC`,
|
||||
values
|
||||
);
|
||||
|
||||
// 월별 비용 유형 집계
|
||||
const byMonth = await query<{
|
||||
year_month: string;
|
||||
cost_type: CostType | null;
|
||||
count: string;
|
||||
total_amount: string;
|
||||
}>(
|
||||
`SELECT
|
||||
TO_CHAR(invoice_date, 'YYYY-MM') as year_month,
|
||||
cost_type,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount
|
||||
FROM tax_invoice
|
||||
WHERE ${whereClause}
|
||||
GROUP BY TO_CHAR(invoice_date, 'YYYY-MM'), cost_type
|
||||
ORDER BY year_month DESC, cost_type`,
|
||||
values
|
||||
);
|
||||
|
||||
// 전체 요약
|
||||
const summaryResult = await query<{
|
||||
total_count: string;
|
||||
total_amount: string;
|
||||
purchase_amount: string;
|
||||
installation_amount: string;
|
||||
repair_amount: string;
|
||||
maintenance_amount: string;
|
||||
disposal_amount: string;
|
||||
other_amount: string;
|
||||
}>(
|
||||
`SELECT
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'purchase' THEN total_amount ELSE 0 END), 0) as purchase_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'installation' THEN total_amount ELSE 0 END), 0) as installation_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'repair' THEN total_amount ELSE 0 END), 0) as repair_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'maintenance' THEN total_amount ELSE 0 END), 0) as maintenance_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'disposal' THEN total_amount ELSE 0 END), 0) as disposal_amount,
|
||||
COALESCE(SUM(CASE WHEN cost_type = 'other' THEN total_amount ELSE 0 END), 0) as other_amount
|
||||
FROM tax_invoice
|
||||
WHERE ${whereClause}`,
|
||||
values
|
||||
);
|
||||
|
||||
const summary = summaryResult[0] || {
|
||||
total_count: "0",
|
||||
total_amount: "0",
|
||||
purchase_amount: "0",
|
||||
installation_amount: "0",
|
||||
repair_amount: "0",
|
||||
maintenance_amount: "0",
|
||||
disposal_amount: "0",
|
||||
other_amount: "0",
|
||||
};
|
||||
|
||||
return {
|
||||
by_cost_type: byCostType.map((row) => ({
|
||||
cost_type: row.cost_type,
|
||||
count: parseInt(row.count, 10),
|
||||
supply_amount: parseFloat(row.supply_amount),
|
||||
tax_amount: parseFloat(row.tax_amount),
|
||||
total_amount: parseFloat(row.total_amount),
|
||||
})),
|
||||
by_month: byMonth.map((row) => ({
|
||||
year_month: row.year_month,
|
||||
cost_type: row.cost_type,
|
||||
count: parseInt(row.count, 10),
|
||||
total_amount: parseFloat(row.total_amount),
|
||||
})),
|
||||
summary: {
|
||||
total_count: parseInt(summary.total_count, 10),
|
||||
total_amount: parseFloat(summary.total_amount),
|
||||
purchase_amount: parseFloat(summary.purchase_amount),
|
||||
installation_amount: parseFloat(summary.installation_amount),
|
||||
repair_amount: parseFloat(summary.repair_amount),
|
||||
maintenance_amount: parseFloat(summary.maintenance_amount),
|
||||
disposal_amount: parseFloat(summary.disposal_amount),
|
||||
other_amount: parseFloat(summary.other_amount),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user