Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-12 14:23:34 +09:00
99 changed files with 14205 additions and 1442 deletions

View File

@@ -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>
);
}

View File

@@ -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>
)}

View File

@@ -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>
);
};

View File

@@ -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: [],
};

View File

@@ -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;
}