feat: field-only INSERT + pop-field key 수정 + 모달 접근성 개선

- popActionRoutes.ts: 카드리스트 없이 필드만으로 INSERT 가능 (field-only 분기)
- PopFieldComponent.tsx: React duplicate key 에러 수정 (staticOptions 문자열 변환 + key fallback)
- pop-field/index.tsx: preview nested map key fallback
- PopViewerWithModals.tsx: 모달 제목 없을 때 sr-only 접근성 처리
- PopWorkDetailComponent.tsx: 모달 내부 헤더 중복 제거 + isInModal 자동 감지
This commit is contained in:
SeongHyun Kim
2026-03-30 10:16:27 +09:00
parent 6fe7bfbefc
commit 1532184065
5 changed files with 129 additions and 32 deletions

View File

@@ -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;
}