ERP-node/frontend/components/barcode/designer/BarcodeLabelCanvasComponent...

263 lines
7.1 KiB
TypeScript

"use client";
import { useRef, useState, useEffect } from "react";
import { BarcodeLabelComponent } from "@/types/barcode";
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
import JsBarcode from "jsbarcode";
import QRCode from "qrcode";
import { getFullImageUrl } from "@/lib/api/client";
import { MM_TO_PX } from "@/contexts/BarcodeDesignerContext";
interface Props {
component: BarcodeLabelComponent;
}
// 1D 바코드 렌더
function Barcode1DRender({
value,
format,
width,
height,
showText,
}: {
value: string;
format: string;
width: number;
height: number;
showText: boolean;
}) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current || !value.trim()) return;
try {
JsBarcode(svgRef.current, value.trim(), {
format: format.toLowerCase(),
width: 2,
height: Math.max(20, height - (showText ? 14 : 4)),
displayValue: showText,
margin: 2,
});
} catch {
// ignore
}
}, [value, format, height, showText]);
return (
<div className="flex h-full w-full items-center justify-center overflow-hidden">
<svg ref={svgRef} className="max-h-full max-w-full" />
</div>
);
}
// QR 렌더
function QRRender({ value, size }: { value: string; size: number }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!canvasRef.current || !value.trim()) return;
QRCode.toCanvas(canvasRef.current, value.trim(), {
width: Math.max(40, size),
margin: 1,
});
}, [value, size]);
return (
<div className="flex h-full w-full items-center justify-center overflow-hidden">
<canvas ref={canvasRef} />
</div>
);
}
export function BarcodeLabelCanvasComponent({ component }: Props) {
const {
updateComponent,
removeComponent,
selectComponent,
selectedComponentId,
snapValueToGrid,
} = useBarcodeDesigner();
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0, compX: 0, compY: 0 });
const [isResizing, setIsResizing] = useState(false);
const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, w: 0, h: 0 });
const ref = useRef<HTMLDivElement>(null);
const selected = selectedComponentId === component.id;
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
selectComponent(component.id);
if ((e.target as HTMLElement).closest("[data-resize-handle]")) {
setIsResizing(true);
setResizeStart({
x: e.clientX,
y: e.clientY,
w: component.width,
h: component.height,
});
} else {
setIsDragging(true);
setDragStart({ x: e.clientX, y: e.clientY, compX: component.x, compY: component.y });
}
};
useEffect(() => {
if (!isDragging && !isResizing) return;
const onMove = (e: MouseEvent) => {
if (isDragging) {
const dx = e.clientX - dragStart.x;
const dy = e.clientY - dragStart.y;
updateComponent(component.id, {
x: Math.max(0, snapValueToGrid(dragStart.compX + dx)),
y: Math.max(0, snapValueToGrid(dragStart.compY + dy)),
});
} else if (isResizing) {
const dx = e.clientX - resizeStart.x;
const dy = e.clientY - resizeStart.y;
updateComponent(component.id, {
width: Math.max(20, resizeStart.w + dx),
height: Math.max(10, resizeStart.h + dy),
});
}
};
const onUp = () => {
setIsDragging(false);
setIsResizing(false);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
return () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
}, [
isDragging,
isResizing,
dragStart,
resizeStart,
component.id,
updateComponent,
snapValueToGrid,
]);
const style: React.CSSProperties = {
position: "absolute",
left: component.x,
top: component.y,
width: component.width,
height: component.height,
zIndex: component.zIndex,
};
const border = selected ? "2px solid #2563eb" : "1px solid transparent";
const isBarcode = component.type === "barcode";
const isQR = component.barcodeType === "QR";
const content = () => {
switch (component.type) {
case "text":
return (
<div
style={{
fontSize: component.fontSize || 10,
color: component.fontColor || "#000",
fontWeight: component.fontWeight || "normal",
overflow: "hidden",
wordBreak: "break-all",
width: "100%",
height: "100%",
}}
>
{component.content || "텍스트"}
</div>
);
case "barcode":
if (isQR) {
return (
<QRRender
value={component.barcodeValue || ""}
size={Math.min(component.width, component.height)}
/>
);
}
return (
<Barcode1DRender
value={component.barcodeValue || "123456789"}
format={component.barcodeType || "CODE128"}
width={component.width}
height={component.height}
showText={component.showBarcodeText !== false}
/>
);
case "image":
return component.imageUrl ? (
<img
src={getFullImageUrl(component.imageUrl)}
alt=""
style={{
width: "100%",
height: "100%",
objectFit: (component.objectFit as "contain") || "contain",
}}
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-gray-100 text-xs text-gray-400">
</div>
);
case "line":
return (
<div
style={{
width: "100%",
height: component.lineWidth || 1,
backgroundColor: component.lineColor || "#000",
marginTop: (component.height - (component.lineWidth || 1)) / 2,
}}
/>
);
case "rectangle":
return (
<div
style={{
width: "100%",
height: "100%",
backgroundColor: component.backgroundColor || "transparent",
border: `${component.lineWidth || 1}px solid ${component.lineColor || "#000"}`,
}}
/>
);
default:
return null;
}
};
return (
<div
ref={ref}
style={{ ...style, border }}
className="cursor-move overflow-hidden bg-white"
onMouseDown={handleMouseDown}
>
{content()}
{selected && component.type !== "line" && (
<div
data-resize-handle
className="absolute bottom-0 right-0 h-2 w-2 cursor-se-resize bg-blue-500"
onMouseDown={(e) => {
e.stopPropagation();
setIsResizing(true);
setResizeStart({
x: e.clientX,
y: e.clientY,
w: component.width,
h: component.height,
});
}}
/>
)}
</div>
);
}