바코드 기능 커밋밋

This commit is contained in:
2026-03-04 20:51:00 +09:00
parent 7ad17065f0
commit b9c0a0f243
23 changed files with 3100 additions and 0 deletions

View File

@@ -90,6 +90,7 @@ import externalCallConfigRoutes from "./routes/externalCallConfigRoutes";
import dataflowExecutionRoutes from "./routes/dataflowExecutionRoutes";
import dashboardRoutes from "./routes/dashboardRoutes";
import reportRoutes from "./routes/reportRoutes";
import barcodeLabelRoutes from "./routes/barcodeLabelRoutes";
import openApiProxyRoutes from "./routes/openApiProxyRoutes"; // 날씨/환율 API
import deliveryRoutes from "./routes/deliveryRoutes"; // 배송/화물 관리
import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관리
@@ -276,6 +277,7 @@ app.use("/api/external-call-configs", externalCallConfigRoutes);
app.use("/api/dataflow", dataflowExecutionRoutes);
app.use("/api/dashboards", dashboardRoutes);
app.use("/api/admin/reports", reportRoutes);
app.use("/api/admin/barcode-labels", barcodeLabelRoutes);
app.use("/api/open-api", openApiProxyRoutes); // 날씨/환율 외부 API
app.use("/api/delivery", deliveryRoutes); // 배송/화물 관리
app.use("/api/risk-alerts", riskAlertRoutes); // 리스크/알림 관리

View File

@@ -0,0 +1,218 @@
/**
* 바코드 라벨 관리 컨트롤러
* ZD421 등 바코드 프린터용 라벨 CRUD 및 레이아웃/템플릿
*/
import { Request, Response, NextFunction } from "express";
import barcodeLabelService from "../services/barcodeLabelService";
function getUserId(req: Request): string {
return (req as any).user?.userId || "SYSTEM";
}
export class BarcodeLabelController {
async getLabels(req: Request, res: Response, next: NextFunction) {
try {
const page = Math.max(1, parseInt((req.query.page as string) || "1", 10));
const limit = Math.min(100, Math.max(1, parseInt((req.query.limit as string) || "20", 10)));
const searchText = (req.query.searchText as string) || "";
const useYn = (req.query.useYn as string) || "Y";
const sortBy = (req.query.sortBy as string) || "created_at";
const sortOrder = (req.query.sortOrder as "ASC" | "DESC") || "DESC";
const data = await barcodeLabelService.getLabels({
page,
limit,
searchText,
useYn,
sortBy,
sortOrder,
});
return res.json({ success: true, data });
} catch (error) {
return next(error);
}
}
async getLabelById(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const label = await barcodeLabelService.getLabelById(labelId);
if (!label) {
return res.status(404).json({
success: false,
message: "바코드 라벨을 찾을 수 없습니다.",
});
}
return res.json({ success: true, data: label });
} catch (error) {
return next(error);
}
}
async getLayout(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const layout = await barcodeLabelService.getLayout(labelId);
if (!layout) {
return res.status(404).json({
success: false,
message: "레이아웃을 찾을 수 없습니다.",
});
}
return res.json({ success: true, data: layout });
} catch (error) {
return next(error);
}
}
async createLabel(req: Request, res: Response, next: NextFunction) {
try {
const body = req.body as {
labelNameKor?: string;
labelNameEng?: string;
description?: string;
templateId?: string;
};
if (!body?.labelNameKor?.trim()) {
return res.status(400).json({
success: false,
message: "라벨명(한글)은 필수입니다.",
});
}
const labelId = await barcodeLabelService.createLabel(
{
labelNameKor: body.labelNameKor.trim(),
labelNameEng: body.labelNameEng?.trim(),
description: body.description?.trim(),
templateId: body.templateId?.trim(),
},
getUserId(req)
);
return res.status(201).json({
success: true,
data: { labelId },
message: "바코드 라벨이 생성되었습니다.",
});
} catch (error) {
return next(error);
}
}
async updateLabel(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const body = req.body as {
labelNameKor?: string;
labelNameEng?: string;
description?: string;
useYn?: string;
};
const success = await barcodeLabelService.updateLabel(
labelId,
{
labelNameKor: body.labelNameKor?.trim(),
labelNameEng: body.labelNameEng?.trim(),
description: body.description !== undefined ? body.description : undefined,
useYn: body.useYn,
},
getUserId(req)
);
if (!success) {
return res.status(404).json({
success: false,
message: "바코드 라벨을 찾을 수 없습니다.",
});
}
return res.json({ success: true, message: "수정되었습니다." });
} catch (error) {
return next(error);
}
}
async saveLayout(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const layout = req.body as { width_mm: number; height_mm: number; components: any[] };
if (!layout || typeof layout.width_mm !== "number" || typeof layout.height_mm !== "number" || !Array.isArray(layout.components)) {
return res.status(400).json({
success: false,
message: "width_mm, height_mm, components 배열이 필요합니다.",
});
}
await barcodeLabelService.saveLayout(
labelId,
{ width_mm: layout.width_mm, height_mm: layout.height_mm, components: layout.components },
getUserId(req)
);
return res.json({ success: true, message: "레이아웃이 저장되었습니다." });
} catch (error) {
return next(error);
}
}
async deleteLabel(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const success = await barcodeLabelService.deleteLabel(labelId);
if (!success) {
return res.status(404).json({
success: false,
message: "바코드 라벨을 찾을 수 없습니다.",
});
}
return res.json({ success: true, message: "삭제되었습니다." });
} catch (error) {
return next(error);
}
}
async copyLabel(req: Request, res: Response, next: NextFunction) {
try {
const { labelId } = req.params;
const newId = await barcodeLabelService.copyLabel(labelId, getUserId(req));
if (!newId) {
return res.status(404).json({
success: false,
message: "바코드 라벨을 찾을 수 없습니다.",
});
}
return res.json({
success: true,
data: { labelId: newId },
message: "복사되었습니다.",
});
} catch (error) {
return next(error);
}
}
async getTemplates(req: Request, res: Response, next: NextFunction) {
try {
const templates = await barcodeLabelService.getTemplates();
return res.json({ success: true, data: templates });
} catch (error) {
return next(error);
}
}
async getTemplateById(req: Request, res: Response, next: NextFunction) {
try {
const { templateId } = req.params;
const template = await barcodeLabelService.getTemplateById(templateId);
if (!template) {
return res.status(404).json({
success: false,
message: "템플릿을 찾을 수 없습니다.",
});
}
const layout = JSON.parse(template.layout_json);
return res.json({ success: true, data: { ...template, layout } });
} catch (error) {
return next(error);
}
}
}
export default new BarcodeLabelController();

