diff --git a/backend-node/src/routes/popActionRoutes.ts b/backend-node/src/routes/popActionRoutes.ts
index 669cc960..6a997e32 100644
--- a/backend-node/src/routes/popActionRoutes.ts
+++ b/backend-node/src/routes/popActionRoutes.ts
@@ -183,7 +183,8 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
const cardMapping = mappings?.cardList;
const fieldMapping = mappings?.field;
- if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0) {
+ if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0 && items.length > 0) {
+ // ── 카드리스트 기반 INSERT (기존: items 반복) ──
if (!isSafeIdentifier(cardMapping.targetTable)) {
throw new Error(`유효하지 않은 테이블명: ${cardMapping.targetTable}`);
}
@@ -300,6 +301,84 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
insertedCount++;
}
}
+ } else if (
+ // ── 필드 단독 INSERT (카드리스트 없이 pop-field만으로 저장) ──
+ items.length === 0 &&
+ fieldMapping?.targetTable &&
+ Object.keys(fieldMapping.columnMapping).length > 0 &&
+ Object.keys(fieldValues).length > 0
+ ) {
+ if (!isSafeIdentifier(fieldMapping.targetTable)) {
+ throw new Error(`유효하지 않은 테이블명: ${fieldMapping.targetTable}`);
+ }
+
+ const columns: string[] = ["company_code"];
+ const values: unknown[] = [companyCode];
+
+ // 필드 매핑 값 추가
+ for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
+ if (!isSafeIdentifier(targetColumn)) continue;
+ columns.push(`"${targetColumn}"`);
+ values.push(fieldValues[sourceField] ?? null);
+ }
+
+ // hiddenMappings 처리
+ for (const hm of (fieldMapping.hiddenMappings ?? [])) {
+ if (!hm.targetColumn || !isSafeIdentifier(hm.targetColumn)) continue;
+ if (columns.includes(`"${hm.targetColumn}"`)) continue;
+ let value: unknown = null;
+ if (hm.valueSource === "static") {
+ value = hm.staticValue ?? null;
+ } else if (hm.valueSource === "db_column" && hm.sourceDbColumn) {
+ value = fieldValues[hm.sourceDbColumn] ?? null;
+ }
+ columns.push(`"${hm.targetColumn}"`);
+ values.push(value);
+ }
+
+ // autoGenMappings 채번 처리
+ for (const ag of (fieldMapping.autoGenMappings ?? [])) {
+ 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,
+ );
+ columns.push(`"${ag.targetColumn}"`);
+ values.push(generatedCode);
+ generatedCodes.push({ targetColumn: ag.targetColumn, code: generatedCode, showResultModal: ag.showResultModal ?? false });
+ } catch (err: any) {
+ logger.error("[pop/execute-action] 필드 단독 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
+ }
+ }
+
+ // 자동 필드 추가 (created_date, updated_date, writer)
+ if (!columns.includes('"created_date"')) {
+ columns.push('"created_date"');
+ values.push(new Date().toISOString());
+ }
+ if (!columns.includes('"updated_date"')) {
+ columns.push('"updated_date"');
+ values.push(new Date().toISOString());
+ }
+ if (!columns.includes('"writer"') && userId) {
+ columns.push('"writer"');
+ values.push(userId);
+ }
+
+ if (columns.length > 1) {
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
+ await client.query(
+ `INSERT INTO "${fieldMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`,
+ values,
+ );
+ insertedCount++;
+ logger.info("[pop/execute-action] 필드 단독 INSERT 실행", {
+ table: fieldMapping.targetTable,
+ columnCount: columns.length,
+ });
+ }
}
break;
}
diff --git a/frontend/components/pop/viewer/PopViewerWithModals.tsx b/frontend/components/pop/viewer/PopViewerWithModals.tsx
index 0688d6cb..7500348c 100644
--- a/frontend/components/pop/viewer/PopViewerWithModals.tsx
+++ b/frontend/components/pop/viewer/PopViewerWithModals.tsx
@@ -231,12 +231,14 @@ export default function PopViewerWithModals({
if (!isTopModal || !closeOnEsc) e.preventDefault();
}}
>
- {definition.title && (
+ {definition.title ? (