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 ? ( {definition.title} + ) : ( + 팝업 )}
- {cfg.sections.map((section) => { + {cfg.sections.map((section, sIdx) => { const fields = section.fields || []; const fieldCount = fields.length; if (fieldCount === 0) return null; const cols = resolveColumns(section.columns, fieldCount); return ( -
+
{section.label && (
{section.label} @@ -480,17 +480,17 @@ export function PopFieldComponent({ className="grid gap-3" style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }} > - {fields.map((field) => { + {fields.map((field, fIdx) => { const fKey = field.fieldName || field.id; return ( @@ -506,8 +506,8 @@ export function PopFieldComponent({ className="grid gap-3" style={{ gridTemplateColumns: `repeat(${resolveColumns("auto", visibleAutoGens.length)}, 1fr)` }} > - {visibleAutoGens.map((ag) => ( - + {visibleAutoGens.map((ag, agIdx) => ( + ))}
@@ -546,7 +546,7 @@ function FieldRenderer({ [onChange, field.fieldName] ); - const resolvedStyle = sectionStyle === "summary" ? "display" : sectionStyle === "form" ? "input" : sectionStyle; + const resolvedStyle = sectionStyle; const inputClassName = cn( "h-9 w-full rounded-md border px-3 text-sm", field.readOnly @@ -740,7 +740,12 @@ function SelectFieldInput({ if (!source) return; if (source.type === "static" && source.staticOptions) { - setOptions(source.staticOptions); + setOptions( + source.staticOptions.map((o) => ({ + value: String(o.value ?? ""), + label: String(o.label ?? ""), + })) + ); return; } @@ -850,8 +855,8 @@ function SelectFieldInput({ {emptyMessage}
) : ( - options.map((opt) => ( - + options.map((opt, idx) => ( + {opt.label} )) diff --git a/frontend/lib/registry/pop-components/pop-field/index.tsx b/frontend/lib/registry/pop-components/pop-field/index.tsx index 60ed1ba7..f5b234b0 100644 --- a/frontend/lib/registry/pop-components/pop-field/index.tsx +++ b/frontend/lib/registry/pop-components/pop-field/index.tsx @@ -30,10 +30,10 @@ function PopFieldPreviewComponent({ {label || "입력 필드"}
- {cfg.sections.map((section) => - (section.fields || []).slice(0, 3).map((field) => ( + {cfg.sections.map((section, sIdx) => + (section.fields || []).slice(0, 3).map((field, fIdx) => (
diff --git a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx index b20c2830..2465cb80 100644 --- a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx @@ -288,6 +288,15 @@ export function PopWorkDetailComponent({ const { getSharedData, publish } = usePopEvent(screenId || "default"); const { user } = useAuth(); + // 모달 내부 렌더링 감지: 부모에 [role="dialog"]가 있으면 모달 모드 + const containerRef = useRef(null); + const [isInModal, setIsInModal] = useState(false); + useEffect(() => { + if (containerRef.current) { + setIsInModal(!!containerRef.current.closest('[role="dialog"]')); + } + }, []); + const cfg: PopWorkDetailConfig = { ...DEFAULT_CFG, ...config, @@ -880,23 +889,25 @@ export function PopWorkDetailComponent({ const woNo = parentRow?.wo_no ? String(parentRow.wo_no) : ""; return ( -
- {/* ── 모달 헤더: 글자 크게 ── */} -
-
-

작업 상세

- {woNo && {woNo}} -
- -
+
+

작업 상세

+ {woNo && {woNo}} +
+ +
+ )} {/* ── 정보바: 미니멀 다크 (고정) ── */} {cfg.infoBar.enabled && (