View File

@@ -0,0 +1,41 @@
import { Router } from "express";
import barcodeLabelController from "../controllers/barcodeLabelController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = Router();
router.use(authenticateToken);
router.get("/", (req, res, next) =>
barcodeLabelController.getLabels(req, res, next)
);
router.get("/templates", (req, res, next) =>
barcodeLabelController.getTemplates(req, res, next)
);
router.get("/templates/:templateId", (req, res, next) =>
barcodeLabelController.getTemplateById(req, res, next)
);
router.post("/", (req, res, next) =>
barcodeLabelController.createLabel(req, res, next)
);
router.get("/:labelId", (req, res, next) =>
barcodeLabelController.getLabelById(req, res, next)
);
router.get("/:labelId/layout", (req, res, next) =>
barcodeLabelController.getLayout(req, res, next)
);
router.put("/:labelId", (req, res, next) =>
barcodeLabelController.updateLabel(req, res, next)
);
router.put("/:labelId/layout", (req, res, next) =>
barcodeLabelController.saveLayout(req, res, next)
);
router.delete("/:labelId", (req, res, next) =>
barcodeLabelController.deleteLabel(req, res, next)
);
router.post("/:labelId/copy", (req, res, next) =>
barcodeLabelController.copyLabel(req, res, next)
);
export default router;

View File

