2025-11-04 13:58:21 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useMemo } from "react";
|
2026-03-17 16:20:24 +09:00
|
|
|
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;
|
2026-03-17 16:20:24 +09:00
|
|
|
/** 큰 미리보기 스트립: 28px, 파트별 색상, 하단 범례 */
|
|
|
|
|
variant?: "default" | "strip";
|
2025-11-04 13:58:21 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
|
|
|
|
|
config,
|
2026-03-17 16:20:24 +09:00
|
|
|
compact = false,
|
|
|
|
|
variant = "default",
|
2025-11-04 13:58:21 +09:00
|
|
|
}) => {
|
2026-03-17 16:20:24 +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(() => {
|
2026-03-17 16:20:24 +09:00
|
|
|
if (partItems.length === 0) return "규칙을 추가해주세요";
|
2026-02-25 12:25:30 +09:00
|
|
|
const globalSep = config.separator ?? "-";
|
|
|
|
|
let result = "";
|
2026-03-17 16:20:24 +09:00
|
|
|
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;
|
2026-02-25 12:25:30 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return result;
|
2026-03-17 16:20:24 +09:00
|
|
|
}, [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">
|
|
|
|
|
<div className="font-mono text-[28px] font-extrabold tracking-tight">
|
|
|
|
|
{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="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={`h-1.5 w-1.5 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 (
|
2025-11-06 11:25:59 +09:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
};
|