diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index fbf0185b..d78f1c29 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -77,7 +77,7 @@ export default function BatchManagementPage() { const loadJobTypes = async () => { try { const types = await BatchAPI.getSupportedJobTypes(); - setJobTypes(types); + setJobTypes(types.map(t => ({ value: t, label: t }))); } catch (error) { console.error("작업 타입 조회 오류:", error); } @@ -172,8 +172,8 @@ export default function BatchManagementPage() { }; const getSuccessRate = (job: BatchJob) => { - if (job.execution_count === 0) return 100; - return Math.round((job.success_count / job.execution_count) * 100); + if (!job.execution_count) return 100; + return Math.round(((job.success_count ?? 0) / job.execution_count) * 100); }; const getSuccessRateColor = (rate: number) => { @@ -361,7 +361,7 @@ export default function BatchManagementPage() {
- {jobs.reduce((sum, job) => sum + job.execution_count, 0)} + {jobs.reduce((sum, job) => sum + (job.execution_count ?? 0), 0)}

누적 실행 횟수

@@ -373,7 +373,7 @@ export default function BatchManagementPage() {
- {jobs.reduce((sum, job) => sum + job.success_count, 0)} + {jobs.reduce((sum, job) => sum + (job.success_count ?? 0), 0)}

총 성공 횟수

@@ -385,7 +385,7 @@ export default function BatchManagementPage() {
- {jobs.reduce((sum, job) => sum + job.failure_count, 0)} + {jobs.reduce((sum, job) => sum + (job.failure_count ?? 0), 0)}

총 실패 횟수

diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index e18ddb53..daaa6d75 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -209,16 +209,101 @@ function FullWidthOverlayRow({ ); } +/** + * 데스크톱: 원본 캔버스 좌표 기반 렌더링 (transform: scale 로 축소) + * 디자이너에서 설계한 레이아웃을 그대로 유지하면서 뷰포트에 맞춤 + */ +function DesktopCanvasRenderer({ + components, + canvasWidth, + canvasHeight, + renderComponent, +}: ResponsiveGridRendererProps) { + const containerRef = useRef(null); + const [containerW, setContainerW] = useState(0); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const ro = new ResizeObserver((entries) => { + const w = entries[0]?.contentRect.width; + if (w && w > 0) setContainerW(w); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const topLevel = components.filter((c) => !c.parentId); + const scale = containerW > 0 ? Math.min(containerW / canvasWidth, 1) : 1; + const scaledHeight = canvasHeight * scale; + + return ( +
+
+ {topLevel.map((component) => { + const typeId = getComponentTypeId(component); + return ( +
+ {renderComponent(component)} +
+ ); + })} +
+
+ ); +} + export function ResponsiveGridRenderer({ components, canvasWidth, canvasHeight, renderComponent, }: ResponsiveGridRendererProps) { - const { isMobile } = useResponsive(); + const { isMobile, isTablet } = useResponsive(); + + // 데스크톱: 원본 캔버스 좌표 유지 (스케일링) + // 풀위드스 컴포넌트(테이블, 스플릿패널 등)가 있으면 flex 레이아웃 사용 + const topLevel = components.filter((c) => !c.parentId); + const hasFullWidthComponent = topLevel.some((c) => isFullWidthComponent(c)); + const useCanvasLayout = !isMobile && !isTablet && !hasFullWidthComponent; + + if (useCanvasLayout) { + return ( + + ); + } const processedRows = useMemo(() => { - const topLevel = components.filter((c) => !c.parentId); const rows = groupComponentsIntoRows(topLevel); const result: ProcessedRow[] = []; @@ -261,7 +346,7 @@ export function ResponsiveGridRenderer({ } } return result; - }, [components]); + }, [topLevel]); return (
{ return result.data || []; }, - staleTime: 5 * 60 * 1000, // 5분간 캐시 유지 - cacheTime: 10 * 60 * 1000, // 10분간 메모리 보관 + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, }); // 웹타입 생성 diff --git a/frontend/lib/api/batch.ts b/frontend/lib/api/batch.ts index 1d56da1a..696f13f3 100644 --- a/frontend/lib/api/batch.ts +++ b/frontend/lib/api/batch.ts @@ -56,9 +56,14 @@ export interface BatchJob { job_type: string; description?: string; cron_schedule: string; + schedule_cron?: string; is_active: string; last_execution?: Date; + last_executed_at?: string; next_execution?: Date; + execution_count?: number; + success_count?: number; + failure_count?: number; status?: string; created_date?: Date; created_by?: string; @@ -390,6 +395,52 @@ export class BatchAPI { } } + /** + * 지원되는 배치 작업 타입 조회 + */ + static async getSupportedJobTypes(): Promise { + try { + const response = await apiClient.get>('/batch-management/job-types'); + if (!response.data.success) { + throw new Error(response.data.message || "작업 타입 조회에 실패했습니다."); + } + return response.data.data || []; + } catch (error) { + console.error("작업 타입 조회 오류:", error); + return []; + } + } + + /** + * 배치 작업 삭제 + */ + static async deleteBatchJob(id: number): Promise { + try { + const response = await apiClient.delete>(`/batch-management/jobs/${id}`); + if (!response.data.success) { + throw new Error(response.data.message || "배치 작업 삭제에 실패했습니다."); + } + } catch (error) { + console.error("배치 작업 삭제 오류:", error); + throw error; + } + } + + /** + * 배치 작업 실행 + */ + static async executeBatchJob(id: number): Promise { + try { + const response = await apiClient.post>(`/batch-management/jobs/${id}/execute`); + if (!response.data.success) { + throw new Error(response.data.message || "배치 작업 실행에 실패했습니다."); + } + } catch (error) { + console.error("배치 작업 실행 오류:", error); + throw error; + } + } + /** * auth_tokens 테이블의 서비스명 목록 조회 */