feat(pop): 입고 확정 시 자동 채번 실행 + 결과 모달 UX + 셀렉트 높이 통일
입고 확정(inbound-confirm) 실행 시 채번 규칙이 설정되어 있어도 inbound_number가 null로 저장되던 문제를 해결한다. [채번 실행 (FIX-1)] - types.ts: SaveMapping에 autoGenMappings 필드 추가 (numberingRuleId, targetColumn, showResultModal) - PopFieldComponent: collect_data 응답에 autoGenMappings 포함하여 백엔드에 채번 규칙 정보 전달 - popActionRoutes: INSERT 전 numberingRuleService.allocateCode() 호출, 생성된 코드를 generatedCodes 배열로 응답에 포함 [결과 모달 UX] - pop-button: showResultModal 토글에 따라 채번 결과 모달 표시 분기 - 모달이 열려 있는 동안 followUpActions(refresh/navigate) 지연하여 사용자가 확인 버튼을 눌러야 후속 액션 실행 [셀렉트 높이 일관성] - SelectTrigger hasCustomHeight에 /\bh-\d/ 패턴 추가하여 className의 h-9 등이 기본 data-size="xs"(h-6)와 충돌하지 않도록 수정 [기타 수정] - SelectFieldInput: Set 기반 dedup으로 React key 중복 방지 - PopFieldConfig: AutoNumberEditor 제거, 채번 규칙을 저장 탭에서 관리 - PopFieldConfig: 전체 채번 규칙 보기 토글 추가 - PopCardListComponent: 장바구니 목록 모드에서 수량 자동 초기화 방지 - PopCardListConfig: 수식 필드 매핑 노출 + 누락 필드 자동 추가
This commit is contained in:
parent
e5abd93600
commit
a6c0ab5664
|
|
@ -2,6 +2,7 @@ import { Router, Request, Response } from "express";
|
|||
import { getPool } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { numberingRuleService } from "../services/numberingRuleService";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -12,9 +13,16 @@ function isSafeIdentifier(name: string): boolean {
|
|||
return SAFE_IDENTIFIER.test(name);
|
||||
}
|
||||
|
||||
interface AutoGenMappingInfo {
|
||||
numberingRuleId: string;
|
||||
targetColumn: string;
|
||||
showResultModal?: boolean;
|
||||
}
|
||||
|
||||
interface MappingInfo {
|
||||
targetTable: string;
|
||||
columnMapping: Record<string, string>;
|
||||
autoGenMappings?: AutoGenMappingInfo[];
|
||||
}
|
||||
|
||||
interface StatusConditionRule {
|
||||
|
|
@ -114,6 +122,7 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
|
|||
|
||||
let processedCount = 0;
|
||||
let insertedCount = 0;
|
||||
const generatedCodes: Array<{ targetColumn: string; code: string }> = [];
|
||||
|
||||
if (action === "inbound-confirm") {
|
||||
// 1. 매핑 기반 INSERT (장바구니 데이터 -> 대상 테이블)
|
||||
|
|
@ -144,6 +153,37 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
|
|||
}
|
||||
}
|
||||
|
||||
// 채번 규칙 실행: field + cardList의 autoGenMappings에서 코드 발급
|
||||
const allAutoGen = [
|
||||
...(fieldMapping?.autoGenMappings ?? []),
|
||||
...(cardMapping?.autoGenMappings ?? []),
|
||||
];
|
||||
for (const ag of allAutoGen) {
|
||||
if (!ag.numberingRuleId || !ag.targetColumn) continue;
|
||||
if (!isSafeIdentifier(ag.targetColumn)) continue;
|
||||
if (columns.includes(`"${ag.targetColumn}"`)) continue;
|
||||
try {
|
||||
const generatedCode = await numberingRuleService.allocateCode(
|
||||
ag.numberingRuleId,
|
||||
companyCode,
|
||||
{ ...fieldValues, ...item },
|
||||
);
|
||||
columns.push(`"${ag.targetColumn}"`);
|
||||
values.push(generatedCode);
|
||||
generatedCodes.push({ targetColumn: ag.targetColumn, code: generatedCode, showResultModal: ag.showResultModal ?? false });
|
||||
logger.info("[pop/execute-action] 채번 완료", {
|
||||
ruleId: ag.numberingRuleId,
|
||||
targetColumn: ag.targetColumn,
|
||||
generatedCode,
|
||||
});
|
||||
} catch (err: any) {
|
||||
logger.error("[pop/execute-action] 채번 실패", {
|
||||
ruleId: ag.numberingRuleId,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (columns.length > 1) {
|
||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const sql = `INSERT INTO "${cardMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
|
|
@ -263,7 +303,7 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
|
|||
return res.json({
|
||||
success: true,
|
||||
message: `${processedCount}건 처리 완료${insertedCount > 0 ? `, ${insertedCount}건 생성` : ""}`,
|
||||
data: { processedCount, insertedCount },
|
||||
data: { processedCount, insertedCount, generatedCodes },
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function SelectTrigger({
|
|||
size?: "xs" | "sm" | "default";
|
||||
}) {
|
||||
// className에 h-full/h-[ 또는 style.height가 있으면 data-size 높이를 무시
|
||||
const hasCustomHeight = className?.includes("h-full") || className?.includes("h-[") || !!style?.height;
|
||||
const hasCustomHeight = className?.includes("h-full") || className?.includes("h-[") || /\bh-\d/.test(className ?? "") || !!style?.height;
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
|
|
|
|||
|
|
@ -421,6 +421,32 @@ export function PopButtonComponent({
|
|||
const [confirmProcessing, setConfirmProcessing] = useState(false);
|
||||
const [showInboundConfirm, setShowInboundConfirm] = useState(false);
|
||||
const [inboundSelectedCount, setInboundSelectedCount] = useState(0);
|
||||
const [generatedCodesResult, setGeneratedCodesResult] = useState<Array<{ targetColumn: string; code: string; showResultModal?: boolean }>>([]);
|
||||
|
||||
const handleCloseGeneratedCodesModal = useCallback(() => {
|
||||
setGeneratedCodesResult([]);
|
||||
toast.success("입고 확정 완료");
|
||||
publish(`__comp_output__${componentId}__action_completed`, {
|
||||
action: "inbound-confirm",
|
||||
success: true,
|
||||
});
|
||||
const followUps = config?.followUpActions ?? [];
|
||||
for (const fa of followUps) {
|
||||
switch (fa.type) {
|
||||
case "navigate":
|
||||
if (fa.targetScreenId) {
|
||||
publish("__pop_navigate__", { screenId: fa.targetScreenId, params: fa.params });
|
||||
}
|
||||
break;
|
||||
case "refresh":
|
||||
publish("__pop_refresh__");
|
||||
break;
|
||||
case "event":
|
||||
if (fa.eventName) publish(fa.eventName, fa.eventPayload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [componentId, publish, config?.followUpActions]);
|
||||
|
||||
// 입고 확정 모드: 선택 항목 수 수신
|
||||
useEffect(() => {
|
||||
|
|
@ -576,28 +602,32 @@ export function PopButtonComponent({
|
|||
});
|
||||
|
||||
if (result.data?.success) {
|
||||
toast.success(`${selectedItems.length}건 입고 확정 완료`);
|
||||
publish(`__comp_output__${componentId}__action_completed`, {
|
||||
action: "inbound-confirm",
|
||||
success: true,
|
||||
count: selectedItems.length,
|
||||
});
|
||||
|
||||
// 후속 액션 실행 (navigate, refresh 등)
|
||||
const followUps = config?.followUpActions ?? [];
|
||||
for (const fa of followUps) {
|
||||
switch (fa.type) {
|
||||
case "navigate":
|
||||
if (fa.targetScreenId) {
|
||||
publish("__pop_navigate__", { screenId: fa.targetScreenId, params: fa.params });
|
||||
}
|
||||
break;
|
||||
case "refresh":
|
||||
publish("__pop_refresh__");
|
||||
break;
|
||||
case "event":
|
||||
if (fa.eventName) publish(fa.eventName, fa.eventPayload);
|
||||
break;
|
||||
const codes: Array<{ targetColumn: string; code: string; showResultModal?: boolean }> = result.data.data?.generatedCodes ?? [];
|
||||
const modalCodes = codes.filter((c) => c.showResultModal);
|
||||
if (modalCodes.length > 0) {
|
||||
setGeneratedCodesResult(modalCodes);
|
||||
} else {
|
||||
toast.success(`${selectedItems.length}건 입고 확정 완료`);
|
||||
publish(`__comp_output__${componentId}__action_completed`, {
|
||||
action: "inbound-confirm",
|
||||
success: true,
|
||||
count: selectedItems.length,
|
||||
});
|
||||
const followUps = config?.followUpActions ?? [];
|
||||
for (const fa of followUps) {
|
||||
switch (fa.type) {
|
||||
case "navigate":
|
||||
if (fa.targetScreenId) {
|
||||
publish("__pop_navigate__", { screenId: fa.targetScreenId, params: fa.params });
|
||||
}
|
||||
break;
|
||||
case "refresh":
|
||||
publish("__pop_refresh__");
|
||||
break;
|
||||
case "event":
|
||||
if (fa.eventName) publish(fa.eventName, fa.eventPayload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -811,6 +841,36 @@ export function PopButtonComponent({
|
|||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 채번 결과 다이얼로그 - 사용자가 확인 누를 때까지 유지 */}
|
||||
<AlertDialog open={generatedCodesResult.length > 0} onOpenChange={(open) => { if (!open) handleCloseGeneratedCodesModal(); }}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">
|
||||
자동 생성 완료
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-2 text-xs sm:text-sm">
|
||||
<p>다음 번호가 자동 생성되었습니다.</p>
|
||||
{generatedCodesResult.map((c, i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-md border p-2">
|
||||
<span className="text-muted-foreground">{c.targetColumn}</span>
|
||||
<span className="font-mono font-semibold">{c.code}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction
|
||||
onClick={handleCloseGeneratedCodesModal}
|
||||
className="h-8 w-full text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 일반 확인 다이얼로그 */}
|
||||
<AlertDialog open={!!pendingConfirm} onOpenChange={(open) => { if (!open) cancelConfirm(); }}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
||||
|
|
|
|||
|
|
@ -998,12 +998,13 @@ function Card({
|
|||
return 999999;
|
||||
}, [limitCol, row]);
|
||||
|
||||
// 제한 컬럼이 있으면 최대값으로 자동 초기화
|
||||
// 제한 컬럼이 있으면 최대값으로 자동 초기화 (장바구니 목록 모드에서는 cart 수량 유지)
|
||||
useEffect(() => {
|
||||
if (isCartListMode) return;
|
||||
if (inputField?.enabled && limitCol && effectiveMax > 0 && effectiveMax < 999999) {
|
||||
setInputValue(effectiveMax);
|
||||
}
|
||||
}, [effectiveMax, inputField?.enabled, limitCol]);
|
||||
}, [effectiveMax, inputField?.enabled, limitCol, isCartListMode]);
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
height: `${scaled.cardHeight}px`,
|
||||
|
|
|
|||
|
|
@ -2784,6 +2784,13 @@ function SaveMappingSection({
|
|||
label: f.label || f.columnName,
|
||||
badge: "본문",
|
||||
});
|
||||
} else if (f.valueType === "formula" && f.label) {
|
||||
const formulaKey = `__formula_${f.id || f.label}`;
|
||||
displayed.push({
|
||||
sourceField: formulaKey,
|
||||
label: f.label,
|
||||
badge: "수식",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (inputFieldConfig?.enabled) {
|
||||
|
|
@ -2855,6 +2862,21 @@ function SaveMappingSection({
|
|||
[mapping.mappings]
|
||||
);
|
||||
|
||||
// 카드에 표시된 필드가 로드되면 매핑에 누락된 필드를 자동 추가 (매핑 안함으로)
|
||||
useEffect(() => {
|
||||
if (!mapping.targetTable || cardDisplayedFields.length === 0) return;
|
||||
const existing = new Set(mapping.mappings.map((m) => m.sourceField));
|
||||
const missing = cardDisplayedFields.filter((f) => !existing.has(f.sourceField));
|
||||
if (missing.length === 0) return;
|
||||
onUpdate({
|
||||
...mapping,
|
||||
mappings: [
|
||||
...mapping.mappings,
|
||||
...missing.map((f) => ({ sourceField: f.sourceField, targetColumn: "" })),
|
||||
],
|
||||
});
|
||||
}, [cardDisplayedFields]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 카드에 표시된 필드 중 아직 매핑되지 않은 것
|
||||
const unmappedCardFields = useMemo(
|
||||
() => cardDisplayedFields.filter((f) => !mappedSourceFields.has(f.sourceField)),
|
||||
|
|
@ -2937,7 +2959,7 @@ function SaveMappingSection({
|
|||
</div>
|
||||
{isCartMeta(entry.sourceField) ? (
|
||||
!badge && <span className="text-[9px] text-muted-foreground">장바구니</span>
|
||||
) : (
|
||||
) : entry.sourceField.startsWith("__formula_") ? null : (
|
||||
<span className="truncate text-[9px] text-muted-foreground">
|
||||
{entry.sourceField}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -211,6 +211,13 @@ export function PopFieldComponent({
|
|||
columnMapping: Object.fromEntries(
|
||||
(cfg.saveConfig.fieldMappings || []).map((m) => [m.fieldId, m.targetColumn])
|
||||
),
|
||||
autoGenMappings: (cfg.saveConfig.autoGenMappings || [])
|
||||
.filter((m) => m.numberingRuleId)
|
||||
.map((m) => ({
|
||||
numberingRuleId: m.numberingRuleId!,
|
||||
targetColumn: m.targetColumn,
|
||||
showResultModal: m.showResultModal,
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
|
@ -585,18 +592,21 @@ function SelectFieldInput({
|
|||
dataApi
|
||||
.getTableData(source.tableName, {
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
sortColumn: source.labelColumn,
|
||||
sortDirection: "asc",
|
||||
size: 500,
|
||||
sortBy: source.labelColumn,
|
||||
sortOrder: "asc",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data?.success && Array.isArray(res.data.data?.data)) {
|
||||
setOptions(
|
||||
res.data.data.data.map((row: Record<string, unknown>) => ({
|
||||
value: String(row[source.valueColumn!] ?? ""),
|
||||
label: String(row[source.labelColumn!] ?? ""),
|
||||
}))
|
||||
);
|
||||
if (Array.isArray(res.data)) {
|
||||
const seen = new Set<string>();
|
||||
const deduped: { value: string; label: string }[] = [];
|
||||
for (const row of res.data) {
|
||||
const v = String(row[source.valueColumn!] ?? "");
|
||||
if (!v || seen.has(v)) continue;
|
||||
seen.add(v);
|
||||
deduped.push({ value: v, label: String(row[source.labelColumn!] ?? "") });
|
||||
}
|
||||
setOptions(deduped);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ import {
|
|||
type ColumnInfo,
|
||||
} from "../pop-dashboard/utils/dataFetcher";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
||||
import { getAvailableNumberingRulesForScreen, getNumberingRules } from "@/lib/api/numberingRule";
|
||||
|
||||
// ========================================
|
||||
// Props
|
||||
|
|
@ -462,6 +462,8 @@ function SaveTabContent({
|
|||
// --- 자동생성 필드 로직 ---
|
||||
const autoGenMappings = cfg.saveConfig?.autoGenMappings ?? [];
|
||||
const [numberingRules, setNumberingRules] = useState<{ ruleId: string; ruleName: string }[]>([]);
|
||||
const [allNumberingRules, setAllNumberingRules] = useState<{ ruleId: string; ruleName: string; tableName: string }[]>([]);
|
||||
const [showAllRules, setShowAllRules] = useState(false);
|
||||
|
||||
// 레이아웃 auto 필드 → autoGenMappings 자동 동기화
|
||||
const autoFieldIdsKey = autoInputFields.map(({ field }) => field.id).join(",");
|
||||
|
|
@ -478,7 +480,7 @@ function SaveTabContent({
|
|||
label: field.labelText || "",
|
||||
targetColumn: "",
|
||||
numberingRuleId: field.autoNumber?.numberingRuleId ?? "",
|
||||
showInForm: true,
|
||||
showInForm: false,
|
||||
showResultModal: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -513,6 +515,24 @@ function SaveTabContent({
|
|||
}
|
||||
}, [saveTableName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAllRules) return;
|
||||
if (allNumberingRules.length > 0) return;
|
||||
getNumberingRules()
|
||||
.then((res) => {
|
||||
if (res.success && Array.isArray(res.data)) {
|
||||
setAllNumberingRules(
|
||||
res.data.map((r: any) => ({
|
||||
ruleId: String(r.ruleId ?? r.rule_id ?? ""),
|
||||
ruleName: String(r.ruleName ?? r.rule_name ?? ""),
|
||||
tableName: String(r.tableName ?? r.table_name ?? ""),
|
||||
}))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => setAllNumberingRules([]));
|
||||
}, [showAllRules, allNumberingRules.length]);
|
||||
|
||||
const addAutoGenMapping = useCallback(() => {
|
||||
const newMapping: PopFieldAutoGenMapping = {
|
||||
id: `autogen_${Date.now()}`,
|
||||
|
|
@ -1248,7 +1268,19 @@ function SaveTabContent({
|
|||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">채번 규칙</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[10px]">채번 규칙</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Switch
|
||||
checked={showAllRules}
|
||||
onCheckedChange={setShowAllRules}
|
||||
className="h-3.5 w-7 data-[state=checked]:bg-primary [&>span]:h-2.5 [&>span]:w-2.5"
|
||||
/>
|
||||
<Label className="cursor-pointer text-[10px] text-muted-foreground" onClick={() => setShowAllRules(!showAllRules)}>
|
||||
전체 보기
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={m.numberingRuleId || "__none__"}
|
||||
onValueChange={(v) =>
|
||||
|
|
@ -1262,11 +1294,19 @@ function SaveTabContent({
|
|||
<SelectItem value="__none__" className="text-xs">
|
||||
선택
|
||||
</SelectItem>
|
||||
{numberingRules.map((r) => (
|
||||
<SelectItem key={r.ruleId} value={r.ruleId} className="text-xs">
|
||||
{r.ruleName || r.ruleId}
|
||||
</SelectItem>
|
||||
))}
|
||||
{showAllRules
|
||||
? allNumberingRules.map((r) => (
|
||||
<SelectItem key={r.ruleId} value={r.ruleId} className="text-xs">
|
||||
{r.ruleName || r.ruleId}
|
||||
<span className="ml-1 text-muted-foreground">({r.tableName || "-"})</span>
|
||||
</SelectItem>
|
||||
))
|
||||
: numberingRules.map((r) => (
|
||||
<SelectItem key={r.ruleId} value={r.ruleId} className="text-xs">
|
||||
{r.ruleName || r.ruleId}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -1690,12 +1730,13 @@ function FieldItemEditor({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* auto 전용: 채번 설정 */}
|
||||
{/* auto 전용: 저장 탭에서 채번 규칙을 연결하라는 안내 */}
|
||||
{field.inputType === "auto" && (
|
||||
<AutoNumberEditor
|
||||
config={field.autoNumber}
|
||||
onUpdate={(autoNumber) => onUpdate({ autoNumber })}
|
||||
/>
|
||||
<div className="rounded border bg-muted/30 p-2">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
채번 규칙은 [저장] 탭 > 자동생성 필드에서 설정합니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1948,108 +1989,7 @@ function TableSourceEditor({
|
|||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// AutoNumberEditor: 자동 채번 설정
|
||||
// ========================================
|
||||
|
||||
function AutoNumberEditor({
|
||||
config,
|
||||
onUpdate,
|
||||
}: {
|
||||
config?: AutoNumberConfig;
|
||||
onUpdate: (config: AutoNumberConfig) => void;
|
||||
}) {
|
||||
const current: AutoNumberConfig = config || {
|
||||
prefix: "",
|
||||
dateFormat: "YYYYMMDD",
|
||||
separator: "-",
|
||||
sequenceDigits: 3,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded border bg-muted/30 p-2">
|
||||
<Label className="text-[10px] text-muted-foreground">자동 채번 설정</Label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">접두사</Label>
|
||||
<Input
|
||||
value={current.prefix || ""}
|
||||
onChange={(e) => onUpdate({ ...current, prefix: e.target.value })}
|
||||
placeholder="IN-"
|
||||
className="mt-0.5 h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">날짜 형식</Label>
|
||||
<Select
|
||||
value={current.dateFormat || "YYYYMMDD"}
|
||||
onValueChange={(v) => onUpdate({ ...current, dateFormat: v })}
|
||||
>
|
||||
<SelectTrigger className="mt-0.5 h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="YYYYMMDD" className="text-xs">
|
||||
YYYYMMDD
|
||||
</SelectItem>
|
||||
<SelectItem value="YYMMDD" className="text-xs">
|
||||
YYMMDD
|
||||
</SelectItem>
|
||||
<SelectItem value="YYMM" className="text-xs">
|
||||
YYMM
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">구분자</Label>
|
||||
<Input
|
||||
value={current.separator || ""}
|
||||
onChange={(e) => onUpdate({ ...current, separator: e.target.value })}
|
||||
placeholder="-"
|
||||
className="mt-0.5 h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">시퀀스 자릿수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={current.sequenceDigits || 3}
|
||||
onChange={(e) =>
|
||||
onUpdate({
|
||||
...current,
|
||||
sequenceDigits: Number(e.target.value) || 3,
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
className="mt-0.5 h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 미리보기 */}
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
미리보기:{" "}
|
||||
<span className="font-mono">
|
||||
{current.prefix || ""}
|
||||
{current.separator || ""}
|
||||
{current.dateFormat === "YYMM"
|
||||
? "2602"
|
||||
: current.dateFormat === "YYMMDD"
|
||||
? "260226"
|
||||
: "20260226"}
|
||||
{current.separator || ""}
|
||||
{"0".repeat(current.sequenceDigits || 3).slice(0, -1)}1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// AutoNumberEditor 삭제됨: 채번 규칙은 저장 탭 > 자동생성 필드에서 관리
|
||||
|
||||
// ========================================
|
||||
// JsonKeySelect: JSON 키 드롭다운 (자동 추출)
|
||||
|
|
|
|||
|
|
@ -638,6 +638,11 @@ export interface CollectedDataResponse {
|
|||
export interface SaveMapping {
|
||||
targetTable: string;
|
||||
columnMapping: Record<string, string>;
|
||||
autoGenMappings?: Array<{
|
||||
numberingRuleId: string;
|
||||
targetColumn: string;
|
||||
showResultModal?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface StatusChangeRule {
|
||||
|
|
|
|||
Loading…
Reference in New Issue