Merge pull request 'jskim-node' (#405) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/405
This commit is contained in:
commit
1ee946d712
|
|
@ -6,9 +6,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { BarcodeLabelComponent } from "@/types/barcode";
|
import { BarcodeLabelComponent } from "@/types/barcode";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const ITEMS: { type: BarcodeLabelComponent["type"]; label: string; icon: React.ReactNode }[] = [
|
const ITEMS: { type: BarcodeLabelComponent["type"]; label: string; icon: React.ReactNode; barcodeType?: string }[] = [
|
||||||
{ type: "text", label: "텍스트", icon: <Type className="h-4 w-4" /> },
|
{ type: "text", label: "텍스트", icon: <Type className="h-4 w-4" /> },
|
||||||
{ type: "barcode", label: "바코드", icon: <Barcode className="h-4 w-4" /> },
|
{ type: "barcode", label: "바코드", icon: <Barcode className="h-4 w-4" /> },
|
||||||
|
{ type: "barcode", label: "QR 코드", icon: <Barcode className="h-4 w-4" />, barcodeType: "QR" },
|
||||||
{ type: "image", label: "이미지", icon: <Image className="h-4 w-4" /> },
|
{ type: "image", label: "이미지", icon: <Image className="h-4 w-4" /> },
|
||||||
{ type: "line", label: "선", icon: <Minus className="h-4 w-4" /> },
|
{ type: "line", label: "선", icon: <Minus className="h-4 w-4" /> },
|
||||||
{ type: "rectangle", label: "사각형", icon: <Square className="h-4 w-4" /> },
|
{ type: "rectangle", label: "사각형", icon: <Square className="h-4 w-4" /> },
|
||||||
|
|
@ -16,22 +17,24 @@ const ITEMS: { type: BarcodeLabelComponent["type"]; label: string; icon: React.R
|
||||||
|
|
||||||
const MM_TO_PX = 4;
|
const MM_TO_PX = 4;
|
||||||
|
|
||||||
function defaultComponent(type: BarcodeLabelComponent["type"]): BarcodeLabelComponent {
|
function defaultComponent(type: BarcodeLabelComponent["type"], barcodeType?: string): BarcodeLabelComponent {
|
||||||
const id = `comp_${uuidv4()}`;
|
const id = `comp_${uuidv4()}`;
|
||||||
const base = { id, type, x: 10 * MM_TO_PX, y: 10 * MM_TO_PX, width: 80, height: 24, zIndex: 0 };
|
const base = { id, type, x: 10 * MM_TO_PX, y: 10 * MM_TO_PX, width: 80, height: 24, zIndex: 0 };
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "text":
|
case "text":
|
||||||
return { ...base, content: "텍스트", fontSize: 10, fontColor: "#000000" };
|
return { ...base, content: "텍스트", fontSize: 10, fontColor: "#000000" };
|
||||||
case "barcode":
|
case "barcode": {
|
||||||
|
const isQR = barcodeType === "QR";
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
width: 120,
|
width: isQR ? 100 : 120,
|
||||||
height: 40,
|
height: isQR ? 100 : 40,
|
||||||
barcodeType: "CODE128",
|
barcodeType: barcodeType || "CODE128",
|
||||||
barcodeValue: "123456789",
|
barcodeValue: isQR ? "" : "123456789",
|
||||||
showBarcodeText: true,
|
showBarcodeText: !isQR,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case "image":
|
case "image":
|
||||||
return { ...base, width: 60, height: 60, imageUrl: "", objectFit: "contain" };
|
return { ...base, width: 60, height: 60, imageUrl: "", objectFit: "contain" };
|
||||||
case "line":
|
case "line":
|
||||||
|
|
@ -47,14 +50,16 @@ function DraggableItem({
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
|
barcodeType,
|
||||||
}: {
|
}: {
|
||||||
type: BarcodeLabelComponent["type"];
|
type: BarcodeLabelComponent["type"];
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
barcodeType?: string;
|
||||||
}) {
|
}) {
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
type: "barcode-component",
|
type: "barcode-component",
|
||||||
item: { component: defaultComponent(type) },
|
item: { component: defaultComponent(type, barcodeType) },
|
||||||
collect: (m) => ({ isDragging: m.isDragging() }),
|
collect: (m) => ({ isDragging: m.isDragging() }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -78,8 +83,14 @@ export function BarcodeComponentPalette() {
|
||||||
<CardTitle className="text-sm">요소 추가</CardTitle>
|
<CardTitle className="text-sm">요소 추가</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
{ITEMS.map((item) => (
|
{ITEMS.map((item, idx) => (
|
||||||
<DraggableItem key={item.type} type={item.type} label={item.label} icon={item.icon} />
|
<DraggableItem
|
||||||
|
key={item.barcodeType ? `${item.type}_${item.barcodeType}` : `${item.type}_${idx}`}
|
||||||
|
type={item.type}
|
||||||
|
label={item.label}
|
||||||
|
icon={item.icon}
|
||||||
|
barcodeType={item.barcodeType}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,125 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2, Plus } from "lucide-react";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
|
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
|
||||||
import { BarcodeLabelComponent } from "@/types/barcode";
|
import { BarcodeLabelComponent } from "@/types/barcode";
|
||||||
|
|
||||||
|
// QR 기본 키: 품번, 품명, 규격
|
||||||
|
const DEFAULT_QR_JSON_KEYS = ["part_no", "part_name", "spec"];
|
||||||
|
|
||||||
|
function parseQRJsonValue(str: string): Record<string, string> {
|
||||||
|
const trimmed = (str || "").trim();
|
||||||
|
if (!trimmed) return {};
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(trimmed);
|
||||||
|
if (o && typeof o === "object" && !Array.isArray(o)) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(o).map(([k, v]) => [String(k), v != null ? String(v) : ""])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function QRJsonFields({
|
||||||
|
selected,
|
||||||
|
update,
|
||||||
|
}: {
|
||||||
|
selected: BarcodeLabelComponent;
|
||||||
|
update: (u: Partial<BarcodeLabelComponent>) => void;
|
||||||
|
}) {
|
||||||
|
const [pairs, setPairs] = useState<{ key: string; value: string }[]>(() => {
|
||||||
|
const parsed = parseQRJsonValue(selected.barcodeValue || "");
|
||||||
|
if (Object.keys(parsed).length > 0) {
|
||||||
|
return Object.entries(parsed).map(([key, value]) => ({ key, value }));
|
||||||
|
}
|
||||||
|
return DEFAULT_QR_JSON_KEYS.map((key) => ({ key, value: "" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 바코드 값이 바깥에서 바뀌면 파싱해서 동기화
|
||||||
|
useEffect(() => {
|
||||||
|
const parsed = parseQRJsonValue(selected.barcodeValue || "");
|
||||||
|
if (Object.keys(parsed).length > 0) {
|
||||||
|
setPairs(Object.entries(parsed).map(([key, value]) => ({ key, value: String(value ?? "") })));
|
||||||
|
}
|
||||||
|
}, [selected.barcodeValue]);
|
||||||
|
|
||||||
|
const applyJson = () => {
|
||||||
|
const obj: Record<string, string> = {};
|
||||||
|
pairs.forEach(({ key, value }) => {
|
||||||
|
const k = key.trim();
|
||||||
|
if (k) obj[k] = value.trim();
|
||||||
|
});
|
||||||
|
update({ barcodeValue: JSON.stringify(obj) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPair = (index: number, field: "key" | "value", val: string) => {
|
||||||
|
setPairs((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
if (!next[index]) next[index] = { key: "", value: "" };
|
||||||
|
next[index] = { ...next[index], [field]: val };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRow = () => setPairs((prev) => [...prev, { key: "", value: "" }]);
|
||||||
|
const removeRow = (index: number) =>
|
||||||
|
setPairs((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-primary/30 bg-muted/20 p-3">
|
||||||
|
<Label className="text-xs font-medium">여러 값 입력 → JSON으로 QR 생성</Label>
|
||||||
|
<p className="text-muted-foreground mt-0.5 text-[10px]">키는 자유 입력, 값 입력 후 적용 버튼을 누르면 QR에 반영됩니다.</p>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{pairs.map((p, i) => (
|
||||||
|
<div key={i} className="flex gap-1 items-center">
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 min-w-0 text-xs"
|
||||||
|
placeholder="키 (예: part_no)"
|
||||||
|
value={p.key}
|
||||||
|
onChange={(e) => setPair(i, "key", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 min-w-0 text-xs"
|
||||||
|
placeholder="값"
|
||||||
|
value={p.value}
|
||||||
|
onChange={(e) => setPair(i, "value", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 shrink-0 text-destructive"
|
||||||
|
onClick={() => removeRow(i)}
|
||||||
|
disabled={pairs.length <= 1}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex gap-1">
|
||||||
|
<Button type="button" size="sm" variant="outline" className="flex-1 gap-1" onClick={addRow}>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
필드 추가
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" className="flex-1" onClick={applyJson}>
|
||||||
|
JSON으로 QR 적용
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BarcodeDesignerRightPanel() {
|
export function BarcodeDesignerRightPanel() {
|
||||||
const {
|
const {
|
||||||
components,
|
components,
|
||||||
|
|
@ -56,8 +167,8 @@ export function BarcodeDesignerRightPanel() {
|
||||||
updateComponent(selected.id, updates);
|
updateComponent(selected.id, updates);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 border-l bg-white">
|
<div className="flex w-72 flex-col border-l bg-white overflow-hidden">
|
||||||
<div className="border-b p-2 flex items-center justify-between">
|
<div className="shrink-0 border-b p-2 flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">속성</span>
|
<span className="text-sm font-medium">속성</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -71,6 +182,7 @@ export function BarcodeDesignerRightPanel() {
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -161,12 +273,15 @@ export function BarcodeDesignerRightPanel() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
{selected.barcodeType === "QR" && (
|
||||||
|
<QRJsonFields selected={selected} update={update} />
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">값</Label>
|
<Label className="text-xs">{selected.barcodeType === "QR" ? "값 (직접 JSON 입력)" : "값"}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={selected.barcodeValue || ""}
|
value={selected.barcodeValue || ""}
|
||||||
onChange={(e) => update({ barcodeValue: e.target.value })}
|
onChange={(e) => update({ barcodeValue: e.target.value })}
|
||||||
placeholder="123456789"
|
placeholder={selected.barcodeType === "QR" ? '{"part_no":"","part_name":"","spec":""}' : "123456789"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -246,6 +361,7 @@ export function BarcodeDesignerRightPanel() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue