diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 5b5eb7d7..f826a86a 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -12,6 +12,7 @@ "@types/mssql": "^9.1.8", "axios": "^1.11.0", "bcryptjs": "^2.4.3", + "bwip-js": "^4.8.0", "compression": "^1.7.4", "cors": "^2.8.5", "docx": "^9.5.1", @@ -4540,6 +4541,15 @@ "node": ">=10.16.0" } }, + "node_modules/bwip-js": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.8.0.tgz", + "integrity": "sha512-gUDkDHSTv8/DJhomSIbO0fX/Dx0MO/sgllLxJyJfu4WixCQe9nfGJzmHm64ZCbxo+gUYQEsQcRmqcwcwPRwUkg==", + "license": "MIT", + "bin": { + "bwip-js": "bin/bwip-js.js" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", diff --git a/backend-node/package.json b/backend-node/package.json index e078043c..e9ce3729 100644 --- a/backend-node/package.json +++ b/backend-node/package.json @@ -26,6 +26,7 @@ "@types/mssql": "^9.1.8", "axios": "^1.11.0", "bcryptjs": "^2.4.3", + "bwip-js": "^4.8.0", "compression": "^1.7.4", "cors": "^2.8.5", "docx": "^9.5.1", diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index a2e8e8a9..438e02e1 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -28,6 +28,7 @@ import { PageOrientation, convertMillimetersToTwip, } from "docx"; +import bwipjs from "bwip-js"; export class ReportController { /** @@ -1326,6 +1327,43 @@ export class ReportController { ); } + // Barcode 컴포넌트 (바코드 이미지가 미리 생성되어 전달된 경우) + else if (component.type === "barcode" && component.barcodeImageBase64) { + try { + const base64Data = + component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64; + const imageBuffer = Buffer.from(base64Data, "base64"); + result.push( + new ParagraphRef({ + children: [ + new ImageRunRef({ + data: imageBuffer, + transformation: { + width: Math.round(component.width * 0.75), + height: Math.round(component.height * 0.75), + }, + type: "png", + }), + ], + }) + ); + } catch (e) { + // 바코드 이미지 생성 실패 시 텍스트로 대체 + const barcodeValue = component.barcodeValue || "BARCODE"; + result.push( + new ParagraphRef({ + children: [ + new TextRunRef({ + text: `[${barcodeValue}]`, + size: pxToHalfPtFn(12), + font: "맑은 고딕", + }), + ], + }) + ); + } + } + // Divider - 테이블 셀로 감싸서 정확한 너비 적용 else if ( component.type === "divider" && @@ -1354,6 +1392,82 @@ export class ReportController { return result; }; + // 바코드 이미지 생성 헬퍼 함수 + const generateBarcodeImage = async ( + component: any, + queryResultsMapRef: Record[] }> + ): Promise => { + try { + const barcodeType = component.barcodeType || "CODE128"; + const barcodeColor = (component.barcodeColor || "#000000").replace("#", ""); + const barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", ""); + + // 바코드 값 결정 (쿼리 바인딩 또는 고정값) + let barcodeValue = component.barcodeValue || "SAMPLE123"; + if (component.barcodeFieldName && component.queryId && queryResultsMapRef[component.queryId]) { + const qResult = queryResultsMapRef[component.queryId]; + if (qResult.rows && qResult.rows.length > 0) { + const row = qResult.rows[0]; + const val = row[component.barcodeFieldName]; + if (val !== null && val !== undefined) { + barcodeValue = String(val); + } + } + } + + // bwip-js 바코드 타입 매핑 + const bcidMap: Record = { + "CODE128": "code128", + "CODE39": "code39", + "EAN13": "ean13", + "EAN8": "ean8", + "UPC": "upca", + "QR": "qrcode", + }; + + const bcid = bcidMap[barcodeType] || "code128"; + const isQR = barcodeType === "QR"; + + // 바코드 옵션 설정 + const options: any = { + bcid: bcid, + text: barcodeValue, + scale: 3, + includetext: !isQR && component.showBarcodeText !== false, + textxalign: "center", + barcolor: barcodeColor, + backgroundcolor: barcodeBackground, + }; + + // QR 코드 옵션 + if (isQR) { + options.eclevel = component.qrErrorCorrectionLevel || "M"; + } + + // 바코드 이미지 생성 + const png = await bwipjs.toBuffer(options); + const base64 = png.toString("base64"); + return `data:image/png;base64,${base64}`; + } catch (error) { + console.error("바코드 생성 오류:", error); + return null; + } + }; + + // 모든 페이지의 바코드 컴포넌트에 대해 이미지 생성 + for (const page of layoutConfig.pages) { + if (page.components) { + for (const component of page.components) { + if (component.type === "barcode") { + const barcodeImage = await generateBarcodeImage(component, queryResultsMap); + if (barcodeImage) { + component.barcodeImageBase64 = barcodeImage; + } + } + } + } + } + // 섹션 생성 (페이지별) const sortedPages = layoutConfig.pages.sort( (a: any, b: any) => a.page_order - b.page_order @@ -2624,6 +2738,77 @@ export class ReportController { lastBottomY = adjustedY + component.height; } + // Barcode 컴포넌트 + else if (component.type === "barcode") { + if (component.barcodeImageBase64) { + try { + const base64Data = + component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64; + const imageBuffer = Buffer.from(base64Data, "base64"); + + // spacing을 위한 빈 paragraph + if (spacingBefore > 0) { + children.push( + new Paragraph({ + spacing: { before: spacingBefore, after: 0 }, + children: [], + }) + ); + } + + children.push( + new Paragraph({ + indent: { left: indentLeft }, + children: [ + new ImageRun({ + data: imageBuffer, + transformation: { + width: Math.round(component.width * 0.75), + height: Math.round(component.height * 0.75), + }, + type: "png", + }), + ], + }) + ); + } catch (imgError) { + console.error("바코드 이미지 오류:", imgError); + // 바코드 이미지 생성 실패 시 텍스트로 대체 + const barcodeValue = component.barcodeValue || "BARCODE"; + children.push( + new Paragraph({ + spacing: { before: spacingBefore, after: 0 }, + indent: { left: indentLeft }, + children: [ + new TextRun({ + text: `[${barcodeValue}]`, + size: pxToHalfPt(12), + font: "맑은 고딕", + }), + ], + }) + ); + } + } else { + // 바코드 이미지가 없는 경우 텍스트로 대체 + const barcodeValue = component.barcodeValue || "BARCODE"; + children.push( + new Paragraph({ + spacing: { before: spacingBefore, after: 0 }, + indent: { left: indentLeft }, + children: [ + new TextRun({ + text: `[${barcodeValue}]`, + size: pxToHalfPt(12), + font: "맑은 고딕", + }), + ], + }) + ); + } + lastBottomY = adjustedY + component.height; + } + // Table 컴포넌트 else if (component.type === "table" && component.queryId) { const queryResult = queryResultsMap[component.queryId]; diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts index 2fe1cfd3..e622a65c 100644 --- a/backend-node/src/types/report.ts +++ b/backend-node/src/types/report.ts @@ -166,3 +166,98 @@ export interface CreateTemplateRequest { layoutConfig?: any; defaultQueries?: any; } + +// 컴포넌트 설정 (프론트엔드와 동기화) +export interface ComponentConfig { + id: string; + type: string; + x: number; + y: number; + width: number; + height: number; + zIndex: number; + fontSize?: number; + fontFamily?: string; + fontWeight?: string; + fontColor?: string; + backgroundColor?: string; + borderWidth?: number; + borderColor?: string; + borderRadius?: number; + textAlign?: string; + padding?: number; + queryId?: string; + fieldName?: string; + defaultValue?: string; + format?: string; + visible?: boolean; + printable?: boolean; + conditional?: string; + locked?: boolean; + groupId?: string; + // 이미지 전용 + imageUrl?: string; + objectFit?: "contain" | "cover" | "fill" | "none"; + // 구분선 전용 + orientation?: "horizontal" | "vertical"; + lineStyle?: "solid" | "dashed" | "dotted" | "double"; + lineWidth?: number; + lineColor?: string; + // 서명/도장 전용 + showLabel?: boolean; + labelText?: string; + labelPosition?: "top" | "left" | "bottom" | "right"; + showUnderline?: boolean; + personName?: string; + // 테이블 전용 + tableColumns?: Array<{ + field: string; + header: string; + width?: number; + align?: "left" | "center" | "right"; + }>; + headerBackgroundColor?: string; + headerTextColor?: string; + showBorder?: boolean; + rowHeight?: number; + // 페이지 번호 전용 + pageNumberFormat?: "number" | "numberTotal" | "koreanNumber"; + // 카드 컴포넌트 전용 + cardTitle?: string; + cardItems?: Array<{ + label: string; + value: string; + fieldName?: string; + }>; + labelWidth?: number; + showCardBorder?: boolean; + showCardTitle?: boolean; + titleFontSize?: number; + labelFontSize?: number; + valueFontSize?: number; + titleColor?: string; + labelColor?: string; + valueColor?: string; + // 계산 컴포넌트 전용 + calcItems?: Array<{ + label: string; + value: number | string; + operator: "+" | "-" | "x" | "÷"; + fieldName?: string; + }>; + resultLabel?: string; + resultColor?: string; + resultFontSize?: number; + showCalcBorder?: boolean; + numberFormat?: "none" | "comma" | "currency"; + currencySuffix?: string; + // 바코드 컴포넌트 전용 + barcodeType?: "CODE128" | "CODE39" | "EAN13" | "EAN8" | "UPC" | "QR"; + barcodeValue?: string; + barcodeFieldName?: string; + showBarcodeText?: boolean; + barcodeColor?: string; + barcodeBackground?: string; + barcodeMargin?: number; + qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; +} diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index 01f0390b..e3f06e9f 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -1,9 +1,143 @@ "use client"; -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, useCallback } from "react"; import { ComponentConfig } from "@/types/report"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; import { getFullImageUrl } from "@/lib/api/client"; +import JsBarcode from "jsbarcode"; +import QRCode from "qrcode"; + +// 1D 바코드 렌더러 컴포넌트 +interface BarcodeRendererProps { + value: string; + format: string; + width: number; + height: number; + displayValue: boolean; + lineColor: string; + background: string; + margin: number; +} + +function BarcodeRenderer({ value, format, width, height, displayValue, lineColor, background, margin }: BarcodeRendererProps) { + const svgRef = useRef(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!svgRef.current || !value) return; + + // 매번 에러 상태 초기화 후 재검사 + setError(null); + + try { + // 바코드 형식에 따른 유효성 검사 + let isValid = true; + let errorMsg = ""; + const trimmedValue = value.trim(); + + if (format === "EAN13" && !/^\d{12,13}$/.test(trimmedValue)) { + isValid = false; + errorMsg = "EAN-13: 12~13자리 숫자 필요"; + } else if (format === "EAN8" && !/^\d{7,8}$/.test(trimmedValue)) { + isValid = false; + errorMsg = "EAN-8: 7~8자리 숫자 필요"; + } else if (format === "UPC" && !/^\d{11,12}$/.test(trimmedValue)) { + isValid = false; + errorMsg = "UPC: 11~12자리 숫자 필요"; + } + + if (!isValid) { + setError(errorMsg); + return; + } + + // JsBarcode는 format을 소문자로 받음 + const barcodeFormat = format.toLowerCase(); + + JsBarcode(svgRef.current, trimmedValue, { + format: barcodeFormat, + width: 2, + height: Math.max(30, height - (displayValue ? 30 : 10)), + displayValue: displayValue, + lineColor: lineColor, + background: background, + margin: margin, + fontSize: 12, + textMargin: 2, + }); + } catch (err: any) { + // JsBarcode 체크섬 오류 등 + setError(err?.message || "바코드 생성 실패"); + } + }, [value, format, width, height, displayValue, lineColor, background, margin]); + + return ( +
+ {/* SVG는 항상 렌더링 (에러 시 숨김) */} + + {/* 에러 메시지 오버레이 */} + {error && ( +
+ {error} + {value} +
+ )} +
+ ); +} + +// QR코드 렌더러 컴포넌트 +interface QRCodeRendererProps { + value: string; + size: number; + fgColor: string; + bgColor: string; + level: "L" | "M" | "Q" | "H"; +} + +function QRCodeRenderer({ value, size, fgColor, bgColor, level }: QRCodeRendererProps) { + const canvasRef = useRef(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (canvasRef.current && value) { + QRCode.toCanvas( + canvasRef.current, + value, + { + width: Math.max(50, size), + margin: 2, + color: { + dark: fgColor, + light: bgColor, + }, + errorCorrectionLevel: level, + }, + (err) => { + if (err) { + setError("QR코드 생성 실패"); + } else { + setError(null); + } + } + ); + } + }, [value, size, fgColor, bgColor, level]); + + if (error) { + return ( +
+ {error} + {value} +
+ ); + } + + return ; +} interface CanvasComponentProps { component: ComponentConfig; @@ -804,6 +938,70 @@ export function CanvasComponent({ component }: CanvasComponentProps) { ); + case "barcode": + // 바코드/QR코드 컴포넌트 렌더링 + const barcodeType = component.barcodeType || "CODE128"; + const showBarcodeText = component.showBarcodeText !== false; + const barcodeColor = component.barcodeColor || "#000000"; + const barcodeBackground = component.barcodeBackground || "#ffffff"; + const barcodeMargin = component.barcodeMargin ?? 10; + const qrErrorLevel = component.qrErrorCorrectionLevel || "M"; + + // 바코드 값 결정 (쿼리 바인딩 또는 고정값) + const getBarcodeValue = (): string => { + if (component.barcodeFieldName && component.queryId) { + const queryResult = getQueryResult(component.queryId); + if (queryResult && queryResult.rows && queryResult.rows.length > 0) { + const row = queryResult.rows[0]; + const val = row[component.barcodeFieldName]; + if (val !== null && val !== undefined) { + return String(val); + } + } + return `{${component.barcodeFieldName}}`; + } + return component.barcodeValue || "SAMPLE123"; + }; + + const barcodeValue = getBarcodeValue(); + const isQR = barcodeType === "QR"; + + return ( +
+
+ {isQR ? "QR코드" : `바코드 (${barcodeType})`} + {component.barcodeFieldName && component.queryId && ( + ● 연결됨 + )} +
+
+ {isQR ? ( + + ) : ( + + )} +
+
+ ); + default: return
알 수 없는 컴포넌트
; } diff --git a/frontend/components/report/designer/ComponentPalette.tsx b/frontend/components/report/designer/ComponentPalette.tsx index 9dd0543f..b7bdcbb0 100644 --- a/frontend/components/report/designer/ComponentPalette.tsx +++ b/frontend/components/report/designer/ComponentPalette.tsx @@ -1,7 +1,7 @@ "use client"; import { useDrag } from "react-dnd"; -import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator } from "lucide-react"; +import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator, Barcode } from "lucide-react"; interface ComponentItem { type: string; @@ -19,6 +19,7 @@ const COMPONENTS: ComponentItem[] = [ { type: "pageNumber", label: "페이지번호", icon: }, { type: "card", label: "정보카드", icon: }, { type: "calculation", label: "계산", icon: }, + { type: "barcode", label: "바코드/QR", icon: }, ]; function DraggableComponentItem({ type, label, icon }: ComponentItem) { diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index bcf9d88f..c4ea2a27 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -68,6 +68,9 @@ export function ReportDesignerCanvas() { } else if (item.componentType === "pageNumber") { width = 100; height = 30; + } else if (item.componentType === "barcode") { + width = 200; + height = 80; } // 여백을 px로 변환 (1mm ≈ 3.7795px) @@ -204,6 +207,17 @@ export function ReportDesignerCanvas() { showBorder: true, rowHeight: 32, }), + // 바코드 컴포넌트 전용 + ...(item.componentType === "barcode" && { + barcodeType: "CODE128" as const, + barcodeValue: "SAMPLE123", + barcodeFieldName: "", + showBarcodeText: true, + barcodeColor: "#000000", + barcodeBackground: "#ffffff", + barcodeMargin: 10, + qrErrorCorrectionLevel: "M" as const, + }), }; addComponent(newComponent); diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index ff832e21..58dd5047 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -1631,10 +1631,214 @@ export function ReportDesignerRightPanel() { )} - {/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */} + {/* 바코드 컴포넌트 설정 */} + {selectedComponent.type === "barcode" && ( + + + 바코드 설정 + + + {/* 바코드 타입 */} +
+ + +
+ + {/* 바코드 값 입력 (쿼리 연결 없을 때) */} + {!selectedComponent.queryId && ( +
+ + + updateComponent(selectedComponent.id, { + barcodeValue: e.target.value, + }) + } + placeholder={ + selectedComponent.barcodeType === "EAN13" ? "13자리 숫자" : + selectedComponent.barcodeType === "EAN8" ? "8자리 숫자" : + selectedComponent.barcodeType === "UPC" ? "12자리 숫자" : + "바코드에 표시할 값" + } + className="h-8" + /> + {(selectedComponent.barcodeType === "EAN13" || + selectedComponent.barcodeType === "EAN8" || + selectedComponent.barcodeType === "UPC") && ( +

+ {selectedComponent.barcodeType === "EAN13" && "EAN-13: 12~13자리 숫자 필요"} + {selectedComponent.barcodeType === "EAN8" && "EAN-8: 7~8자리 숫자 필요"} + {selectedComponent.barcodeType === "UPC" && "UPC: 11~12자리 숫자 필요"} +

+ )} +
+ )} + + {/* 쿼리 연결 시 필드 선택 */} + {selectedComponent.queryId && ( +
+ + +
+ )} + + {/* 1D 바코드 전용 옵션 */} + {selectedComponent.barcodeType !== "QR" && ( +
+ + updateComponent(selectedComponent.id, { + showBarcodeText: e.target.checked, + }) + } + className="h-4 w-4 rounded border-gray-300" + /> + +
+ )} + + {/* QR 오류 보정 수준 */} + {selectedComponent.barcodeType === "QR" && ( +
+ + +

+ 높을수록 손상에 강하지만 크기 증가 +

+
+ )} + + {/* 색상 설정 */} +
+
+ + + updateComponent(selectedComponent.id, { + barcodeColor: e.target.value, + }) + } + className="h-8 w-full" + /> +
+
+ + + updateComponent(selectedComponent.id, { + barcodeBackground: e.target.value, + }) + } + className="h-8 w-full" + /> +
+
+ + {/* 여백 */} +
+ + + updateComponent(selectedComponent.id, { + barcodeMargin: Number(e.target.value), + }) + } + min={0} + max={50} + className="h-8" + /> +
+ + {/* 쿼리 연결 안내 */} + {!selectedComponent.queryId && ( +
+ 쿼리를 연결하면 데이터베이스 값으로 바코드를 생성할 수 있습니다. +
+ )} +
+
+ )} + + {/* 데이터 바인딩 (텍스트/라벨/테이블/바코드 컴포넌트) */} {(selectedComponent.type === "text" || selectedComponent.type === "label" || - selectedComponent.type === "table") && ( + selectedComponent.type === "table" || + selectedComponent.type === "barcode") && (
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a0929370..e4f5a1fd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -46,6 +46,7 @@ "@turf/union": "^7.2.0", "@types/d3": "^7.4.3", "@types/leaflet": "^1.9.21", + "@types/qrcode": "^1.5.6", "@types/react-window": "^1.8.8", "@types/three": "^0.180.0", "@xyflow/react": "^12.8.4", @@ -61,11 +62,13 @@ "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "isomorphic-dompurify": "^2.28.0", + "jsbarcode": "^3.12.1", "jspdf": "^3.0.3", "leaflet": "^1.9.4", "lucide-react": "^0.525.0", "mammoth": "^1.11.0", "next": "^15.4.8", + "qrcode": "^1.5.4", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dnd": "^16.0.1", @@ -91,6 +94,7 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@tanstack/react-query-devtools": "^5.86.0", + "@types/jsbarcode": "^3.11.4", "@types/node": "^20", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", @@ -6022,6 +6026,16 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/jsbarcode": { + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/@types/jsbarcode/-/jsbarcode-3.11.4.tgz", + "integrity": "sha512-VBcpTAnEMH0Gbh8JpV14CgOtJjCYjsvR2FoDRyoYPE0gUxtApf8N4c+HKEOyz/iiIZkMzqrzBA3XX7+KgKxxsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6071,7 +6085,6 @@ "version": "20.19.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -6089,6 +6102,15 @@ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -6917,11 +6939,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7387,6 +7417,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camera-controls": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz", @@ -7529,6 +7568,17 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -7580,7 +7630,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -7593,7 +7642,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -8292,6 +8340,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -8420,6 +8477,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dingbat-to-unicode": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", @@ -9606,6 +9669,15 @@ "quickselect": "^1.0.1" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -10256,6 +10328,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -10593,6 +10674,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbarcode": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.1.tgz", + "integrity": "sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ==", + "license": "MIT" + }, "node_modules/jsdom": { "version": "27.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.1.0.tgz", @@ -11700,6 +11787,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -11735,7 +11831,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11818,6 +11913,15 @@ "pathe": "^2.0.3" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/point-in-polygon": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", @@ -12348,6 +12452,23 @@ ], "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12873,6 +12994,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -12882,6 +13012,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -13110,6 +13246,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13451,6 +13593,26 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -13564,6 +13726,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -14191,7 +14365,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -14511,6 +14684,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -14561,6 +14740,20 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -14675,6 +14868,99 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 37ba05be..e9cf087c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "@turf/union": "^7.2.0", "@types/d3": "^7.4.3", "@types/leaflet": "^1.9.21", + "@types/qrcode": "^1.5.6", "@types/react-window": "^1.8.8", "@types/three": "^0.180.0", "@xyflow/react": "^12.8.4", @@ -69,11 +70,13 @@ "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "isomorphic-dompurify": "^2.28.0", + "jsbarcode": "^3.12.1", "jspdf": "^3.0.3", "leaflet": "^1.9.4", "lucide-react": "^0.525.0", "mammoth": "^1.11.0", "next": "^15.4.8", + "qrcode": "^1.5.4", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dnd": "^16.0.1", @@ -99,6 +102,7 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@tanstack/react-query-devtools": "^5.86.0", + "@types/jsbarcode": "^3.11.4", "@types/node": "^20", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", diff --git a/frontend/types/report.ts b/frontend/types/report.ts index 46c8db89..bd4c8e68 100644 --- a/frontend/types/report.ts +++ b/frontend/types/report.ts @@ -189,6 +189,15 @@ export interface ComponentConfig { showCalcBorder?: boolean; // 테두리 표시 여부 numberFormat?: "none" | "comma" | "currency"; // 숫자 포맷 (없음, 천단위, 원화) currencySuffix?: string; // 통화 접미사 (예: "원") + // 바코드 컴포넌트 전용 + barcodeType?: "CODE128" | "CODE39" | "EAN13" | "EAN8" | "UPC" | "QR"; // 바코드 타입 + barcodeValue?: string; // 고정값 + barcodeFieldName?: string; // 쿼리 필드 바인딩 + showBarcodeText?: boolean; // 바코드 아래 텍스트 표시 (1D만) + barcodeColor?: string; // 바코드 색상 + barcodeBackground?: string; // 배경 색상 + barcodeMargin?: number; // 여백 + qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준 } // 리포트 상세