feat: Add report cell value management functionality

- Introduced a new controller for managing custom input values in report cells, allowing users to retrieve and upsert values associated with specific reports and targets.
- Implemented API routes for fetching and saving report cell values, ensuring proper authentication and data handling.
- Enhanced the frontend components to support the new report cell input functionality, including the ability to edit and save input values in a modal.
- Updated inventory and equipment management pages to include new features for handling missing items and managing warehouse locations effectively.
This commit is contained in:
kjs
2026-04-20 17:59:28 +09:00
parent 725aa976bf
commit 51c4fddde0
35 changed files with 2696 additions and 295 deletions

View File

@@ -157,6 +157,7 @@ import shippingOrderRoutes from "./routes/shippingOrderRoutes"; // 출하지시
import workInstructionRoutes from "./routes/workInstructionRoutes"; // 작업지시 관리
import salesReportRoutes from "./routes/salesReportRoutes"; // 영업 리포트
import reportPresetRoutes from "./routes/reportPresetRoutes"; // 리포트 프리셋 저장 (회사별/리포트별)
import reportCellValueRoutes from "./routes/reportCellValueRoutes"; // 리포트 셀 커스텀 입력값 (input 셀)
import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형)
import systemNoticeRoutes from "./routes/systemNoticeRoutes"; // 시스템 공지
import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN)
@@ -381,6 +382,7 @@ app.use("/api/shipping-order", shippingOrderRoutes); // 출하지시 관리
app.use("/api/work-instruction", workInstructionRoutes); // 작업지시 관리
app.use("/api/sales-report", salesReportRoutes); // 영업 리포트
app.use("/api/report-presets", reportPresetRoutes); // 리포트 프리셋 (회사별/리포트별 저장)
app.use("/api/report-cell-values", reportCellValueRoutes); // 리포트 셀 커스텀 입력값
app.use("/api/system-notice", systemNoticeRoutes); // 시스템 공지
app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형)
app.use("/api/design", designRoutes); // 설계 모듈

View File

@@ -0,0 +1,93 @@
/**
* 리포트 셀 커스텀 입력값 컨트롤러
*
* 리포트 디자이너에서 cellType="input"으로 지정한 셀에 대해
* 각 대상 레코드(quote 등)별로 사용자가 입력한 값을 관리
*/
import type { Response } from "express";
import { getPool } from "../database/db";
import type { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
// 목록 조회: 특정 리포트 + 타겟에 대한 모든 셀 값
export async function getList(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { report_id, target_type, target_id } = req.query;
if (!report_id || !target_type || !target_id) {
return res.status(400).json({
success: false,
message: "report_id, target_type, target_id는 필수입니다.",
});
}
const pool = getPool();
const result = await pool.query(
`SELECT id, report_id, target_type, target_id, component_id, cell_id, value
FROM report_cell_values
WHERE company_code = $1 AND report_id = $2 AND target_type = $3 AND target_id = $4`,
[companyCode, report_id, target_type, target_id],
);
return res.json({ success: true, data: result.rows });
} catch (error: any) {
logger.error("리포트 셀 값 조회 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });
}
}
// UPSERT 단건: 같은 (report_id, target_type, target_id, component_id, cell_id)면 UPDATE, 아니면 INSERT
export async function upsert(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const { report_id, target_type, target_id, component_id, cell_id, value } =
req.body;
if (!report_id || !target_type || !target_id || !component_id || !cell_id) {
return res.status(400).json({
success: false,
message: "필수 필드 누락",
});
}
const pool = getPool();
// value가 빈 문자열이면 DELETE (오버라이드 해제)
if (value === "" || value === null || value === undefined) {
await pool.query(
`DELETE FROM report_cell_values
WHERE company_code = $1 AND report_id = $2 AND target_type = $3
AND target_id = $4 AND component_id = $5 AND cell_id = $6`,
[companyCode, report_id, target_type, target_id, component_id, cell_id],
);
return res.json({ success: true, data: null });
}
const result = await pool.query(
`INSERT INTO report_cell_values
(id, company_code, report_id, target_type, target_id, component_id, cell_id, value, created_by, updated_by)
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $8)
ON CONFLICT (company_code, report_id, target_type, target_id, component_id, cell_id)
DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP, updated_by = EXCLUDED.updated_by
RETURNING *`,
[
companyCode,
report_id,
target_type,
target_id,
component_id,
cell_id,
value,
userId,
],
);
return res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
logger.error("리포트 셀 값 저장 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });
}
}

View File

@@ -0,0 +1,12 @@
import { Router } from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import * as controller from "../controllers/reportCellValueController";
const router = Router();
router.use(authenticateToken);
router.get("/", controller.getList);
router.post("/", controller.upsert);
export default router;