263 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|