바코드 기능 커밋밋
This commit is contained in:
@@ -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); // 리스크/알림 관리
|
||||
|
||||
218
backend-node/src/controllers/barcodeLabelController.ts
Normal file
218
backend-node/src/controllers/barcodeLabelController.ts
Normal 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();
|
||||
41
backend-node/src/routes/barcodeLabelRoutes.ts
Normal file
41
backend-node/src/routes/barcodeLabelRoutes.ts
Normal 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;
|
||||
247
backend-node/src/services/barcodeLabelService.ts
Normal file
247
backend-node/src/services/barcodeLabelService.ts
Normal 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();
|
||||
61
backend-node/src/types/barcode.ts
Normal file
61
backend-node/src/types/barcode.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user