바코드 기능 커밋밋
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user