@@ -0,0 +1,247 @@
/**
* 바코드 라벨 관리 서비스
* ZD421 등 라벨 디자인 CRUD 및 기본 템플릿 제공
*/
import { v4 as uuidv4 } from "uuid";
import { query, queryOne, transaction } from "../database/db";
import { BarcodeLabelLayout } from "../types/barcode";
export interface BarcodeLabelMaster {
label_id: string;
label_name_kor: string;
label_name_eng: string | null;
description: string | null;
width_mm: number;
height_mm: number;
layout_json: string | null;
use_yn: string;
created_at: string;
created_by: string | null;
updated_at: string | null;
updated_by: string | null;
}
export interface BarcodeLabelTemplate {
template_id: string;
template_name_kor: string;
template_name_eng: string | null;
width_mm: number;
height_mm: number;
layout_json: string;
sort_order: number;
}
export interface GetBarcodeLabelsParams {
page?: number;
limit?: number;
searchText?: string;
useYn?: string;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
}
export interface GetBarcodeLabelsResult {
items: BarcodeLabelMaster[];
total: number;
page: number;
limit: number;
}
export class BarcodeLabelService {
async getLabels(params: GetBarcodeLabelsParams): Promise<GetBarcodeLabelsResult> {
const {
page = 1,
limit = 20,
searchText = "",
useYn = "Y",
sortBy = "created_at",
sortOrder = "DESC",
} = params;
const offset = (page - 1) * limit;
const conditions: string[] = [];
const values: any[] = [];
let idx = 1;
if (useYn) {
conditions.push(`use_yn = $${idx++}`);
values.push(useYn);
}
if (searchText) {
conditions.push(`(label_name_kor LIKE $${idx} OR label_name_eng LIKE $${idx})`);
values.push(`%${searchText}%`);
idx++;
}
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const countSql = `SELECT COUNT(*) as total FROM barcode_labels ${where}`;
const countRow = await queryOne<{ total: string }>(countSql, values);
const total = parseInt(countRow?.total || "0", 10);
const listSql = `
SELECT label_id, label_name_kor, label_name_eng, description, width_mm, height_mm,
layout_json, use_yn, created_at, created_by, updated_at, updated_by
FROM barcode_labels ${where}
ORDER BY ${sortBy} ${sortOrder}
LIMIT $${idx++} OFFSET $${idx}
`;
const items = await query<BarcodeLabelMaster>(listSql, [...values, limit, offset]);
return { items, total, page, limit };
}
async getLabelById(labelId: string): Promise<BarcodeLabelMaster | null> {
const sql = `
SELECT label_id, label_name_kor, label_name_eng, description, width_mm, height_mm,
layout_json, use_yn, created_at, created_by, updated_at, updated_by
FROM barcode_labels WHERE label_id = $1
`;
return queryOne<BarcodeLabelMaster>(sql, [labelId]);
}
async getLayout(labelId: string): Promise<BarcodeLabelLayout | null> {
const row = await this.getLabelById(labelId);
if (!row?.layout_json) return null;
try {
return JSON.parse(row.layout_json) as BarcodeLabelLayout;
} catch {
return null;
}
}
async createLabel(
data: { labelNameKor: string; labelNameEng?: string; description?: string; templateId?: string },
userId: string
): Promise<string> {
const labelId = `LBL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
let widthMm = 50;
let heightMm = 30;
let layoutJson: string | null = null;
if (data.templateId) {
const t = await this.getTemplateById(data.templateId);
if (t) {
widthMm = t.width_mm;
heightMm = t.height_mm;
layoutJson = t.layout_json;
}
}
if (!layoutJson) {
const defaultLayout: BarcodeLabelLayout = {
width_mm: widthMm,
height_mm: heightMm,
components: [],
};
layoutJson = JSON.stringify(defaultLayout);
}
await query(
`INSERT INTO barcode_labels (label_id, label_name_kor, label_name_eng, description, width_mm, height_mm, layout_json, use_yn, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', $8)`,
[
labelId,
data.labelNameKor,
data.labelNameEng || null,
data.description || null,
widthMm,
heightMm,
layoutJson,
userId,
]
);
return labelId;
}
async updateLabel(
labelId: string,
data: { labelNameKor?: string; labelNameEng?: string; description?: string; useYn?: string },
userId: string
): Promise<boolean> {
const setClauses: string[] = [];
const values: any[] = [];
let idx = 1;
if (data.labelNameKor !== undefined) {
setClauses.push(`label_name_kor = $${idx++}`);
values.push(data.labelNameKor);
}
if (data.labelNameEng !== undefined) {
setClauses.push(`label_name_eng = $${idx++}`);
values.push(data.labelNameEng);
}
if (data.description !== undefined) {
setClauses.push(`description = $${idx++}`);
values.push(data.description);
}
if (data.useYn !== undefined) {
setClauses.push(`use_yn = $${idx++}`);
values.push(data.useYn);
}
if (setClauses.length === 0) return false;
setClauses.push(`updated_at = CURRENT_TIMESTAMP`);
setClauses.push(`updated_by = $${idx++}`);
values.push(userId);
values.push(labelId);
const updated = await query<{ label_id: string }>(
`UPDATE barcode_labels SET ${setClauses.join(", ")} WHERE label_id = $${idx} RETURNING label_id`,
values
);
return updated.length > 0;
}
async saveLayout(labelId: string, layout: BarcodeLabelLayout, userId: string): Promise<boolean> {
const layoutJson = JSON.stringify(layout);
await query(
`UPDATE barcode_labels SET width_mm = $1, height_mm = $2, layout_json = $3, updated_at = CURRENT_TIMESTAMP, updated_by = $4 WHERE label_id = $5`,
[layout.width_mm, layout.height_mm, layoutJson, userId, labelId]
);
return true;
}
async deleteLabel(labelId: string): Promise<boolean> {
const deleted = await query<{ label_id: string }>(
`DELETE FROM barcode_labels WHERE label_id = $1 RETURNING label_id`,
[labelId]
);
return deleted.length > 0;
}
async copyLabel(labelId: string, userId: string): Promise<string | null> {
const row = await this.getLabelById(labelId);
if (!row) return null;
const newId = `LBL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
await query(
`INSERT INTO barcode_labels (label_id, label_name_kor, label_name_eng, description, width_mm, height_mm, layout_json, use_yn, created_by)
VALUES ($1, $2 || ' (복사)', $3, $4, $5, $6, $7, 'Y', $8)`,
[
newId,
row.label_name_kor,
row.label_name_eng,
row.description,
row.width_mm,
row.height_mm,
row.layout_json,
userId,
]
);
return newId;
}
async getTemplates(): Promise<BarcodeLabelTemplate[]> {
const sql = `
SELECT template_id, template_name_kor, template_name_eng, width_mm, height_mm, layout_json, sort_order
FROM barcode_label_templates ORDER BY sort_order, template_id
`;
const rows = await query<BarcodeLabelTemplate>(sql);
return rows || [];
}
async getTemplateById(templateId: string): Promise<BarcodeLabelTemplate | null> {
const sql = `SELECT template_id, template_name_kor, template_name_eng, width_mm, height_mm, layout_json, sort_order
FROM barcode_label_templates WHERE template_id = $1`;
return queryOne<BarcodeLabelTemplate>(sql, [templateId]);
}
}
export default new BarcodeLabelService();

View File

@@ -0,0 +1,61 @@
/**
* 바코드 라벨 백엔드 타입
*/
export interface BarcodeLabelComponent {
id: string;
type: "text" | "barcode" | "image" | "line" | "rectangle";
x: number;
y: number;
width: number;
height: number;
zIndex: number;
// text
content?: string;
fontSize?: number;
fontColor?: string;
fontWeight?: string;
// barcode
barcodeType?: string;
barcodeValue?: string;
showBarcodeText?: boolean;
// image
imageUrl?: string;
objectFit?: string;
// line/rectangle
lineColor?: string;
lineWidth?: number;
backgroundColor?: string;
}
export interface BarcodeLabelLayout {
width_mm: number;
height_mm: number;
components: BarcodeLabelComponent[];
}
export interface BarcodeLabelRow {
label_id: string;
label_name_kor: string;
label_name_eng: string | null;
description: string | null;
width_mm: number;
height_mm: number;
layout_json: string | null;
use_yn: string;
created_at: string;
created_by: string | null;
updated_at: string | null;
updated_by: string | null;
}
export interface BarcodeLabelTemplateRow {
template_id: string;
template_name_kor: string;
template_name_eng: string | null;
width_mm: number;
height_mm: number;
layout_json: string;
sort_order: number;
created_at: string;
}