('table');
// Execute modal (individual card)
const [executeModalOpen, setExecuteModalOpen] = useState(false);
@@ -243,10 +243,23 @@ export default function Jobs() {
{/* View mode toggle */}
+
setViewMode('table')}
+ title="테이블 보기"
+ className={`px-3 py-2 transition-colors ${
+ viewMode === 'table'
+ ? 'bg-wing-accent text-white'
+ : 'bg-wing-surface text-wing-muted hover:text-wing-text'
+ }`}
+ >
+
+
+
+
setViewMode('card')}
title="카드 보기"
- className={`px-3 py-2 transition-colors ${
+ className={`px-3 py-2 transition-colors border-l border-wing-border ${
viewMode === 'card'
? 'bg-wing-accent text-white'
: 'bg-wing-surface text-wing-muted hover:text-wing-text'
@@ -261,19 +274,6 @@ export default function Jobs() {
/>
-
setViewMode('table')}
- title="테이블 보기"
- className={`px-3 py-2 transition-colors border-l border-wing-border ${
- viewMode === 'table'
- ? 'bg-wing-accent text-white'
- : 'bg-wing-surface text-wing-muted hover:text-wing-text'
- }`}
- >
-
-
-
-
@@ -316,9 +316,6 @@ export default function Jobs() {
{getJobLabel(job)}
- {job.displayName && (
- {job.jobName}
- )}
{isRunning && (
@@ -436,12 +433,7 @@ export default function Jobs() {
{isRunning && (
)}
-
-
{getJobLabel(job)}
- {job.displayName && (
-
{job.jobName}
- )}
-
+
{getJobLabel(job)}
diff --git a/frontend/src/pages/Recollects.tsx b/frontend/src/pages/Recollects.tsx
index 7eb4e3b..16ba5db 100644
--- a/frontend/src/pages/Recollects.tsx
+++ b/frontend/src/pages/Recollects.tsx
@@ -477,7 +477,7 @@ export default function Recollects() {
- API명
+ 배치 작업명
마지막 수집 완료일시
경과시간
상태
diff --git a/frontend/src/pages/Schedules.tsx b/frontend/src/pages/Schedules.tsx
index 3993886..83e1b0d 100644
--- a/frontend/src/pages/Schedules.tsx
+++ b/frontend/src/pages/Schedules.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import { useState, useEffect, useCallback, useMemo } from 'react';
import { batchApi, type ScheduleResponse, type JobDisplayName } from '../api/batchApi';
import { formatDateTime } from '../utils/formatters';
import { useToastContext } from '../contexts/ToastContext';
@@ -8,6 +8,7 @@ import LoadingSpinner from '../components/LoadingSpinner';
import { getNextExecutions } from '../utils/cronPreview';
type ScheduleMode = 'new' | 'existing';
+type ScheduleViewMode = 'card' | 'table';
interface ConfirmAction {
type: 'toggle' | 'delete';
@@ -95,11 +96,14 @@ export default function Schedules() {
const [listLoading, setListLoading] = useState(true);
const [displayNames, setDisplayNames] = useState([]);
+ // View mode state
+ const [viewMode, setViewMode] = useState('table');
+
// Confirm modal state
const [confirmAction, setConfirmAction] = useState(null);
- // 폼 영역 ref (편집 버튼 클릭 시 스크롤)
- const formRef = useRef(null);
+ // Form modal state
+ const [formOpen, setFormOpen] = useState(false);
const loadSchedules = useCallback(async () => {
try {
@@ -186,8 +190,8 @@ export default function Schedules() {
showToast('스케줄이 등록되었습니다', 'success');
}
await loadSchedules();
- // Reload schedule info for current job
- await handleJobSelect(selectedJob);
+ setFormOpen(false);
+ resetForm();
} catch (err) {
const message = err instanceof Error ? err.message : '저장 실패';
showToast(message, 'error');
@@ -216,12 +220,10 @@ export default function Schedules() {
await batchApi.deleteSchedule(schedule.jobName);
showToast(`${schedule.jobName} 스케줄이 삭제되었습니다`, 'success');
await loadSchedules();
- // Clear form if deleted schedule was selected
+ // Close form if deleted schedule was being edited
if (selectedJob === schedule.jobName) {
- setSelectedJob('');
- setCronExpression('');
- setDescription('');
- setScheduleMode('new');
+ resetForm();
+ setFormOpen(false);
}
} catch (err) {
const message = err instanceof Error ? err.message : '삭제 실패';
@@ -230,124 +232,159 @@ export default function Schedules() {
setConfirmAction(null);
};
+ const resetForm = () => {
+ setSelectedJob('');
+ setCronExpression('');
+ setDescription('');
+ setScheduleMode('new');
+ };
+
const handleEditFromCard = (schedule: ScheduleResponse) => {
setSelectedJob(schedule.jobName);
setCronExpression(schedule.cronExpression);
setDescription(schedule.description ?? '');
setScheduleMode('existing');
- formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ setFormOpen(true);
+ };
+
+ const handleNewSchedule = () => {
+ resetForm();
+ setFormOpen(true);
};
return (
- {/* Form Section */}
-
-
스케줄 등록 / 수정
-
-
- {/* Job Select */}
-
-
- 작업 선택
-
-
-
handleJobSelect(e.target.value)}
- className="flex-1 rounded-lg border border-wing-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
- disabled={formLoading}
+ {/* Form Modal */}
+ {formOpen && (
+
+
setFormOpen(false)} />
+
+
+
+ {scheduleMode === 'existing' ? '스케줄 수정' : '스케줄 등록'}
+
+
setFormOpen(false)}
+ className="p-1 text-wing-muted hover:text-wing-text transition-colors"
>
- -- 작업을 선택하세요 --
- {jobs.map((job) => (
-
- {displayNameMap[job] || job}
-
- ))}
-
- {selectedJob && (
-
- {scheduleMode === 'existing' ? '기존 스케줄' : '새 스케줄'}
-
- )}
- {formLoading && (
-
- )}
+
+
+
+
-
- {/* Cron Expression */}
-
-
- Cron 표현식
-
- setCronExpression(e.target.value)}
- placeholder="0 0/15 * * * ?"
- className="w-full rounded-lg border border-wing-border px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
- disabled={!selectedJob || formLoading}
- />
-
+
+ {/* Job Select */}
+
+
+ 작업 선택
+
+
+
handleJobSelect(e.target.value)}
+ className="flex-1 rounded-lg border border-wing-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
+ disabled={formLoading || scheduleMode === 'existing'}
+ >
+ -- 작업을 선택하세요 --
+ {jobs.map((job) => (
+
+ {displayNameMap[job] || job}
+
+ ))}
+
+ {selectedJob && (
+
+ {scheduleMode === 'existing' ? '기존 스케줄' : '새 스케줄'}
+
+ )}
+ {formLoading && (
+
+ )}
+
+
- {/* Cron Presets */}
-
-
- 프리셋
-
-
- {CRON_PRESETS.map(({ label, cron }) => (
-
setCronExpression(cron)}
+ {/* Cron Expression */}
+
+
+ Cron 표현식
+
+ setCronExpression(e.target.value)}
+ placeholder="0 0/15 * * * ?"
+ className="w-full rounded-lg border border-wing-border px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
disabled={!selectedJob || formLoading}
- className="px-3 py-1 text-xs font-medium bg-wing-card text-wing-text rounded-lg hover:bg-wing-accent/15 hover:text-wing-accent transition-colors disabled:opacity-50"
- >
- {label}
-
- ))}
+ />
+
+
+ {/* Cron Presets */}
+
+
+ 프리셋
+
+
+ {CRON_PRESETS.map(({ label, cron }) => (
+ setCronExpression(cron)}
+ disabled={!selectedJob || formLoading}
+ className="px-3 py-1 text-xs font-medium bg-wing-card text-wing-text rounded-lg hover:bg-wing-accent/15 hover:text-wing-accent transition-colors disabled:opacity-50"
+ >
+ {label}
+
+ ))}
+
+
+
+ {/* Cron Preview */}
+ {cronExpression.trim() && (
+
+ )}
+
+ {/* Description */}
+
+
+ 설명
+
+ setDescription(e.target.value)}
+ placeholder="스케줄 설명 (선택)"
+ className="w-full rounded-lg border border-wing-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
+ disabled={!selectedJob || formLoading}
+ />
+
+
+
+ {/* Modal Footer */}
+
+ setFormOpen(false)}
+ className="px-4 py-2 text-sm font-medium text-wing-muted bg-wing-card rounded-lg hover:bg-wing-hover transition-colors"
+ >
+ 취소
+
+
+ {saving ? '저장 중...' : '저장'}
+
-
- {/* Cron Preview */}
- {cronExpression.trim() && (
-
- )}
-
- {/* Description */}
-
-
- 설명
-
- setDescription(e.target.value)}
- placeholder="스케줄 설명 (선택)"
- className="w-full rounded-lg border border-wing-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-wing-accent focus:border-wing-accent"
- disabled={!selectedJob || formLoading}
- />
-
-
- {/* Save Button */}
-
-
- {saving ? '저장 중...' : '저장'}
-
-
-
+ )}
{/* Schedule List */}
@@ -360,19 +397,47 @@ export default function Schedules() {
)}
-
- 새로고침
-
+
+
+ + 새 스케줄
+
+
+
setViewMode('table')}
+ className={`p-1.5 ${viewMode === 'table' ? 'bg-wing-accent text-white' : 'bg-wing-surface text-wing-muted hover:bg-wing-hover'}`}
+ title="테이블 보기"
+ >
+
+
+
+
+
setViewMode('card')}
+ className={`p-1.5 ${viewMode === 'card' ? 'bg-wing-accent text-white' : 'bg-wing-surface text-wing-muted hover:bg-wing-hover'}`}
+ title="카드 보기"
+ >
+
+
+
+
+
+
+ 새로고침
+
+
{listLoading ? (
) : schedules.length === 0 ? (
-
- ) : (
+
+ ) : viewMode === 'card' ? (
{schedules.map((schedule) => (
))}
+ ) : (
+
+
+
+
+ 작업명
+ Cron 표현식
+ 상태
+ 다음 실행
+ 이전 실행
+ 액션
+
+
+
+ {schedules.map((schedule) => (
+
+
+
+ {displayNameMap[schedule.jobName] || schedule.jobName}
+
+ {schedule.description && (
+ {schedule.description}
+ )}
+
+
+ {schedule.cronExpression}
+
+
+
+ {schedule.active ? '활성' : '비활성'}
+
+
+ {formatDateTime(schedule.nextFireTime)}
+ {schedule.previousFireTime ? formatDateTime(schedule.previousFireTime) : '-'}
+
+
+ handleEditFromCard(schedule)}
+ className="px-2 py-1 text-xs font-medium text-wing-accent bg-wing-accent/10 rounded hover:bg-wing-accent/20 transition-colors"
+ >
+ 편집
+
+ setConfirmAction({ type: 'toggle', schedule })}
+ className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
+ schedule.active
+ ? 'text-amber-600 bg-amber-50 hover:bg-amber-100'
+ : 'text-emerald-600 bg-emerald-50 hover:bg-emerald-100'
+ }`}
+ >
+ {schedule.active ? '비활성화' : '활성화'}
+
+ setConfirmAction({ type: 'delete', schedule })}
+ className="px-2 py-1 text-xs font-medium text-red-600 bg-red-50 rounded hover:bg-red-100 transition-colors"
+ >
+ 삭제
+
+
+
+
+ ))}
+
+
+
)}