ERP-node/frontend/components/numbering-rule/NumberingRulePreview.tsx

176 lines
6.0 KiB
TypeScript
Raw Normal View History

2025-11-04 13:58:21 +09:00
"use client";
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import { NumberingRuleConfig, NumberingRulePart, CodePartType } from "@/types/numbering-rule";
import { CODE_PART_TYPE_OPTIONS } from "@/types/numbering-rule";
/** 파트별 표시값 + 타입 (미리보기 스트립/세그먼트용) */
export interface PartDisplayItem {
partType: CodePartType;
displayValue: string;
order: number;
}
/** config에서 파트별 표시값 배열 계산 (정렬된 parts 기준) */
export function computePartDisplayItems(config: NumberingRuleConfig): PartDisplayItem[] {
if (!config.parts || config.parts.length === 0) return [];
const sorted = [...config.parts].sort((a, b) => a.order - b.order);
const globalSep = config.separator ?? "-";
return sorted.map((part) => ({
order: part.order,
partType: part.partType,
displayValue: getPartDisplayValue(part),
}));
}
function getPartDisplayValue(part: NumberingRulePart): string {
if (part.generationMethod === "manual") {
return part.manualConfig?.value || "XXX";
}
const c = part.autoConfig || {};
switch (part.partType) {
case "sequence":
return String(c.startFrom ?? 1).padStart(c.sequenceLength ?? 3, "0");
case "number":
return String(c.numberValue ?? 0).padStart(c.numberLength ?? 4, "0");
case "date": {
const format = c.dateFormat || "YYYYMMDD";
if (c.useColumnValue && c.sourceColumnName) {
return format === "YYYY" ? "[YYYY]" : format === "YY" ? "[YY]" : format === "YYYYMM" ? "[YYYYMM]" : format === "YYMM" ? "[YYMM]" : format === "YYMMDD" ? "[YYMMDD]" : "[DATE]";
}
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, "0");
const d = String(now.getDate()).padStart(2, "0");
if (format === "YYYY") return String(y);
if (format === "YY") return String(y).slice(-2);
if (format === "YYYYMM") return `${y}${m}`;
if (format === "YYMM") return `${String(y).slice(-2)}${m}`;
if (format === "YYYYMMDD") return `${y}${m}${d}`;
if (format === "YYMMDD") return `${String(y).slice(-2)}${m}${d}`;
return `${y}${m}${d}`;
}
case "text":
return c.textValue || "TEXT";
default:
return "XXX";
}
}
/** 파트 타입별 미리보기용 텍스트 색상 클래스 (CSS 변수 기반) */
export function getPartTypeColorClass(partType: CodePartType): string {
switch (partType) {
case "date":
return "text-warning";
case "text":
return "text-primary";
case "sequence":
return "text-primary";
case "number":
return "text-muted-foreground";
case "category":
case "reference":
return "text-muted-foreground";
default:
return "text-foreground";
}
}
/** 파트 타입별 점(dot) 배경 색상 (범례용) */
export function getPartTypeDotClass(partType: CodePartType): string {
switch (partType) {
case "date":
return "bg-warning";
case "text":
case "sequence":
return "bg-primary";
case "number":
case "category":
case "reference":
return "bg-muted-foreground";
default:
return "bg-foreground";
}
}
2025-11-04 13:58:21 +09:00
interface NumberingRulePreviewProps {
config: NumberingRuleConfig;
compact?: boolean;
/** 큰 미리보기 스트립: 28px, 파트별 색상, 하단 범례 */
variant?: "default" | "strip";
2025-11-04 13:58:21 +09:00
}
export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
config,
compact = false,
variant = "default",
2025-11-04 13:58:21 +09:00
}) => {
const partItems = useMemo(() => computePartDisplayItems(config), [config]);
const sortedParts = useMemo(
() => (config.parts ? [...config.parts].sort((a, b) => a.order - b.order) : []),
[config.parts]
);
2025-11-04 13:58:21 +09:00
const generatedCode = useMemo(() => {
if (partItems.length === 0) return "규칙을 추가해주세요";
const globalSep = config.separator ?? "-";
let result = "";
partItems.forEach((item, idx) => {
result += item.displayValue;
if (idx < partItems.length - 1) {
const part = sortedParts.find((p) => p.order === item.order);
result += part?.separatorAfter ?? globalSep;
}
});
return result;
}, [config.separator, partItems, sortedParts]);
if (variant === "strip") {
const globalSep = config.separator ?? "-";
return (
<div className="rounded-lg bg-gradient-to-b from-muted to-card px-4 py-4 sm:px-6 sm:py-5">
<div className="font-mono text-[22px] font-extrabold tracking-tight sm:text-[28px]">
{partItems.length === 0 ? (
<span className="text-muted-foreground"> </span>
) : (
partItems.map((item, idx) => (
<React.Fragment key={item.order}>
<span className={getPartTypeColorClass(item.partType)}>{item.displayValue}</span>
{idx < partItems.length - 1 && (
<span className="text-muted-foreground">
{sortedParts.find((p) => p.order === item.order)?.separatorAfter ?? globalSep}
</span>
)}
</React.Fragment>
))
)}
</div>
{partItems.length > 0 && (
<div className="preview-desc mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
{CODE_PART_TYPE_OPTIONS.filter((opt) => partItems.some((p) => p.partType === opt.value)).map((opt) => (
<span key={opt.value} className="flex items-center gap-1.5">
<span className={cn("h-[6px] w-[6px] shrink-0 rounded-full", getPartTypeDotClass(opt.value))} />
{opt.label}
</span>
))}
</div>
)}
</div>
);
}
2025-11-04 13:58:21 +09:00
if (compact) {
return (
<div className="rounded-md bg-muted px-2 py-1">
<code className="text-xs font-mono text-foreground">{generatedCode}</code>
</div>
);
}
return (
<div className="rounded-md bg-muted px-3 py-2">
<code className="text-sm font-mono text-foreground">{generatedCode}</code>
2025-11-04 13:58:21 +09:00
</div>
);
};