2026-03-04 20:51:00 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-03-05 19:08:08 +09:00
|
|
|
import { useRef, useState, useEffect } from "react";
|
2026-03-04 20:51:00 +09:00
|
|
|
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";
|
|
|
|
|
|
2026-03-05 19:08:08 +09:00
|
|
|
/** 작업 영역에 라벨이 들어가도록 스케일 (최소 0.5=작게 맞춤, 최대 3) */
|
|
|
|
|
const MIN_SCALE = 0.5;
|
|
|
|
|
const MAX_SCALE = 3;
|
|
|
|
|
|
2026-03-04 20:51:00 +09:00
|
|
|
export function BarcodeDesignerCanvas() {
|
2026-03-05 19:08:08 +09:00
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2026-03-04 20:51:00 +09:00
|
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
2026-03-05 19:08:08 +09:00
|
|
|
const [scale, setScale] = useState(1);
|
2026-03-04 20:51:00 +09:00
|
|
|
const {
|
|
|
|
|
widthMm,
|
|
|
|
|
heightMm,
|
|
|
|
|
components,
|
|
|
|
|
addComponent,
|
|
|
|
|
selectComponent,
|
|
|
|
|
showGrid,
|
|
|
|
|
snapValueToGrid,
|
|
|
|
|
} = useBarcodeDesigner();
|
|
|
|
|
|
|
|
|
|
const widthPx = widthMm * MM_TO_PX;
|
|
|
|
|
const heightPx = heightMm * MM_TO_PX;
|
|
|
|
|
|
2026-03-05 19:08:08 +09:00
|
|
|
// 컨테이너 크기에 맞춰 캔버스 스케일 계산 (라벨이 너무 작게 보이지 않도록)
|
|
|
|
|
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]);
|
|
|
|
|
|
2026-03-04 20:51:00 +09:00
|
|
|
const [{ isOver }, drop] = useDrop(() => ({
|
|
|
|
|
accept: "barcode-component",
|
|
|
|
|
drop: (item: { component: BarcodeLabelComponent }, monitor) => {
|
2026-03-05 19:08:08 +09:00
|
|
|
const canvasEl = canvasRef.current;
|
|
|
|
|
if (!canvasEl) return;
|
2026-03-04 20:51:00 +09:00
|
|
|
const offset = monitor.getClientOffset();
|
2026-03-05 19:08:08 +09:00
|
|
|
const rect = canvasEl.getBoundingClientRect();
|
2026-03-04 20:51:00 +09:00
|
|
|
if (!offset) return;
|
2026-03-05 19:08:08 +09:00
|
|
|
// 스케일 적용된 좌표 → 실제 캔버스 좌표
|
|
|
|
|
const s = scale;
|
|
|
|
|
let x = (offset.x - rect.left) / s;
|
|
|
|
|
let y = (offset.y - rect.top) / s;
|
2026-03-04 20:51:00 +09:00
|
|
|
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() }),
|
2026-03-05 19:08:08 +09:00
|
|
|
}), [widthPx, heightPx, scale, components.length, addComponent, snapValueToGrid]);
|
|
|
|
|
|
|
|
|
|
// 스케일된 캔버스가 컨테이너 안에 들어가도록 fit (하단 잘림 방지)
|
|
|
|
|
const scaledW = widthPx * scale;
|
|
|
|
|
const scaledH = heightPx * scale;
|
2026-03-04 20:51:00 +09:00
|
|
|
|
|
|
|
|
return (
|
2026-03-05 19:08:08 +09:00
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
className="flex min-h-0 flex-1 items-center justify-center overflow-auto bg-gray-100 p-6"
|
|
|
|
|
>
|
|
|
|
|
{/* 래퍼: 스케일된 크기만큼 차지해서 flex로 정확히 가운데 + 하단 잘림 방지 */}
|
2026-03-04 20:51:00 +09:00
|
|
|
<div
|
2026-03-05 19:08:08 +09:00
|
|
|
className="flex shrink-0 items-center justify-center"
|
|
|
|
|
style={{ width: scaledW, height: scaledH }}
|
2026-03-04 20:51:00 +09:00
|
|
|
>
|
2026-03-05 19:08:08 +09:00
|
|
|
<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>
|
2026-03-04 20:51:00 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|