diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts index 0845b1cb..ac023907 100644 --- a/backend-node/src/controllers/batchManagementController.ts +++ b/backend-node/src/controllers/batchManagementController.ts @@ -501,12 +501,13 @@ export class BatchManagementController { }); // RestApiConnector 사용하여 데이터 조회 + // 미리보기는 프론트 기본 30초 타임아웃 안에 응답해야 하므로 20초로 제한 — 실패 시 빠르게 피드백 const { RestApiConnector } = await import("../database/RestApiConnector"); const connector = new RestApiConnector({ baseUrl: apiUrl, apiKey: finalApiKey, - timeout: 30000, + timeout: 20000, }); // 연결 테스트 @@ -600,15 +601,15 @@ export class BatchManagementController { } const data = extractedData.slice(0, 5); // 최대 5개 샘플만 + // 미리보기 응답 CPU 부담 줄이기 위해 데이터 본문 로깅은 생략 (개수만 기록) console.log( - `[previewRestApiData] 슬라이스된 데이터 (${extractedData.length}개 중 ${data.length}개):`, - data + `[previewRestApiData] 샘플 ${data.length}건 추출 (전체 ${extractedData.length}건)` ); if (data.length > 0) { // 첫 번째 객체에서 필드명 추출 const fields = Object.keys(data[0]); - console.log(`[previewRestApiData] 추출된 필드:`, fields); + console.log(`[previewRestApiData] 추출된 필드 수: ${fields.length}`); return res.json({ success: true, diff --git a/backend-node/src/services/batchManagementService.ts b/backend-node/src/services/batchManagementService.ts index 72ff52c4..294374d3 100644 --- a/backend-node/src/services/batchManagementService.ts +++ b/backend-node/src/services/batchManagementService.ts @@ -5,6 +5,62 @@ import { query, queryOne } from "../database/db"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; +// 외부 DB 메타데이터 TTL 캐시 — 같은 연결/테이블 반복 조회 시 커넥터 생성 오버헤드 회피 +type CacheEntry = { value: T; expiresAt: number }; +const metadataCache = new Map>(); +const METADATA_TTL_MS = 3 * 60 * 1000; // 3분 + +function cacheGet(key: string): T | null { + const entry = metadataCache.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + metadataCache.delete(key); + return null; + } + return entry.value as T; +} + +function cacheSet(key: string, value: T, ttlMs = METADATA_TTL_MS): void { + metadataCache.set(key, { value, expiresAt: Date.now() + ttlMs }); +} + +export function invalidateBatchMetadataCache(connectionId?: number): void { + if (connectionId === undefined) { + metadataCache.clear(); + return; + } + const prefix = `:${connectionId}:`; + for (const key of metadataCache.keys()) { + if (key.includes(prefix)) metadataCache.delete(key); + } +} + +// 외부 DB 메타데이터 조회 하드 타임아웃 — 응답 없는 DB로 인한 프론트 타임아웃(30초) 방지 +const EXTERNAL_DB_TIMEOUT_MS = 15000; + +class ExternalDbTimeoutError extends Error { + constructor(public readonly context: string, public readonly timeoutMs: number) { + super(`${context} (${timeoutMs / 1000}초 초과)`); + this.name = "ExternalDbTimeoutError"; + } +} + +function withTimeout( + promise: Promise, + timeoutMs: number, + context: string +): Promise { + let timeoutHandle: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutHandle = setTimeout(() => { + reject(new ExternalDbTimeoutError(context, timeoutMs)); + }, timeoutMs); + }); + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timeoutHandle) clearTimeout(timeoutHandle); + }); +} + // 배치관리 전용 타입 정의 export interface BatchConnectionInfo { type: "internal" | "external"; @@ -109,20 +165,27 @@ export class BatchManagementService { let tables: BatchTableInfo[] = []; if (connectionType === "internal") { - // 내부 DB 테이블 조회 - const result = await query<{ table_name: string }>( - `SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_type = 'BASE TABLE' - ORDER BY table_name`, - [] - ); + // 내부 DB 테이블 목록도 TTL 캐시 — 스키마는 자주 바뀌지 않음 + const cacheKey = `int-tables`; + const cached = cacheGet(cacheKey); + if (cached) { + tables = cached; + } else { + const result = await query<{ table_name: string }>( + `SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name`, + [] + ); - tables = result.map((row) => ({ - table_name: row.table_name, - columns: [], - })); + tables = result.map((row) => ({ + table_name: row.table_name, + columns: [], + })); + cacheSet(cacheKey, tables); + } } else if (connectionType === "external" && connectionId) { // 외부 DB 테이블 조회 (회사별 필터링) const tablesResult = await this.getExternalTables( @@ -168,39 +231,38 @@ export class BatchManagementService { let columns: BatchColumnInfo[] = []; if (connectionType === "internal") { - // 내부 DB 컬럼 조회 - console.log( - `[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}` - ); + // 내부 DB 컬럼도 TTL 캐시 — information_schema 반복 조회로 DB 풀 고갈 방지 + const cacheKey = `int-columns:${tableName}`; + const cached = cacheGet(cacheKey); + if (cached) { + columns = cached; + } else { + const result = await query<{ + column_name: string; + data_type: string; + is_nullable: string; + column_default: string | null; + }>( + `SELECT + column_name, + data_type, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = $1 + ORDER BY ordinal_position`, + [tableName] + ); - const result = await query<{ - column_name: string; - data_type: string; - is_nullable: string; - column_default: string | null; - }>( - `SELECT - column_name, - data_type, - is_nullable, - column_default - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = $1 - ORDER BY ordinal_position`, - [tableName] - ); - - console.log(`[BatchManagementService] 쿼리 결과:`, result); - - console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result); - - columns = result.map((row) => ({ - column_name: row.column_name, - data_type: row.data_type, - is_nullable: row.is_nullable, - column_default: row.column_default, - })); + columns = result.map((row) => ({ + column_name: row.column_name, + data_type: row.data_type, + is_nullable: row.is_nullable, + column_default: row.column_default, + })); + cacheSet(cacheKey, columns); + } } else if (connectionType === "external" && connectionId) { // 외부 DB 컬럼 조회 (회사별 필터링) console.log( @@ -247,6 +309,17 @@ export class BatchManagementService { userCompanyCode?: string ): Promise> { try { + // TTL 캐시 조회 (커넥터 생성/SSL 핸드쉐이크 오버헤드 회피) + const cacheKey = `ext-tables:${connectionId}:${userCompanyCode || "*"}`; + const cached = cacheGet(cacheKey); + if (cached) { + return { + success: true, + message: "테이블 목록을 조회했습니다. (cache)", + data: cached, + }; + } + // 연결 정보 조회 (회사별 필터링) let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`; const params: any[] = [connectionId]; @@ -296,13 +369,23 @@ export class BatchManagementService { : false, }; - // DatabaseConnectorFactory를 통한 테이블 목록 조회 - const connector = await DatabaseConnectorFactory.createConnector( - connection.db_type, - config, - connectionId + // DatabaseConnectorFactory를 통한 테이블 목록 조회 (하드 타임아웃) + const connector = await withTimeout( + DatabaseConnectorFactory.createConnector( + connection.db_type, + config, + connectionId + ), + EXTERNAL_DB_TIMEOUT_MS, + `외부 DB '${connection.connection_name}' 연결 시간 초과` ); - const tables = await connector.getTables(); + const tables = await withTimeout( + connector.getTables(), + EXTERNAL_DB_TIMEOUT_MS, + `외부 DB '${connection.connection_name}' 테이블 목록 조회 시간 초과` + ); + + cacheSet(cacheKey, tables); return { success: true, @@ -311,6 +394,13 @@ export class BatchManagementService { }; } catch (error) { console.error("외부 DB 테이블 목록 조회 오류:", error); + if (error instanceof ExternalDbTimeoutError) { + return { + success: false, + message: `${error.message}. 해당 DB 서버의 상태와 네트워크 연결을 확인해주세요.`, + error: error.message, + }; + } return { success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", @@ -332,6 +422,20 @@ export class BatchManagementService { `[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}` ); + // TTL 캐시 조회 (커넥터 생성/메타데이터 조회 오버헤드 회피) + const cacheKey = `ext-columns:${connectionId}:${tableName}:${userCompanyCode || "*"}`; + const cached = cacheGet(cacheKey); + if (cached) { + console.log( + `[BatchManagementService] 캐시에서 컬럼 정보 반환: key=${cacheKey}, 개수=${cached.length}` + ); + return { + success: true, + data: cached, + message: "컬럼 정보를 조회했습니다. (cache)", + }; + } + // 연결 정보 조회 (회사별 필터링) let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`; const params: any[] = [connectionId]; @@ -391,20 +495,28 @@ export class BatchManagementService { `[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}` ); - // 데이터베이스 타입에 따른 커넥터 생성 - const connector = await DatabaseConnectorFactory.createConnector( - connection.db_type, - config, - connectionId + // 데이터베이스 타입에 따른 커넥터 생성 (하드 타임아웃) + const connector = await withTimeout( + DatabaseConnectorFactory.createConnector( + connection.db_type, + config, + connectionId + ), + EXTERNAL_DB_TIMEOUT_MS, + `외부 DB '${connection.connection_name}' 연결 시간 초과` ); console.log( `[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}` ); - // 컬럼 정보 조회 + // 컬럼 정보 조회 (하드 타임아웃) console.log(`[BatchManagementService] connector.getColumns 호출 전`); - const columns = await connector.getColumns(tableName); + const columns = await withTimeout( + connector.getColumns(tableName), + EXTERNAL_DB_TIMEOUT_MS, + `외부 DB '${connection.connection_name}' 컬럼 조회 시간 초과` + ); console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns); console.log( @@ -451,6 +563,8 @@ export class BatchManagementService { standardizedColumns ); + cacheSet(cacheKey, standardizedColumns); + return { success: true, data: standardizedColumns, @@ -461,6 +575,13 @@ export class BatchManagementService { "[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error ); + if (error instanceof ExternalDbTimeoutError) { + return { + success: false, + message: `${error.message}. 해당 DB 서버의 상태와 네트워크 연결을 확인해주세요.`, + error: error.message, + }; + } console.error( "[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : "No stack trace" diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index c8b6ecbe..5a98a7c6 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -72,13 +72,20 @@ export class BatchService { const total = parseInt(countResult[0].count); const totalPages = Math.ceil(total / limit); - // 목록 조회 (최근 실행 정보 포함) + // 목록 조회 — LATERAL JOIN으로 batch_execution_logs 최신 1건을 단일 스캔으로 조회 (N+1 방지) const configs = await query( `SELECT bc.*, - (SELECT bel.execution_status FROM batch_execution_logs bel WHERE bel.batch_config_id = bc.id ORDER BY bel.start_time DESC LIMIT 1) as last_status, - (SELECT bel.start_time FROM batch_execution_logs bel WHERE bel.batch_config_id = bc.id ORDER BY bel.start_time DESC LIMIT 1) as last_executed_at, - (SELECT bel.total_records FROM batch_execution_logs bel WHERE bel.batch_config_id = bc.id ORDER BY bel.start_time DESC LIMIT 1) as last_total_records + bel.execution_status as last_status, + bel.start_time as last_executed_at, + bel.total_records as last_total_records FROM batch_configs bc + LEFT JOIN LATERAL ( + SELECT execution_status, start_time, total_records + FROM batch_execution_logs + WHERE batch_config_id = bc.id + ORDER BY start_time DESC + LIMIT 1 + ) bel ON true ${whereClause} ORDER BY bc.created_date DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`, diff --git a/backend-node/src/services/externalDbConnectionService.ts b/backend-node/src/services/externalDbConnectionService.ts index 410e8daf..6640e593 100644 --- a/backend-node/src/services/externalDbConnectionService.ts +++ b/backend-node/src/services/externalDbConnectionService.ts @@ -10,6 +10,7 @@ import { } from "../types/externalDbTypes"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; +import { invalidateBatchMetadataCache } from "./batchManagementService"; import logger from "../utils/logger"; export class ExternalDbConnectionService { @@ -490,6 +491,9 @@ export class ExternalDbConnectionService { updateParams ); + // 연결 정보가 바뀌었으므로 배치관리 메타데이터 캐시 무효화 + invalidateBatchMetadataCache(id); + // 비밀번호는 반환하지 않음 const safeConnection = { ...updatedConnection, @@ -541,6 +545,9 @@ export class ExternalDbConnectionService { // 물리 삭제 (실제 데이터 삭제) await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]); + // 연결이 삭제되었으므로 배치관리 메타데이터 캐시 무효화 + invalidateBatchMetadataCache(id); + logger.info( `외부 DB 연결 삭제: ID ${id} (회사: ${userCompanyCode || "전체"})` ); diff --git a/frontend/app/(main)/COMPANY_10/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_10/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_10/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_10/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_16/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_16/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_16/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_16/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_29/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_29/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_29/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_29/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_30/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_30/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_30/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_30/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_7/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_7/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_7/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_7/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_8/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_8/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_8/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_8/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/COMPANY_9/production/work-instruction/WorkStandardEditModal.tsx b/frontend/app/(main)/COMPANY_9/production/work-instruction/WorkStandardEditModal.tsx index 96b96535..ccb0afda 100644 --- a/frontend/app/(main)/COMPANY_9/production/work-instruction/WorkStandardEditModal.tsx +++ b/frontend/app/(main)/COMPANY_9/production/work-instruction/WorkStandardEditModal.tsx @@ -1211,7 +1211,7 @@ export function WorkStandardEditModal({ const openAddWorkItem = useCallback((phaseKey: string) => { setAddItemPhase(phaseKey); setAddItemTitle(""); - setAddItemRequired("Y"); + setAddItemRequired("N"); setEditingWorkItem(null); setAddItemOpen(true); }, []); @@ -1581,12 +1581,6 @@ export function WorkStandardEditModal({ {item.details?.length || item.detail_count || 0}개 - - {item.is_required === "Y" ? "필수" : "선택"} -
@@ -1766,10 +1760,6 @@ export function WorkStandardEditModal({ setAddItemTitle(e.target.value)} placeholder="작업항목 제목" className="h-8 text-xs mt-1" />
-
- setAddItemRequired(v ? "Y" : "N")} /> - -
diff --git a/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx index e8b90461..9590aad0 100644 --- a/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx @@ -245,7 +245,7 @@ export default function BatchCreatePage() { const selectedFlow = nodeFlows.find(f => f.flow_id === selectedFlowId); return ( -
+
{/* 헤더 */}

#{batchId} 배치 설정을 수정해요

- +
+ + +
+ + {/* 매핑이 비어 있으면 안내 */} + {batchConfig && (!batchConfig.batch_mappings || batchConfig.batch_mappings.length === 0) && executionType !== "node_flow" && ( +
+

매핑 정보가 비어 있어요

+

+ 이 배치에 저장된 컬럼 매핑이 없습니다. 방금 저장하셨다면 [새로고침] 버튼을 눌러 최신 상태로 다시 불러와 주세요. +

+
+ )} {/* 기본 정보 */} diff --git a/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx index 4f927006..e64823b9 100644 --- a/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx @@ -338,13 +338,26 @@ export default function BatchManagementPage() { ]); if (configsResponse.success && configsResponse.data) { setBatchConfigs(configsResponse.data); - // 각 배치의 스파크라인을 백그라운드로 로드 + // 스파크라인 백그라운드 로드 — 동시 요청 3개로 제한하여 DB 풀/타임아웃 방지 const ids = configsResponse.data.map(b => b.id!).filter(Boolean); - Promise.all(ids.map(id => BatchAPI.getBatchSparkline(id).then(data => ({ id, data })))).then(results => { - const cache: Record = {}; - results.forEach(r => { cache[r.id] = r.data; }); - setSparklineCache(prev => ({ ...prev, ...cache })); - }); + (async () => { + const CONCURRENCY = 3; + for (let i = 0; i < ids.length; i += CONCURRENCY) { + const chunk = ids.slice(i, i + CONCURRENCY); + const results = await Promise.all( + chunk.map(id => + BatchAPI.getBatchSparkline(id) + .then(data => ({ id, data })) + .catch(() => ({ id, data: [] as SparklineData[] })) + ) + ); + setSparklineCache(prev => { + const next = { ...prev }; + results.forEach(r => { next[r.id] = r.data; }); + return next; + }); + } + })(); } else { setBatchConfigs([]); } @@ -445,8 +458,8 @@ export default function BatchManagementPage() { const failDiff = stats ? stats.todayFailures - stats.prevDayFailures : 0; return ( -
-
+
+
{/* 헤더 */}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx index 1745b3b6..67c86541 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx @@ -65,7 +65,6 @@ export function WorkItemAddModal({ selectedItemCode, }: WorkItemAddModalProps) { const [title, setTitle] = useState(""); - const [isRequired, setIsRequired] = useState("Y"); const [description, setDescription] = useState(""); const [details, setDetails] = useState([]); const [detailFormOpen, setDetailFormOpen] = useState(false); @@ -73,11 +72,9 @@ export function WorkItemAddModal({ useEffect(() => { if (open && editItem) { setTitle(editItem.title || ""); - setIsRequired(editItem.is_required || "Y"); setDescription(editItem.description || ""); } else if (open && !editItem) { setTitle(""); - setIsRequired("Y"); setDescription(""); setDetails([]); } @@ -85,7 +82,6 @@ export function WorkItemAddModal({ const resetForm = () => { setTitle(""); - setIsRequired("Y"); setDescription(""); setDetails([]); }; @@ -95,7 +91,7 @@ export function WorkItemAddModal({ onSave({ work_phase: phaseKey, title: title.trim(), - is_required: isRequired, + is_required: "N", description: description.trim() || undefined, details: details .filter((d) => d.content.trim()) @@ -163,34 +159,16 @@ export function WorkItemAddModal({

기본 정보

-
-
- - setTitle(e.target.value)} - placeholder="예: 장비 점검, 품질 검사" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> -
-
- - -
+
+ + setTitle(e.target.value)} + placeholder="예: 장비 점검, 품질 검사" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + />
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemCard.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemCard.tsx index 6d5787aa..56d8f6bd 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemCard.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemCard.tsx @@ -48,12 +48,6 @@ export function WorkItemCard({ > {item.detail_count}개 - - {item.is_required === "Y" ? "필수" : "선택"} -