ERP-node/frontend/components/barcode/designer/BarcodeDesignerCanvas.tsx

139 lines
4.7 KiB
TypeScript

"use client";
import { useRef, useState, useEffect } from "react";
import { useDrop } from "react-dnd";
import { useBarcodeDesigner, MM_TO_PX } from "@/contexts/BarcodeDesignerContext";
import { BarcodeLabelCanvasComponent } from "./BarcodeLabelCanvasComponent";
import { BarcodeLabelComponent } from "@/types/barcode";
import { v4 as uuidv4 } from "uuid";
/** 작업 영역에 라벨이 들어가도록 스케일 (최소 0.5=작게 맞춤, 최대 3) */
const MIN_SCALE = 0.5;
const MAX_SCALE = 3;
export function BarcodeDesignerCanvas() {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const {
widthMm,
heightMm,
components,
addComponent,
selectComponent,
showGrid,
snapValueToGrid,
} = useBarcodeDesigner();
const widthPx = widthMm * MM_TO_PX;
const heightPx = heightMm * MM_TO_PX;
// 컨테이너 크기에 맞춰 캔버스 스케일 계산 (라벨이 너무 작게 보이지 않도록)
useEffect(() => {
const el = containerRef.current;
if (!el || widthPx <= 0 || heightPx <= 0) return;
const observer = new ResizeObserver(() => {
const w = el.clientWidth - 48;
const h = el.clientHeight - 48;
if (w <= 0 || h <= 0) return;
const scaleX = w / widthPx;
const scaleY = h / heightPx;
const fitScale = Math.min(scaleX, scaleY);
const s = Math.max(MIN_SCALE, Math.min(MAX_SCALE, fitScale));
setScale(s);
});
observer.observe(el);
const w = el.clientWidth - 48;
const h = el.clientHeight - 48;
if (w > 0 && h > 0) {
const scaleX = w / widthPx;
const scaleY = h / heightPx;
const fitScale = Math.min(scaleX, scaleY);
const s = Math.max(MIN_SCALE, Math.min(MAX_SCALE, fitScale));
setScale(s);
}
return () => observer.disconnect();
}, [widthPx, heightPx]);
const [{ isOver }, drop] = useDrop(() => ({
accept: "barcode-component",
drop: (item: { component: BarcodeLabelComponent }, monitor) => {
const canvasEl = canvasRef.current;
if (!canvasEl) return;
const offset = monitor.getClientOffset();
const rect = canvasEl.getBoundingClientRect();
if (!offset) return;
// 스케일 적용된 좌표 → 실제 캔버스 좌표
const s = scale;
let x = (offset.x - rect.left) / s;
let y = (offset.y - rect.top) / s;
x -= item.component.width / 2;
y -= item.component.height / 2;
x = Math.max(0, Math.min(x, widthPx - item.component.width));
y = Math.max(0, Math.min(y, heightPx - item.component.height));
const newComp: BarcodeLabelComponent = {
...item.component,
id: `comp_${uuidv4()}`,
x: snapValueToGrid(x),
y: snapValueToGrid(y),
zIndex: components.length,
};
addComponent(newComp);
},
collect: (m) => ({ isOver: m.isOver() }),
}), [widthPx, heightPx, scale, components.length, addComponent, snapValueToGrid]);
// 스케일된 캔버스가 컨테이너 안에 들어가도록 fit (하단 잘림 방지)
const scaledW = widthPx * scale;
const scaledH = heightPx * scale;
return (
<div
ref={containerRef}
className="flex min-h-0 flex-1 items-center justify-center overflow-auto bg-gray-100 p-6"
>
{/* 래퍼: 스케일된 크기만큼 차지해서 flex로 정확히 가운데 + 하단 잘림 방지 */}
<div
className="flex shrink-0 items-center justify-center"
style={{ width: scaledW, height: scaledH }}
>
<div
style={{
transform: `scale(${scale})`,
transformOrigin: "0 0",
}}
>
<div
key={`canvas-${widthMm}-${heightMm}`}
ref={(r) => {
(canvasRef as { current: HTMLDivElement | null }).current = r;
drop(r);
}}
className="relative bg-white shadow-lg"
style={{
width: widthPx,
height: heightPx,
minWidth: widthPx,
minHeight: heightPx,
backgroundImage: showGrid
? `linear-gradient(to right, #e5e7eb 1px, transparent 1px),
linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)`
: undefined,
backgroundSize: showGrid ? `${MM_TO_PX * 5}px ${MM_TO_PX * 5}px` : undefined,
outline: isOver ? "2px dashed #2563eb" : "1px solid #d1d5db",
}}
onClick={(e) => {
if (e.target === e.currentTarget) selectComponent(null);
}}
>
{components.map((c) => (
<BarcodeLabelCanvasComponent key={c.id} component={c} />
))}
</div>
</div>
</div>
</div>
);
}