Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
arrayMove,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { FormatSegment } from "./types";
|
||||
import { SEGMENT_TYPE_LABELS, buildFormattedString, SAMPLE_VALUES } from "./config";
|
||||
|
||||
// 개별 세그먼트 행
|
||||
interface SortableSegmentRowProps {
|
||||
segment: FormatSegment;
|
||||
index: number;
|
||||
onChange: (index: number, updates: Partial<FormatSegment>) => void;
|
||||
}
|
||||
|
||||
function SortableSegmentRow({ segment, index, onChange }: SortableSegmentRowProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: `${segment.type}-${index}` });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"grid grid-cols-[16px_56px_18px_1fr_1fr_1fr] items-center gap-1 rounded border bg-white px-2 py-1.5",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<GripVertical className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
|
||||
<span className="truncate text-xs font-medium">
|
||||
{SEGMENT_TYPE_LABELS[segment.type]}
|
||||
</span>
|
||||
|
||||
<Checkbox
|
||||
checked={segment.showLabel}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange(index, { showLabel: checked === true })
|
||||
}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
|
||||
<Input
|
||||
value={segment.label}
|
||||
onChange={(e) => onChange(index, { label: e.target.value })}
|
||||
placeholder=""
|
||||
className={cn(
|
||||
"h-6 px-1 text-xs",
|
||||
!segment.showLabel && "text-gray-400 line-through",
|
||||
)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
value={segment.separatorAfter}
|
||||
onChange={(e) => onChange(index, { separatorAfter: e.target.value })}
|
||||
placeholder=""
|
||||
className="h-6 px-1 text-center text-xs"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={5}
|
||||
value={segment.pad}
|
||||
onChange={(e) =>
|
||||
onChange(index, { pad: parseInt(e.target.value) || 0 })
|
||||
}
|
||||
disabled={segment.type !== "row" && segment.type !== "level"}
|
||||
className={cn(
|
||||
"h-6 px-1 text-center text-xs",
|
||||
segment.type !== "row" && segment.type !== "level" && "bg-gray-100 opacity-50",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// FormatSegmentEditor 메인 컴포넌트
|
||||
interface FormatSegmentEditorProps {
|
||||
label: string;
|
||||
segments: FormatSegment[];
|
||||
onChange: (segments: FormatSegment[]) => void;
|
||||
sampleValues?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function FormatSegmentEditor({
|
||||
label,
|
||||
segments,
|
||||
onChange,
|
||||
sampleValues = SAMPLE_VALUES,
|
||||
}: FormatSegmentEditorProps) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 5 },
|
||||
}),
|
||||
useSensor(KeyboardSensor),
|
||||
);
|
||||
|
||||
const preview = useMemo(
|
||||
() => buildFormattedString(segments, sampleValues),
|
||||
[segments, sampleValues],
|
||||
);
|
||||
|
||||
const sortableIds = useMemo(
|
||||
() => segments.map((seg, i) => `${seg.type}-${i}`),
|
||||
[segments],
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = sortableIds.indexOf(active.id as string);
|
||||
const newIndex = sortableIds.indexOf(over.id as string);
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
|
||||
onChange(arrayMove([...segments], oldIndex, newIndex));
|
||||
};
|
||||
|
||||
const handleSegmentChange = (index: number, updates: Partial<FormatSegment>) => {
|
||||
const updated = segments.map((seg, i) =>
|
||||
i === index ? { ...seg, ...updates } : seg,
|
||||
);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-gray-600">{label}</div>
|
||||
|
||||
<div className="grid grid-cols-[16px_56px_18px_1fr_1fr_1fr] items-center gap-1 px-2 text-[10px] text-gray-500">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span>라벨</span>
|
||||
<span className="text-center">구분</span>
|
||||
<span className="text-center">자릿수</span>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-1">
|
||||
{segments.map((segment, index) => (
|
||||
<SortableSegmentRow
|
||||
key={sortableIds[index]}
|
||||
segment={segment}
|
||||
index={index}
|
||||
onChange={handleSegmentChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
<div className="rounded bg-gray-50 px-2 py-1.5">
|
||||
<span className="text-[10px] text-gray-500">미리보기: </span>
|
||||
<span className="text-xs font-medium text-gray-800">
|
||||
{preview || "(빈 값)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
GeneratedLocation,
|
||||
RackStructureContext,
|
||||
} from "./types";
|
||||
import { defaultFormatConfig, buildFormattedString } from "./config";
|
||||
|
||||
// 기존 위치 데이터 타입
|
||||
interface ExistingLocation {
|
||||
@@ -95,12 +96,12 @@ const ConditionCard: React.FC<ConditionCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-white shadow-sm">
|
||||
<div className="border-border relative rounded-lg border bg-white shadow-sm">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between rounded-t-lg bg-primary px-4 py-2 text-white">
|
||||
<div className="bg-primary flex items-center justify-between rounded-t-lg px-4 py-2 text-white">
|
||||
<span className="font-medium">조건 {index + 1}</span>
|
||||
{!readonly && (
|
||||
<button onClick={() => onRemove(condition.id)} className="rounded p-1 transition-colors hover:bg-primary/90">
|
||||
<button onClick={() => onRemove(condition.id)} className="hover:bg-primary/90 rounded p-1 transition-colors">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
@@ -111,7 +112,7 @@ const ConditionCard: React.FC<ConditionCardProps> = ({
|
||||
{/* 열 범위 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-foreground">
|
||||
<label className="text-foreground mb-1 block text-xs font-medium">
|
||||
열 범위 <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -139,7 +140,7 @@ const ConditionCard: React.FC<ConditionCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-20">
|
||||
<label className="mb-1 block text-xs font-medium text-foreground">
|
||||
<label className="text-foreground mb-1 block text-xs font-medium">
|
||||
단 수 <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
@@ -156,7 +157,7 @@ const ConditionCard: React.FC<ConditionCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 계산 결과 */}
|
||||
<div className="rounded-md bg-primary/10 px-3 py-2 text-center text-sm text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-md px-3 py-2 text-center text-sm">
|
||||
{locationCount > 0 ? (
|
||||
<>
|
||||
{localValues.startRow}열 ~ {localValues.endRow}열 x {localValues.levels}단 ={" "}
|
||||
@@ -288,11 +289,10 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
return ctx;
|
||||
}, [propContext, formData, fieldMapping, getCategoryLabel]);
|
||||
|
||||
// 필수 필드 검증
|
||||
// 필수 필드 검증 (층은 선택 입력)
|
||||
const missingFields = useMemo(() => {
|
||||
const missing: string[] = [];
|
||||
if (!context.warehouseCode) missing.push("창고 코드");
|
||||
if (!context.floor) missing.push("층");
|
||||
if (!context.zone) missing.push("구역");
|
||||
return missing;
|
||||
}, [context]);
|
||||
@@ -377,9 +377,8 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
// 기존 데이터 조회 (창고/층/구역이 변경될 때마다)
|
||||
useEffect(() => {
|
||||
const loadExistingLocations = async () => {
|
||||
// 필수 조건이 충족되지 않으면 기존 데이터 초기화
|
||||
// DB에는 라벨 값(예: "1층", "A구역")으로 저장되어 있으므로 라벨 값 사용
|
||||
if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) {
|
||||
// 창고 코드와 구역은 필수, 층은 선택
|
||||
if (!warehouseCodeForQuery || !zoneForQuery) {
|
||||
setExistingLocations([]);
|
||||
setDuplicateErrors([]);
|
||||
return;
|
||||
@@ -387,14 +386,13 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
|
||||
setIsCheckingDuplicates(true);
|
||||
try {
|
||||
// warehouse_location 테이블에서 해당 창고/층/구역의 기존 데이터 조회
|
||||
// DB에는 라벨 값으로 저장되어 있으므로 라벨 값으로 필터링
|
||||
// equals 연산자를 사용하여 정확한 일치 검색 (ILIKE가 아닌 = 연산자 사용)
|
||||
const searchParams = {
|
||||
const searchParams: Record<string, any> = {
|
||||
warehouse_code: { value: warehouseCodeForQuery, operator: "equals" },
|
||||
floor: { value: floorForQuery, operator: "equals" },
|
||||
zone: { value: zoneForQuery, operator: "equals" },
|
||||
};
|
||||
if (floorForQuery) {
|
||||
searchParams.floor = { value: floorForQuery, operator: "equals" };
|
||||
}
|
||||
|
||||
// 직접 apiClient 사용하여 정확한 형식으로 요청
|
||||
// 백엔드는 search를 객체로 받아서 각 필드를 WHERE 조건으로 처리
|
||||
@@ -493,23 +491,26 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
return { totalLocations, totalRows, maxLevel };
|
||||
}, [conditions]);
|
||||
|
||||
// 위치 코드 생성
|
||||
// 포맷 설정 (ConfigPanel에서 관리자가 설정한 값, 미설정 시 기본값)
|
||||
const formatConfig = config.formatConfig || defaultFormatConfig;
|
||||
|
||||
// 위치 코드 생성 (세그먼트 기반 - 순서/구분자/라벨/자릿수 모두 formatConfig에 따름)
|
||||
const generateLocationCode = useCallback(
|
||||
(row: number, level: number): { code: string; name: string } => {
|
||||
const warehouseCode = context?.warehouseCode || "WH001";
|
||||
const floor = context?.floor || "1";
|
||||
const zone = context?.zone || "A";
|
||||
const values: Record<string, string> = {
|
||||
warehouseCode: context?.warehouseCode || "WH001",
|
||||
floor: context?.floor || "",
|
||||
zone: context?.zone || "A",
|
||||
row: row.toString(),
|
||||
level: level.toString(),
|
||||
};
|
||||
|
||||
// 코드 생성 (예: WH001-1층D구역-01-1)
|
||||
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
||||
|
||||
// 이름 생성 - zone에 이미 "구역"이 포함되어 있으면 그대로 사용
|
||||
const zoneName = zone.includes("구역") ? zone : `${zone}구역`;
|
||||
const name = `${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`;
|
||||
const code = buildFormattedString(formatConfig.codeSegments, values);
|
||||
const name = buildFormattedString(formatConfig.nameSegments, values);
|
||||
|
||||
return { code, name };
|
||||
},
|
||||
[context],
|
||||
[context, formatConfig],
|
||||
);
|
||||
|
||||
// 미리보기 생성
|
||||
@@ -626,7 +627,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<div className="h-4 w-1 rounded bg-gradient-to-b from-green-500 to-primary/50" />렉 라인 구조 설정
|
||||
<div className="to-primary/50 h-4 w-1 rounded bg-gradient-to-b from-green-500" />렉 라인 구조 설정
|
||||
</CardTitle>
|
||||
{!readonly && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -719,8 +720,8 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
|
||||
{/* 기존 데이터 존재 알림 */}
|
||||
{!isCheckingDuplicates && existingLocations.length > 0 && !hasDuplicateWithExisting && (
|
||||
<Alert className="mb-4 border-primary/20 bg-primary/10">
|
||||
<AlertCircle className="h-4 w-4 text-primary" />
|
||||
<Alert className="border-primary/20 bg-primary/10 mb-4">
|
||||
<AlertCircle className="text-primary h-4 w-4" />
|
||||
<AlertDescription className="text-primary">
|
||||
해당 창고/층/구역에 <strong>{existingLocations.length}개</strong>의 위치가 이미 등록되어 있습니다.
|
||||
</AlertDescription>
|
||||
@@ -729,9 +730,9 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
|
||||
{/* 현재 매핑된 값 표시 */}
|
||||
{(context.warehouseCode || context.warehouseName || context.floor || context.zone) && (
|
||||
<div className="mb-4 flex flex-wrap gap-2 rounded-lg bg-muted p-3">
|
||||
<div className="bg-muted mb-4 flex flex-wrap gap-2 rounded-lg p-3">
|
||||
{(context.warehouseCode || context.warehouseName) && (
|
||||
<span className="rounded bg-primary/10 px-2 py-1 text-xs text-primary">
|
||||
<span className="bg-primary/10 text-primary rounded px-2 py-1 text-xs">
|
||||
창고: {context.warehouseName || context.warehouseCode}
|
||||
{context.warehouseName && context.warehouseCode && ` (${context.warehouseCode})`}
|
||||
</span>
|
||||
@@ -748,28 +749,28 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
</span>
|
||||
)}
|
||||
{context.status && (
|
||||
<span className="rounded bg-muted/80 px-2 py-1 text-xs text-foreground">상태: {context.status}</span>
|
||||
<span className="bg-muted/80 text-foreground rounded px-2 py-1 text-xs">상태: {context.status}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 안내 메시지 */}
|
||||
<div className="mb-4 rounded-lg bg-primary/10 p-4">
|
||||
<ol className="space-y-1 text-sm text-primary">
|
||||
<div className="bg-primary/10 mb-4 rounded-lg p-4">
|
||||
<ol className="text-primary space-y-1 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary text-xs font-bold text-white">
|
||||
<span className="bg-primary flex h-5 w-5 shrink-0 items-center justify-center rounded text-xs font-bold text-white">
|
||||
1
|
||||
</span>
|
||||
조건 추가 버튼을 클릭하여 렉 라인 조건을 생성하세요
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary text-xs font-bold text-white">
|
||||
<span className="bg-primary flex h-5 w-5 shrink-0 items-center justify-center rounded text-xs font-bold text-white">
|
||||
2
|
||||
</span>
|
||||
각 조건마다 열 범위와 단 수를 입력하세요
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary text-xs font-bold text-white">
|
||||
<span className="bg-primary flex h-5 w-5 shrink-0 items-center justify-center rounded text-xs font-bold text-white">
|
||||
3
|
||||
</span>
|
||||
예시: 조건1(1~3열, 3단), 조건2(4~6열, 5단)
|
||||
@@ -779,9 +780,9 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
|
||||
{/* 조건 목록 또는 빈 상태 */}
|
||||
{conditions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-border py-12">
|
||||
<div className="mb-4 text-6xl text-muted-foreground/50">📦</div>
|
||||
<p className="mb-4 text-muted-foreground">조건을 추가하여 렉 구조를 설정하세요</p>
|
||||
<div className="border-border flex flex-col items-center justify-center rounded-lg border-2 border-dashed py-12">
|
||||
<div className="text-muted-foreground/50 mb-4 text-6xl">📦</div>
|
||||
<p className="text-muted-foreground mb-4">조건을 추가하여 렉 구조를 설정하세요</p>
|
||||
{!readonly && (
|
||||
<Button onClick={addCondition} className="gap-1">
|
||||
<Plus className="h-4 w-4" />첫 번째 조건 추가
|
||||
@@ -832,15 +833,15 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
{config.showStatistics && (
|
||||
<div className="mb-4 grid grid-cols-3 gap-4">
|
||||
<div className="rounded-lg border bg-white p-4 text-center">
|
||||
<div className="text-sm text-muted-foreground">총 위치</div>
|
||||
<div className="text-muted-foreground text-sm">총 위치</div>
|
||||
<div className="text-2xl font-bold">{statistics.totalLocations}개</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-white p-4 text-center">
|
||||
<div className="text-sm text-muted-foreground">열 수</div>
|
||||
<div className="text-muted-foreground text-sm">열 수</div>
|
||||
<div className="text-2xl font-bold">{statistics.totalRows}개</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-white p-4 text-center">
|
||||
<div className="text-sm text-muted-foreground">최대 단</div>
|
||||
<div className="text-muted-foreground text-sm">최대 단</div>
|
||||
<div className="text-2xl font-bold">{statistics.maxLevel}단</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -851,7 +852,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
<div className="rounded-lg border">
|
||||
<ScrollArea className="h-[400px]">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 bg-muted">
|
||||
<TableHeader className="bg-muted sticky top-0">
|
||||
<TableRow>
|
||||
<TableHead className="w-12 text-center">No</TableHead>
|
||||
<TableHead>위치코드</TableHead>
|
||||
@@ -870,7 +871,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
<TableCell className="text-center">{idx + 1}</TableCell>
|
||||
<TableCell className="font-mono">{loc.location_code}</TableCell>
|
||||
<TableCell>{loc.location_name}</TableCell>
|
||||
<TableCell className="text-center">{loc.floor || context?.floor || "1"}</TableCell>
|
||||
<TableCell className="text-center">{loc.floor || context?.floor || "-"}</TableCell>
|
||||
<TableCell className="text-center">{loc.zone || context?.zone || "A"}</TableCell>
|
||||
<TableCell className="text-center">{loc.row_num.padStart(2, "0")}</TableCell>
|
||||
<TableCell className="text-center">{loc.level_num}</TableCell>
|
||||
@@ -883,8 +884,8 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-border py-8 text-muted-foreground">
|
||||
<Eye className="mb-2 h-8 w-8 text-muted-foreground/50" />
|
||||
<div className="border-border text-muted-foreground flex flex-col items-center justify-center rounded-lg border-2 border-dashed py-8">
|
||||
<Eye className="text-muted-foreground/50 mb-2 h-8 w-8" />
|
||||
<p>미리보기 생성 버튼을 클릭하여 결과를 확인하세요</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -931,16 +932,16 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
{/* 템플릿 목록 */}
|
||||
{templates.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-foreground">저장된 템플릿</div>
|
||||
<div className="text-foreground text-sm font-medium">저장된 템플릿</div>
|
||||
<ScrollArea className="h-[200px]">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className="flex items-center justify-between rounded-lg border p-3 hover:bg-muted"
|
||||
className="hover:bg-muted flex items-center justify-between rounded-lg border p-3"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{template.conditions.length}개 조건</div>
|
||||
<div className="text-muted-foreground text-xs">{template.conditions.length}개 조건</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => loadTemplate(template)}>
|
||||
@@ -955,7 +956,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center text-muted-foreground">저장된 템플릿이 없습니다</div>
|
||||
<div className="text-muted-foreground py-8 text-center">저장된 템플릿이 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,14 +4,10 @@ import React, { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { RackStructureComponentConfig, FieldMapping } from "./types";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { RackStructureComponentConfig, FieldMapping, FormatSegment } from "./types";
|
||||
import { defaultFormatConfig, SAMPLE_VALUES } from "./config";
|
||||
import { FormatSegmentEditor } from "./FormatSegmentEditor";
|
||||
|
||||
interface RackStructureConfigPanelProps {
|
||||
config: RackStructureComponentConfig;
|
||||
@@ -34,9 +30,7 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||
tables = [],
|
||||
}) => {
|
||||
// 사용 가능한 컬럼 목록 추출
|
||||
const [availableColumns, setAvailableColumns] = useState<
|
||||
Array<{ value: string; label: string }>
|
||||
>([]);
|
||||
const [availableColumns, setAvailableColumns] = useState<Array<{ value: string; label: string }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 모든 테이블의 컬럼을 플랫하게 추출
|
||||
@@ -69,14 +63,24 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||
|
||||
const fieldMapping = config.fieldMapping || {};
|
||||
|
||||
const formatConfig = config.formatConfig || defaultFormatConfig;
|
||||
|
||||
const handleFormatChange = (key: "codeSegments" | "nameSegments", segments: FormatSegment[]) => {
|
||||
onChange({
|
||||
...config,
|
||||
formatConfig: {
|
||||
...formatConfig,
|
||||
[key]: segments,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 필드 매핑 섹션 */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-foreground">필드 매핑</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
상위 폼에 배치된 필드 중 어떤 필드를 사용할지 선택하세요
|
||||
</p>
|
||||
<div className="text-foreground text-sm font-medium">필드 매핑</div>
|
||||
<p className="text-muted-foreground text-xs">상위 폼에 배치된 필드 중 어떤 필드를 사용할지 선택하세요</p>
|
||||
|
||||
{/* 창고 코드 필드 */}
|
||||
<div>
|
||||
@@ -207,7 +211,7 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||
|
||||
{/* 제한 설정 */}
|
||||
<div className="space-y-3 border-t pt-3">
|
||||
<div className="text-sm font-medium text-foreground">제한 설정</div>
|
||||
<div className="text-foreground text-sm font-medium">제한 설정</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">최대 조건 수</Label>
|
||||
@@ -248,7 +252,7 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||
|
||||
{/* UI 설정 */}
|
||||
<div className="space-y-3 border-t pt-3">
|
||||
<div className="text-sm font-medium text-foreground">UI 설정</div>
|
||||
<div className="text-foreground text-sm font-medium">UI 설정</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">템플릿 기능</Label>
|
||||
@@ -276,12 +280,31 @@ export const RackStructureConfigPanel: React.FC<RackStructureConfigPanelProps> =
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">읽기 전용</Label>
|
||||
<Switch
|
||||
checked={config.readonly ?? false}
|
||||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||
/>
|
||||
<Switch checked={config.readonly ?? false} onCheckedChange={(checked) => handleChange("readonly", checked)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 포맷 설정 */}
|
||||
<div className="space-y-3 border-t pt-3">
|
||||
<div className="text-sm font-medium text-gray-700">포맷 설정</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
위치코드와 위치명의 구성 요소를 드래그로 순서 변경하고, 구분자/라벨을 편집할 수 있습니다
|
||||
</p>
|
||||
|
||||
<FormatSegmentEditor
|
||||
label="위치코드 포맷"
|
||||
segments={formatConfig.codeSegments}
|
||||
onChange={(segs) => handleFormatChange("codeSegments", segs)}
|
||||
sampleValues={SAMPLE_VALUES}
|
||||
/>
|
||||
|
||||
<FormatSegmentEditor
|
||||
label="위치명 포맷"
|
||||
segments={formatConfig.nameSegments}
|
||||
onChange={(segs) => handleFormatChange("nameSegments", segs)}
|
||||
sampleValues={SAMPLE_VALUES}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,26 +2,107 @@
|
||||
* 렉 구조 컴포넌트 기본 설정
|
||||
*/
|
||||
|
||||
import { RackStructureComponentConfig } from "./types";
|
||||
import {
|
||||
RackStructureComponentConfig,
|
||||
FormatSegment,
|
||||
FormatSegmentType,
|
||||
LocationFormatConfig,
|
||||
} from "./types";
|
||||
|
||||
// 세그먼트 타입별 한글 표시명
|
||||
export const SEGMENT_TYPE_LABELS: Record<FormatSegmentType, string> = {
|
||||
warehouseCode: "창고코드",
|
||||
floor: "층",
|
||||
zone: "구역",
|
||||
row: "열",
|
||||
level: "단",
|
||||
};
|
||||
|
||||
// 위치코드 기본 세그먼트 (현재 하드코딩과 동일한 결과)
|
||||
export const defaultCodeSegments: FormatSegment[] = [
|
||||
{ type: "warehouseCode", enabled: true, showLabel: false, label: "", separatorAfter: "-", pad: 0 },
|
||||
{ type: "floor", enabled: true, showLabel: true, label: "층", separatorAfter: "", pad: 0 },
|
||||
{ type: "zone", enabled: true, showLabel: true, label: "구역", separatorAfter: "-", pad: 0 },
|
||||
{ type: "row", enabled: true, showLabel: false, label: "", separatorAfter: "-", pad: 2 },
|
||||
{ type: "level", enabled: true, showLabel: false, label: "", separatorAfter: "", pad: 0 },
|
||||
];
|
||||
|
||||
// 위치명 기본 세그먼트 (현재 하드코딩과 동일한 결과)
|
||||
export const defaultNameSegments: FormatSegment[] = [
|
||||
{ type: "zone", enabled: true, showLabel: true, label: "구역", separatorAfter: "-", pad: 0 },
|
||||
{ type: "row", enabled: true, showLabel: true, label: "열", separatorAfter: "-", pad: 2 },
|
||||
{ type: "level", enabled: true, showLabel: true, label: "단", separatorAfter: "", pad: 0 },
|
||||
];
|
||||
|
||||
export const defaultFormatConfig: LocationFormatConfig = {
|
||||
codeSegments: defaultCodeSegments,
|
||||
nameSegments: defaultNameSegments,
|
||||
};
|
||||
|
||||
// 세그먼트 타입별 기본 한글 접미사 (context 값에 포함되어 있는 한글)
|
||||
const KNOWN_SUFFIXES: Partial<Record<FormatSegmentType, string>> = {
|
||||
floor: "층",
|
||||
zone: "구역",
|
||||
};
|
||||
|
||||
// 값에서 알려진 한글 접미사를 제거하여 순수 값만 추출
|
||||
function stripKnownSuffix(type: FormatSegmentType, val: string): string {
|
||||
const suffix = KNOWN_SUFFIXES[type];
|
||||
if (suffix && val.endsWith(suffix)) {
|
||||
return val.slice(0, -suffix.length);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// 세그먼트 배열로 포맷된 문자열 생성
|
||||
export function buildFormattedString(
|
||||
segments: FormatSegment[],
|
||||
values: Record<string, string>,
|
||||
): string {
|
||||
const activeSegments = segments.filter(
|
||||
(seg) => seg.enabled && values[seg.type],
|
||||
);
|
||||
|
||||
return activeSegments
|
||||
.map((seg, idx) => {
|
||||
// 1) 원본 값에서 한글 접미사를 먼저 벗겨냄 ("A구역" → "A", "1층" → "1")
|
||||
let val = stripKnownSuffix(seg.type, values[seg.type]);
|
||||
|
||||
// 2) showLabel이 켜져 있고 label이 있으면 붙임
|
||||
if (seg.showLabel && seg.label) {
|
||||
val += seg.label;
|
||||
}
|
||||
|
||||
if (seg.pad > 0 && !isNaN(Number(val))) {
|
||||
val = val.padStart(seg.pad, "0");
|
||||
}
|
||||
|
||||
if (idx < activeSegments.length - 1) {
|
||||
val += seg.separatorAfter;
|
||||
}
|
||||
return val;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
// 미리보기용 샘플 값
|
||||
export const SAMPLE_VALUES: Record<string, string> = {
|
||||
warehouseCode: "WH001",
|
||||
floor: "1층",
|
||||
zone: "A구역",
|
||||
row: "1",
|
||||
level: "1",
|
||||
};
|
||||
|
||||
export const defaultConfig: RackStructureComponentConfig = {
|
||||
// 기본 제한
|
||||
maxConditions: 10,
|
||||
maxRows: 99,
|
||||
maxLevels: 20,
|
||||
|
||||
// 기본 코드 패턴
|
||||
codePattern: "{warehouseCode}-{floor}{zone}-{row:02d}-{level}",
|
||||
namePattern: "{zone}구역-{row:02d}열-{level}단",
|
||||
|
||||
// UI 설정
|
||||
showTemplates: true,
|
||||
showPreview: true,
|
||||
showStatistics: true,
|
||||
readonly: false,
|
||||
|
||||
// 초기 조건 없음
|
||||
initialConditions: [],
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,24 @@ export interface FieldMapping {
|
||||
statusField?: string; // 사용 여부로 사용할 폼 필드명
|
||||
}
|
||||
|
||||
// 포맷 세그먼트 (위치코드/위치명의 각 구성요소)
|
||||
export type FormatSegmentType = 'warehouseCode' | 'floor' | 'zone' | 'row' | 'level';
|
||||
|
||||
export interface FormatSegment {
|
||||
type: FormatSegmentType;
|
||||
enabled: boolean; // 이 세그먼트를 포함할지 여부
|
||||
showLabel: boolean; // 한글 라벨 표시 여부 (false면 값에서 라벨 제거)
|
||||
label: string; // 한글 라벨 (예: "층", "구역", "열", "단")
|
||||
separatorAfter: string; // 이 세그먼트 뒤의 구분자 (예: "-", "/", "")
|
||||
pad: number; // 최소 자릿수 (0 = 그대로, 2 = "01"처럼 2자리 맞춤)
|
||||
}
|
||||
|
||||
// 위치코드 + 위치명 포맷 설정
|
||||
export interface LocationFormatConfig {
|
||||
codeSegments: FormatSegment[];
|
||||
nameSegments: FormatSegment[];
|
||||
}
|
||||
|
||||
// 컴포넌트 설정
|
||||
export interface RackStructureComponentConfig {
|
||||
// 기본 설정
|
||||
@@ -54,8 +72,9 @@ export interface RackStructureComponentConfig {
|
||||
fieldMapping?: FieldMapping;
|
||||
|
||||
// 위치 코드 생성 규칙
|
||||
codePattern?: string; // 코드 패턴 (예: "{warehouse}-{floor}{zone}-{row:02d}-{level}")
|
||||
namePattern?: string; // 이름 패턴 (예: "{zone}구역-{row:02d}열-{level}단")
|
||||
codePattern?: string; // 코드 패턴 (하위 호환용 유지)
|
||||
namePattern?: string; // 이름 패턴 (하위 호환용 유지)
|
||||
formatConfig?: LocationFormatConfig; // 구조화된 포맷 설정
|
||||
|
||||
// UI 설정
|
||||
showTemplates?: boolean; // 템플릿 기능 표시
|
||||
@@ -93,5 +112,3 @@ export interface RackStructureComponentProps {
|
||||
isPreview?: boolean;
|
||||
tableName?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user