fix: 구매입고 확정 시 품목 NULL + 채번 실패 수정

- popActionRoutes.ts: autoGenMappings 대상 컬럼을 columnMapping에서 제외
- 근본 원인: autoGen 컬럼이 빈값으로 선점되어 채번 SKIP
- pop-button.tsx, PopCardListComponent.tsx: 관련 프론트엔드 수정
This commit is contained in:
SeongHyun Kim 2026-03-31 09:31:14 +09:00
parent 4fa8c3969d
commit 76a708cab2
3 changed files with 46 additions and 8 deletions

View File

@ -183,6 +183,15 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
const cardMapping = mappings?.cardList;
const fieldMapping = mappings?.field;
logger.info("[pop/execute-action] data-save 분기 판정", {
hasCardMapping: !!cardMapping?.targetTable,
cardMappingColumns: cardMapping?.columnMapping ? Object.keys(cardMapping.columnMapping).length : 0,
itemCount: items.length,
hasFieldMapping: !!fieldMapping?.targetTable,
fieldMappingColumns: fieldMapping?.columnMapping ? Object.keys(fieldMapping.columnMapping).length : 0,
fieldValueKeys: Object.keys(fieldValues),
});
if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0 && items.length > 0) {
// ── 카드리스트 기반 INSERT (기존: items 반복) ──
if (!isSafeIdentifier(cardMapping.targetTable)) {
@ -214,12 +223,18 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
}
}
// autoGen 대상 컬럼은 필드/히든 매핑에서 제외 (빈값으로 덮어쓰기 방지)
const autoGenTargetColumns = new Set(
allAutoGen.filter(ag => ag.numberingRuleId && ag.targetColumn).map(ag => ag.targetColumn)
);
for (const item of items) {
const columns: string[] = ["company_code"];
const values: unknown[] = [companyCode];
for (const [sourceField, targetColumn] of Object.entries(cardMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
if (autoGenTargetColumns.has(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(item[sourceField] ?? null);
}
@ -228,6 +243,7 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
if (columns.includes(`"${targetColumn}"`)) continue;
if (autoGenTargetColumns.has(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(fieldValues[sourceField] ?? null);
}
@ -315,9 +331,15 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
const columns: string[] = ["company_code"];
const values: unknown[] = [companyCode];
// autoGen 대상 컬럼은 필드 매핑에서 제외 (빈값으로 덮어쓰기 방지)
const fieldAutoGenCols = new Set(
(fieldMapping.autoGenMappings ?? []).filter(ag => ag.numberingRuleId && ag.targetColumn).map(ag => ag.targetColumn)
);
// 필드 매핑 값 추가
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
if (fieldAutoGenCols.has(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(fieldValues[sourceField] ?? null);
}
@ -638,12 +660,18 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
}
}
// autoGen 대상 컬럼은 필드/히든 매핑에서 제외 (빈값으로 덮어쓰기 방지)
const autoGenTargetColumns = new Set(
allAutoGen.filter(ag => ag.numberingRuleId && ag.targetColumn).map(ag => ag.targetColumn)
);
for (const item of items) {
const columns: string[] = ["company_code"];
const values: unknown[] = [companyCode];
for (const [sourceField, targetColumn] of Object.entries(cardMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
if (autoGenTargetColumns.has(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(item[sourceField] ?? null);
}
@ -652,6 +680,7 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
if (columns.includes(`"${targetColumn}"`)) continue;
if (autoGenTargetColumns.has(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(fieldValues[sourceField] ?? null);
}

View File

@ -654,7 +654,7 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp
// 장바구니 모드 상태 (v1 preset 또는 v2 tasks에 cart-save가 있으면 활성)
// showCartBadge: true인 경우에도 활성화 (cart-save 없이 배지만 표시할 때)
const v2Tasks = (config && "tasks" in config && Array.isArray((config as any).tasks))
? (config as any).tasks as PopButtonTask[]
? (config as any).tasks as ButtonTask[]
: null;
const hasCartSaveTask = !!v2Tasks?.some((t) => t.type === "cart-save");
const isCartMode = config?.preset === "cart" || hasCartSaveTask || !!(config as any)?.showCartBadge;
@ -798,11 +798,18 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp
cartSaveTimeoutRef.current = setTimeout(() => {
setCartSaving((prev) => {
if (prev) {
toast.error("장바구니 저장 응답이 없습니다. 연결 설정을 확인하세요.");
// 이벤트 응답이 없지만 저장 대상 화면이 있으면 네비게이션 시도 (폴백)
const targetScreenId = cartScreenIdRef.current;
if (targetScreenId) {
const cleanId = String(targetScreenId).replace(/^.*\/(\d+)$/, "$1").trim();
window.location.href = `/pop/screens/${cleanId}`;
} else {
toast.error("장바구니 저장 응답이 없습니다. 연결 설정을 확인하세요.");
}
}
return false;
});
}, 10_000);
}, 5_000);
}, [componentId, publish, config?.cart?.rowDataMode, config?.cart?.selectedColumns]);
// 저장 완료 시 타임아웃 정리
@ -943,8 +950,8 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp
items: cardListData?.data?.items ?? [],
fieldValues: fieldData?.data?.values ?? {},
mappings: {
cardList: cardListData?.mapping ?? null,
field: fieldData?.mapping ?? null,
cardList: (cardListData?.mapping ?? null) as Record<string, unknown> | null | undefined,
field: (fieldData?.mapping ?? null) as Record<string, unknown> | null | undefined,
},
cartChanges: (cardListData?.data as Record<string, unknown>)?.cartChanges as CollectedPayload["cartChanges"],
};

View File

@ -527,7 +527,7 @@ export function PopCardListComponent({
try {
// 원본 화면 레이아웃에서 설정 전체 상속 (cardTemplate, inputField, packageConfig, cardSize 등)
try {
const layoutJson = await screenApi.getLayoutPop(cartListMode.sourceScreenId);
const layoutJson = await screenApi.getLayoutPop(cartListMode.sourceScreenId!);
const componentsMap = layoutJson?.components || {};
const componentList = Object.values(componentsMap) as any[];
const matched = cartListMode.sourceComponentId
@ -694,7 +694,9 @@ export function PopCardListComponent({
const request = (payload as Record<string, unknown>)?.value as CollectDataRequest | undefined;
const selectedItems = isCartListMode
? filteredRows.filter(r => selectedKeys.has(String(r.__cart_id ?? "")))
? (selectedKeys.size > 0
? filteredRows.filter(r => selectedKeys.has(String(r.__cart_id ?? "")))
: filteredRows)
: rows;
// CardListSaveMapping → SaveMapping 변환
@ -716,7 +718,7 @@ export function PopCardListComponent({
requestId: request?.requestId ?? "",
componentId: componentId,
componentType: "pop-card-list",
data: { items: selectedItems, cartChanges },
data: { items: selectedItems, cartChanges } as any,
mapping,
};