feat: 배치 관리 페이지 및 API 개선

- 배치 작업 타입 조회 API 추가 및 오류 처리 개선
- 배치 관리 페이지에서 작업 타입을 객체 형태로 변환하여 설정
- 성공률 계산 로직에서 null 체크 추가
- 누적 실행 횟수, 총 성공 횟수, 총 실패 횟수 계산 시 null 체크 추가

Made-with: Cursor
This commit is contained in:
DDD1542 2026-03-10 03:09:28 +09:00
parent c120492378
commit b6ec4a4904
4 changed files with 147 additions and 11 deletions

View File

@ -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() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{jobs.reduce((sum, job) => sum + job.execution_count, 0)}
{jobs.reduce((sum, job) => sum + (job.execution_count ?? 0), 0)}
</div>
<p className="text-xs text-muted-foreground"> </p>
</CardContent>
@ -373,7 +373,7 @@ export default function BatchManagementPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-success">
{jobs.reduce((sum, job) => sum + job.success_count, 0)}
{jobs.reduce((sum, job) => sum + (job.success_count ?? 0), 0)}
</div>
<p className="text-xs text-muted-foreground"> </p>
</CardContent>
@ -385,7 +385,7 @@ export default function BatchManagementPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-destructive">
{jobs.reduce((sum, job) => sum + job.failure_count, 0)}
{jobs.reduce((sum, job) => sum + (job.failure_count ?? 0), 0)}
</div>
<p className="text-xs text-muted-foreground"> </p>
</CardContent>

View File

@ -209,16 +209,101 @@ function FullWidthOverlayRow({
);
}
/**
* 데스크톱: 원본 (transform: scale )
*
*/
function DesktopCanvasRenderer({
components,
canvasWidth,
canvasHeight,
renderComponent,
}: ResponsiveGridRendererProps) {
const containerRef = useRef<HTMLDivElement>(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 (
<div
ref={containerRef}
data-screen-runtime="true"
className="bg-background relative w-full overflow-x-hidden"
style={{ minHeight: `${scaledHeight}px` }}
>
<div
style={{
width: `${canvasWidth}px`,
height: `${canvasHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
position: "relative",
}}
>
{topLevel.map((component) => {
const typeId = getComponentTypeId(component);
return (
<div
key={component.id}
data-component-id={component.id}
data-component-type={typeId}
style={{
position: "absolute",
left: `${component.position.x}px`,
top: `${component.position.y}px`,
width: component.size?.width ? `${component.size.width}px` : "auto",
height: component.size?.height ? `${component.size.height}px` : "auto",
zIndex: component.position.z || 1,
}}
>
{renderComponent(component)}
</div>
);
})}
</div>
</div>
);
}
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 (
<DesktopCanvasRenderer
components={components}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
renderComponent={renderComponent}
/>
);
}
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 (
<div

View File

@ -88,8 +88,8 @@ export const useWebTypes = (params?: WebTypeQueryParams) => {
return result.data || [];
},
staleTime: 5 * 60 * 1000, // 5분간 캐시 유지
cacheTime: 10 * 60 * 1000, // 10분간 메모리 보관
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});
// 웹타입 생성

View File

@ -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<string[]> {
try {
const response = await apiClient.get<ApiResponse<string[]>>('/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<void> {
try {
const response = await apiClient.delete<ApiResponse<void>>(`/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<void> {
try {
const response = await apiClient.post<ApiResponse<void>>(`/batch-management/jobs/${id}/execute`);
if (!response.data.success) {
throw new Error(response.data.message || "배치 작업 실행에 실패했습니다.");
}
} catch (error) {
console.error("배치 작업 실행 오류:", error);
throw error;
}
}
/**
* auth_tokens
*/