Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
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();
|
||||
@@ -20,7 +20,7 @@ const pool = getPool();
|
||||
export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { page = 1, size = 20, searchTerm } = req.query;
|
||||
const { page = 1, size = 20, searchTerm, excludePop } = req.query;
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(size as string);
|
||||
|
||||
let whereClause = "WHERE 1=1";
|
||||
@@ -34,6 +34,11 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response)
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// POP 그룹 제외 (PC 화면관리용)
|
||||
if (excludePop === "true") {
|
||||
whereClause += ` AND (hierarchy_path IS NULL OR (hierarchy_path NOT LIKE 'POP/%' AND hierarchy_path != 'POP'))`;
|
||||
}
|
||||
|
||||
// 검색어 필터링
|
||||
if (searchTerm) {
|
||||
whereClause += ` AND (group_name ILIKE $${paramIndex} OR group_code ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`;
|
||||
@@ -2573,11 +2578,11 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { searchTerm } = req.query;
|
||||
|
||||
let whereClause = "WHERE hierarchy_path LIKE 'POP/%' OR hierarchy_path = 'POP'";
|
||||
let whereClause = "WHERE (hierarchy_path LIKE 'POP/%' OR hierarchy_path = 'POP')";
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사 코드 필터링 (멀티테넌시)
|
||||
// 회사 코드 필터링 (멀티테넌시) - 일반 회사는 자기 회사 데이터만
|
||||
if (companyCode !== "*") {
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
params.push(companyCode);
|
||||
@@ -2591,11 +2596,13 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// POP 그룹 조회 (계층 구조를 위해 전체 조회)
|
||||
// POP 그룹 조회 (POP 레이아웃이 있는 화면만 카운트/포함)
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
sg.*,
|
||||
(SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count,
|
||||
(SELECT COUNT(*) FROM screen_group_screens sgs
|
||||
INNER JOIN screen_layouts_pop slp ON sgs.screen_id = slp.screen_id AND sgs.company_code = slp.company_code
|
||||
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code) as screen_count,
|
||||
(SELECT json_agg(
|
||||
json_build_object(
|
||||
'id', sgs.id,
|
||||
@@ -2608,7 +2615,8 @@ export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Respons
|
||||
) ORDER BY sgs.display_order
|
||||
) FROM screen_group_screens sgs
|
||||
LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
||||
WHERE sgs.group_id = sg.id
|
||||
INNER JOIN screen_layouts_pop slp ON sgs.screen_id = slp.screen_id AND sgs.company_code = slp.company_code
|
||||
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code
|
||||
) as screens
|
||||
FROM screen_groups sg
|
||||
${whereClause}
|
||||
@@ -2767,6 +2775,14 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
|
||||
|
||||
const existing = await pool.query(checkQuery, checkParams);
|
||||
if (existing.rows.length === 0) {
|
||||
// 그룹이 다른 회사 소속인지 확인하여 구체적 메시지 제공
|
||||
const anyGroup = await pool.query(`SELECT company_code FROM screen_groups WHERE id = $1`, [id]);
|
||||
if (anyGroup.rows.length > 0) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: `이 그룹은 ${anyGroup.rows[0].company_code === "*" ? "최고관리자" : anyGroup.rows[0].company_code} 소속이라 삭제할 수 없습니다.`
|
||||
});
|
||||
}
|
||||
return res.status(404).json({ success: false, message: "그룹을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
@@ -2781,7 +2797,10 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
|
||||
[id]
|
||||
);
|
||||
if (parseInt(childCheck.rows[0].count) > 0) {
|
||||
return res.status(400).json({ success: false, message: "하위 그룹이 있어 삭제할 수 없습니다." });
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `하위 그룹이 ${childCheck.rows[0].count}개 있어 삭제할 수 없습니다. 하위 그룹을 먼저 삭제해주세요.`
|
||||
});
|
||||
}
|
||||
|
||||
// 연결된 화면 확인
|
||||
@@ -2790,7 +2809,10 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
|
||||
[id]
|
||||
);
|
||||
if (parseInt(screenCheck.rows[0].count) > 0) {
|
||||
return res.status(400).json({ success: false, message: "그룹에 연결된 화면이 있어 삭제할 수 없습니다." });
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `그룹에 연결된 화면이 ${screenCheck.rows[0].count}개 있어 삭제할 수 없습니다. 화면을 먼저 제거해주세요.`
|
||||
});
|
||||
}
|
||||
|
||||
// 삭제
|
||||
@@ -2805,33 +2827,44 @@ export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Respo
|
||||
}
|
||||
};
|
||||
|
||||
// POP 루트 그룹 확보 (없으면 자동 생성)
|
||||
// POP 루트 그룹 확보 (최고관리자 전용 - 일반 회사는 복사 기능으로 배포)
|
||||
export const ensurePopRootGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// POP 루트 그룹 확인
|
||||
const checkQuery = `
|
||||
SELECT * FROM screen_groups
|
||||
WHERE hierarchy_path = 'POP' AND company_code = $1
|
||||
`;
|
||||
const existing = await pool.query(checkQuery, [companyCode]);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
return res.json({ success: true, data: existing.rows[0], message: "POP 루트 그룹이 이미 존재합니다." });
|
||||
// 최고관리자만 자동 생성
|
||||
if (companyCode !== "*") {
|
||||
const existing = await pool.query(
|
||||
`SELECT * FROM screen_groups WHERE hierarchy_path = 'POP' AND company_code = $1`,
|
||||
[companyCode]
|
||||
);
|
||||
if (existing.rows.length > 0) {
|
||||
return res.json({ success: true, data: existing.rows[0] });
|
||||
}
|
||||
return res.json({ success: true, data: null, message: "POP 루트 그룹이 없습니다." });
|
||||
}
|
||||
|
||||
// 최고관리자(*): 루트 그룹 확인 후 없으면 생성
|
||||
const checkQuery = `
|
||||
SELECT * FROM screen_groups
|
||||
WHERE hierarchy_path = 'POP' AND company_code = '*'
|
||||
`;
|
||||
const existing = await pool.query(checkQuery, []);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
return res.json({ success: true, data: existing.rows[0] });
|
||||
}
|
||||
|
||||
// 없으면 생성 (writer 컬럼 사용, is_active는 'Y' - 기존 스키마에 맞춤)
|
||||
const insertQuery = `
|
||||
INSERT INTO screen_groups (
|
||||
group_name, group_code, hierarchy_path, company_code,
|
||||
description, display_order, is_active, writer
|
||||
) VALUES ('POP 화면', 'POP', 'POP', $1, 'POP 화면 관리 루트', 0, 'Y', $2)
|
||||
) VALUES ('POP 화면', 'POP', 'POP', '*', 'POP 화면 관리 루트', 0, 'Y', $1)
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await pool.query(insertQuery, [companyCode, req.user?.userId || ""]);
|
||||
const result = await pool.query(insertQuery, [req.user?.userId || ""]);
|
||||
|
||||
logger.info("POP 루트 그룹 생성", { groupId: result.rows[0].id, companyCode });
|
||||
logger.info("POP 루트 그룹 생성", { groupId: result.rows[0].id });
|
||||
|
||||
res.json({ success: true, data: result.rows[0], message: "POP 루트 그룹이 생성되었습니다." });
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { auditLogService, getClientIp } from "../services/auditLogService";
|
||||
export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const { page = 1, size = 20, searchTerm, companyCode } = req.query;
|
||||
const { page = 1, size = 20, searchTerm, companyCode, excludePop } = req.query;
|
||||
|
||||
// 쿼리 파라미터로 companyCode가 전달되면 해당 회사의 화면 조회 (최고 관리자 전용)
|
||||
// 아니면 현재 사용자의 companyCode 사용
|
||||
@@ -25,7 +25,8 @@ export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
targetCompanyCode,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string),
|
||||
searchTerm as string // 검색어 전달
|
||||
searchTerm as string,
|
||||
{ excludePop: excludePop === "true" },
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -1537,3 +1538,82 @@ export const copyCascadingRelation = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// POP 화면 연결 분석
|
||||
export const analyzePopScreenLinks = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const { companyCode } = req.user as any;
|
||||
|
||||
const result = await screenManagementService.analyzePopScreenLinks(
|
||||
parseInt(screenId),
|
||||
companyCode,
|
||||
);
|
||||
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
console.error("POP 화면 연결 분석 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "POP 화면 연결 분석에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// POP 화면 배포 (다른 회사로 복사)
|
||||
export const deployPopScreens = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screens, targetCompanyCode, groupStructure } = req.body;
|
||||
const { companyCode, userId } = req.user as any;
|
||||
|
||||
if (!screens || !Array.isArray(screens) || screens.length === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "배포할 화면 목록이 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetCompanyCode) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "대상 회사 코드가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (companyCode !== "*") {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 POP 화면을 배포할 수 있습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await screenManagementService.deployPopScreens({
|
||||
screens,
|
||||
groupStructure: groupStructure || undefined,
|
||||
targetCompanyCode,
|
||||
companyCode,
|
||||
userId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `POP 화면 ${result.deployedScreens.length}개가 ${targetCompanyCode}에 배포되었습니다.`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("POP 화면 배포 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "POP 화면 배포에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user