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 { getPool } from "../database/db";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import { numberingRuleService } from "../services/numberingRuleService";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -12,9 +13,16 @@ function isSafeIdentifier(name: string): boolean {
|
||||||
return SAFE_IDENTIFIER.test(name);
|
return SAFE_IDENTIFIER.test(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AutoGenMappingInfo {
|
||||||
|
numberingRuleId: string;
|
||||||
|
targetColumn: string;
|
||||||
|
showResultModal?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface MappingInfo {
|
interface MappingInfo {
|
||||||
targetTable: string;
|
targetTable: string;
|
||||||
columnMapping: Record<string, string>;
|
columnMapping: Record<string, string>;
|
||||||
|
autoGenMappings?: AutoGenMappingInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusConditionRule {
|
interface StatusConditionRule {
|
||||||
|
|
@ -114,6 +122,7 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
|
||||||
|
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
|
const generatedCodes: Array<{ targetColumn: string; code: string }> = [];
|
||||||
|
|
||||||
if (action === "inbound-confirm") {
|
if (action === "inbound-confirm") {
|
||||||
// 1. 매핑 기반 INSERT (장바구니 데이터 -> 대상 테이블)
|
// 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) {
|
if (columns.length > 1) {
|
||||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
const sql = `INSERT INTO "${cardMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;
|
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({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `${processedCount}건 처리 완료${insertedCount > 0 ? `, ${insertedCount}건 생성` : ""}`,
|
message: `${processedCount}건 처리 완료${insertedCount > 0 ? `, ${insertedCount}건 생성` : ""}`,
|
||||||
data: { processedCount, insertedCount },
|
data: { processedCount, insertedCount, generatedCodes },
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ function SelectTrigger({
|
||||||
size?: "xs" | "sm" | "default";
|
size?: "xs" | "sm" | "default";
|
||||||
}) {
|
}) {
|
||||||
// className에 h-full/h-[ 또는 style.height가 있으면 data-size 높이를 무시
|
// 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 (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,32 @@ export function PopButtonComponent({
|
||||||
const [confirmProcessing, setConfirmProcessing] = useState(false);
|
const [confirmProcessing, setConfirmProcessing] = useState(false);
|
||||||
const [showInboundConfirm, setShowInboundConfirm] = useState(false);
|
const [showInboundConfirm, setShowInboundConfirm] = useState(false);
|
||||||
const [inboundSelectedCount, setInboundSelectedCount] = useState(0);
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -576,14 +602,17 @@ export function PopButtonComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.data?.success) {
|
if (result.data?.success) {
|
||||||
|
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}건 입고 확정 완료`);
|
toast.success(`${selectedItems.length}건 입고 확정 완료`);
|
||||||
publish(`__comp_output__${componentId}__action_completed`, {
|
publish(`__comp_output__${componentId}__action_completed`, {
|
||||||
action: "inbound-confirm",
|
action: "inbound-confirm",
|
||||||
success: true,
|
success: true,
|
||||||
count: selectedItems.length,
|
count: selectedItems.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 후속 액션 실행 (navigate, refresh 등)
|
|
||||||
const followUps = config?.followUpActions ?? [];
|
const followUps = config?.followUpActions ?? [];
|
||||||
for (const fa of followUps) {
|
for (const fa of followUps) {
|
||||||
switch (fa.type) {
|
switch (fa.type) {
|
||||||
|
|
@ -600,6 +629,7 @@ export function PopButtonComponent({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.data?.message || "입고 확정에 실패했습니다.");
|
toast.error(result.data?.message || "입고 확정에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
@ -811,6 +841,36 @@ export function PopButtonComponent({
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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(); }}>
|
<AlertDialog open={!!pendingConfirm} onOpenChange={(open) => { if (!open) cancelConfirm(); }}>
|
||||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
<AlertDialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
||||||
|
|
|
||||||
|
|
@ -998,12 +998,13 @@ function Card({
|
||||||
return 999999;
|
return 999999;
|
||||||
}, [limitCol, row]);
|
}, [limitCol, row]);
|
||||||
|
|
||||||
// 제한 컬럼이 있으면 최대값으로 자동 초기화
|
// 제한 컬럼이 있으면 최대값으로 자동 초기화 (장바구니 목록 모드에서는 cart 수량 유지)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isCartListMode) return;
|
||||||
if (inputField?.enabled && limitCol && effectiveMax > 0 && effectiveMax < 999999) {
|
if (inputField?.enabled && limitCol && effectiveMax > 0 && effectiveMax < 999999) {
|
||||||
setInputValue(effectiveMax);
|
setInputValue(effectiveMax);
|
||||||
}
|
}
|
||||||
}, [effectiveMax, inputField?.enabled, limitCol]);
|
}, [effectiveMax, inputField?.enabled, limitCol, isCartListMode]);
|
||||||
|
|
||||||
const cardStyle: React.CSSProperties = {
|
const cardStyle: React.CSSProperties = {
|
||||||
height: `${scaled.cardHeight}px`,
|
height: `${scaled.cardHeight}px`,
|
||||||
|
|
|
||||||
|
|
@ -2784,6 +2784,13 @@ function SaveMappingSection({
|
||||||
label: f.label || f.columnName,
|
label: f.label || f.columnName,
|
||||||
badge: "본문",
|
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) {
|
if (inputFieldConfig?.enabled) {
|
||||||
|
|
@ -2855,6 +2862,21 @@ function SaveMappingSection({
|
||||||
[mapping.mappings]
|
[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(
|
const unmappedCardFields = useMemo(
|
||||||
() => cardDisplayedFields.filter((f) => !mappedSourceFields.has(f.sourceField)),
|
() => cardDisplayedFields.filter((f) => !mappedSourceFields.has(f.sourceField)),
|
||||||
|
|
@ -2937,7 +2959,7 @@ function SaveMappingSection({
|
||||||
</div>
|
</div>
|
||||||
{isCartMeta(entry.sourceField) ? (
|
{isCartMeta(entry.sourceField) ? (
|
||||||
!badge && <span className="text-[9px] text-muted-foreground">장바구니</span>
|
!badge && <span className="text-[9px] text-muted-foreground">장바구니</span>
|
||||||
) : (
|
) : entry.sourceField.startsWith("__formula_") ? null : (
|
||||||
<span className="truncate text-[9px] text-muted-foreground">
|
<span className="truncate text-[9px] text-muted-foreground">
|
||||||
{entry.sourceField}
|
{entry.sourceField}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,13 @@ export function PopFieldComponent({
|
||||||
columnMapping: Object.fromEntries(
|
columnMapping: Object.fromEntries(
|
||||||
(cfg.saveConfig.fieldMappings || []).map((m) => [m.fieldId, m.targetColumn])
|
(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,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
@ -585,18 +592,21 @@ function SelectFieldInput({
|
||||||
dataApi
|
dataApi
|
||||||
.getTableData(source.tableName, {
|
.getTableData(source.tableName, {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 500,
|
size: 500,
|
||||||
sortColumn: source.labelColumn,
|
sortBy: source.labelColumn,
|
||||||
sortDirection: "asc",
|
sortOrder: "asc",
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data?.success && Array.isArray(res.data.data?.data)) {
|
if (Array.isArray(res.data)) {
|
||||||
setOptions(
|
const seen = new Set<string>();
|
||||||
res.data.data.data.map((row: Record<string, unknown>) => ({
|
const deduped: { value: string; label: string }[] = [];
|
||||||
value: String(row[source.valueColumn!] ?? ""),
|
for (const row of res.data) {
|
||||||
label: String(row[source.labelColumn!] ?? ""),
|
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(() => {
|
.catch(() => {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ import {
|
||||||
type ColumnInfo,
|
type ColumnInfo,
|
||||||
} from "../pop-dashboard/utils/dataFetcher";
|
} from "../pop-dashboard/utils/dataFetcher";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
import { getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
import { getAvailableNumberingRulesForScreen, getNumberingRules } from "@/lib/api/numberingRule";
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -462,6 +462,8 @@ function SaveTabContent({
|
||||||
// --- 자동생성 필드 로직 ---
|
// --- 자동생성 필드 로직 ---
|
||||||
const autoGenMappings = cfg.saveConfig?.autoGenMappings ?? [];
|
const autoGenMappings = cfg.saveConfig?.autoGenMappings ?? [];
|
||||||
const [numberingRules, setNumberingRules] = useState<{ ruleId: string; ruleName: string }[]>([]);
|
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 자동 동기화
|
// 레이아웃 auto 필드 → autoGenMappings 자동 동기화
|
||||||
const autoFieldIdsKey = autoInputFields.map(({ field }) => field.id).join(",");
|
const autoFieldIdsKey = autoInputFields.map(({ field }) => field.id).join(",");
|
||||||
|
|
@ -478,7 +480,7 @@ function SaveTabContent({
|
||||||
label: field.labelText || "",
|
label: field.labelText || "",
|
||||||
targetColumn: "",
|
targetColumn: "",
|
||||||
numberingRuleId: field.autoNumber?.numberingRuleId ?? "",
|
numberingRuleId: field.autoNumber?.numberingRuleId ?? "",
|
||||||
showInForm: true,
|
showInForm: false,
|
||||||
showResultModal: true,
|
showResultModal: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -513,6 +515,24 @@ function SaveTabContent({
|
||||||
}
|
}
|
||||||
}, [saveTableName]);
|
}, [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 addAutoGenMapping = useCallback(() => {
|
||||||
const newMapping: PopFieldAutoGenMapping = {
|
const newMapping: PopFieldAutoGenMapping = {
|
||||||
id: `autogen_${Date.now()}`,
|
id: `autogen_${Date.now()}`,
|
||||||
|
|
@ -1248,7 +1268,19 @@ function SaveTabContent({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-[10px]">채번 규칙</Label>
|
<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
|
<Select
|
||||||
value={m.numberingRuleId || "__none__"}
|
value={m.numberingRuleId || "__none__"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
|
|
@ -1262,11 +1294,19 @@ function SaveTabContent({
|
||||||
<SelectItem value="__none__" className="text-xs">
|
<SelectItem value="__none__" className="text-xs">
|
||||||
선택
|
선택
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{numberingRules.map((r) => (
|
{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">
|
<SelectItem key={r.ruleId} value={r.ruleId} className="text-xs">
|
||||||
{r.ruleName || r.ruleId}
|
{r.ruleName || r.ruleId}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1690,12 +1730,13 @@ function FieldItemEditor({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* auto 전용: 채번 설정 */}
|
{/* auto 전용: 저장 탭에서 채번 규칙을 연결하라는 안내 */}
|
||||||
{field.inputType === "auto" && (
|
{field.inputType === "auto" && (
|
||||||
<AutoNumberEditor
|
<div className="rounded border bg-muted/30 p-2">
|
||||||
config={field.autoNumber}
|
<p className="text-[10px] text-muted-foreground">
|
||||||
onUpdate={(autoNumber) => onUpdate({ autoNumber })}
|
채번 규칙은 [저장] 탭 > 자동생성 필드에서 설정합니다.
|
||||||
/>
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1948,108 +1989,7 @@ function TableSourceEditor({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// AutoNumberEditor 삭제됨: 채번 규칙은 저장 탭 > 자동생성 필드에서 관리
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// JsonKeySelect: JSON 키 드롭다운 (자동 추출)
|
// JsonKeySelect: JSON 키 드롭다운 (자동 추출)
|
||||||
|
|
|
||||||
|
|
@ -638,6 +638,11 @@ export interface CollectedDataResponse {
|
||||||
export interface SaveMapping {
|
export interface SaveMapping {
|
||||||
targetTable: string;
|
targetTable: string;
|
||||||
columnMapping: Record<string, string>;
|
columnMapping: Record<string, string>;
|
||||||
|
autoGenMappings?: Array<{
|
||||||
|
numberingRuleId: string;
|
||||||
|
targetColumn: string;
|
||||||
|
showResultModal?: boolean;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatusChangeRule {
|
export interface StatusChangeRule {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue