From 033daff378226ee2d42575eabbb5728f6d2f7f20 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 13 Mar 2026 16:02:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=EA=B0=81=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EB=B3=84=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GuideModal 컴포넌트 신규 생성 (아코디언 방식 가이드 모달 + HelpButton) - 8개 페이지에 (?) 도움말 버튼 및 화면별 사용자 가이드 추가 - 대시보드, 작업 목록, 실행 이력, 실행 상세 - 재수집 이력, 재수집 상세, 스케줄 관리, 타임라인 Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/GuideModal.tsx | 92 ++++++++++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 40 ++++++++++- frontend/src/pages/ExecutionDetail.tsx | 41 +++++++++++- frontend/src/pages/Executions.tsx | 38 ++++++++++- frontend/src/pages/Jobs.tsx | 34 +++++++++- frontend/src/pages/RecollectDetail.tsx | 45 ++++++++++++- frontend/src/pages/Recollects.tsx | 35 +++++++++- frontend/src/pages/Schedules.tsx | 48 +++++++++++--- frontend/src/pages/Timeline.tsx | 32 +++++++++ 9 files changed, 387 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/GuideModal.tsx diff --git a/frontend/src/components/GuideModal.tsx b/frontend/src/components/GuideModal.tsx new file mode 100644 index 0000000..0c4ee2e --- /dev/null +++ b/frontend/src/components/GuideModal.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; + +interface GuideSection { + title: string; + content: string; +} + +interface Props { + open: boolean; + pageTitle: string; + sections: GuideSection[]; + onClose: () => void; +} + +export default function GuideModal({ open, pageTitle, sections, onClose }: Props) { + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +
+

{pageTitle} 사용 가이드

+ +
+ +
+ {sections.map((section, i) => ( + + ))} +
+ +
+ +
+
+
+ ); +} + +function GuideAccordion({ title, content, defaultOpen }: { title: string; content: string; defaultOpen: boolean }) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ + {isOpen && ( +
+ {content} +
+ )} +
+ ); +} + +export function HelpButton({ onClick }: { onClick: () => void }) { + return ( + + ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index cc8475a..0f5b5b1 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -15,9 +15,37 @@ import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; import BarChart from '../components/BarChart'; import { formatDateTime, calculateDuration } from '../utils/formatters'; +import GuideModal, { HelpButton } from '../components/GuideModal'; const POLLING_INTERVAL = 5000; +const DASHBOARD_GUIDE = [ + { + title: '통계 카드', + content: '화면 상단에 전체 스케줄, 활성/비활성 스케줄 수, 전체 작업 수, 최근 24시간 실패 건수를 한눈에 보여줍니다.', + }, + { + title: '실행 중인 작업', + content: '현재 실행 중인 배치 작업 목록을 실시간으로 보여줍니다.\n5초마다 자동으로 갱신됩니다.\n오래 실행 중인 작업이 있으면 상단에 경고 배너가 표시되며, "전체 강제 종료" 버튼으로 일괄 중지할 수 있습니다.', + }, + { + title: '최근 실행 이력', + content: '최근 완료된 배치 작업 5건을 보여줍니다.\n각 행의 작업명, 상태, 시작 시간, 소요 시간을 확인할 수 있습니다.\n"전체 보기"를 클릭하면 실행 이력 화면으로 이동합니다.', + }, + { + title: '최근 실패 이력', + content: '최근 24시간 내 실패한 작업이 있을 때만 표시됩니다.\n실패 원인을 빠르게 파악할 수 있도록 종료 코드와 메시지를 함께 보여줍니다.', + }, + { + title: '재수집 현황', + content: '마지막 수집 완료일시를 API별로 보여줍니다.\n최근 5건의 재수집 이력도 함께 확인할 수 있습니다.', + }, + { + title: '실행 통계 차트', + content: '최근 30일간의 배치 실행 통계를 바 차트로 보여줍니다.\n초록색은 성공, 빨간색은 실패, 회색은 기타 상태를 나타냅니다.', + }, +]; + interface StatCardProps { label: string; value: number; @@ -46,6 +74,7 @@ export default function Dashboard() { const { showToast } = useToastContext(); const [dashboard, setDashboard] = useState(null); const [loading, setLoading] = useState(true); + const [guideOpen, setGuideOpen] = useState(false); const [abandoning, setAbandoning] = useState(false); const [statistics, setStatistics] = useState(null); @@ -142,7 +171,10 @@ export default function Dashboard() {
{/* Header */}
-

대시보드

+
+

대시보드

+ setGuideOpen(true)} /> +
{/* F1: Stale Execution Warning Banner */} @@ -452,6 +484,12 @@ export default function Dashboard() { )} + setGuideOpen(false)} + />
); } diff --git a/frontend/src/pages/ExecutionDetail.tsx b/frontend/src/pages/ExecutionDetail.tsx index 33cf33c..f2c7707 100644 --- a/frontend/src/pages/ExecutionDetail.tsx +++ b/frontend/src/pages/ExecutionDetail.tsx @@ -10,9 +10,33 @@ import Pagination from '../components/Pagination'; import DetailStatCard from '../components/DetailStatCard'; import ApiLogSection from '../components/ApiLogSection'; import InfoItem from '../components/InfoItem'; +import GuideModal, { HelpButton } from '../components/GuideModal'; const POLLING_INTERVAL_MS = 5000; +const EXECUTION_DETAIL_GUIDE = [ + { + title: '실행 기본 정보', + content: '실행의 시작/종료 시간, 소요 시간, 종료 코드, 에러 메시지 등 기본 정보를 보여줍니다.\n실행 중인 경우 5초마다 자동으로 갱신됩니다.', + }, + { + title: '처리 통계', + content: '4개의 통계 카드로 전체 처리 현황을 요약합니다.\n• 읽기(Read): 외부 API에서 조회한 건수\n• 쓰기(Write): DB에 저장된 건수\n• 건너뜀(Skip): 처리하지 않은 건수\n• 필터(Filter): 조건에 의해 제외된 건수', + }, + { + title: 'Step 실행 정보', + content: '배치 작업은 하나 이상의 Step으로 구성됩니다.\n각 Step의 상태, 처리 건수, 커밋/롤백 횟수를 확인할 수 있습니다.\nAPI 호출 정보에서는 총 호출 수, 성공/에러 수, 평균 응답 시간을 보여줍니다.', + }, + { + title: 'API 호출 로그', + content: '각 Step에서 호출한 외부 API의 상세 로그를 확인할 수 있습니다.\n요청 URL, 응답 코드, 응답 시간 등을 페이지 단위로 조회합니다.', + }, + { + title: '실패 건 관리', + content: '처리 중 실패한 레코드가 있으면 목록으로 표시됩니다.\n• 실패 건 재수집: 실패한 데이터를 다시 수집합니다\n• 일괄 RESOLVED: 모든 실패 건을 해결됨으로 처리합니다\n• 재시도 초기화: 재시도 횟수를 초기화하여 자동 재수집 대상에 포함시킵니다', + }, +]; + interface StepCardProps { step: StepExecutionDto; jobName: string; @@ -161,6 +185,7 @@ export default function ExecutionDetail() { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [guideOpen, setGuideOpen] = useState(false); const isRunning = detail ? detail.status === 'STARTED' || detail.status === 'STARTING' @@ -227,9 +252,12 @@ export default function ExecutionDetail() {
-

- 실행 #{detail.executionId} -

+
+

+ 실행 #{detail.executionId} +

+ setGuideOpen(true)} /> +

{detail.jobName}

@@ -341,6 +369,13 @@ export default function ExecutionDetail() {
)}
+ + setGuideOpen(false)} + pageTitle="실행 상세" + sections={EXECUTION_DETAIL_GUIDE} + />
); } diff --git a/frontend/src/pages/Executions.tsx b/frontend/src/pages/Executions.tsx index e3b45de..0e77bf8 100644 --- a/frontend/src/pages/Executions.tsx +++ b/frontend/src/pages/Executions.tsx @@ -9,6 +9,7 @@ import ConfirmModal from '../components/ConfirmModal'; import InfoModal from '../components/InfoModal'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import GuideModal, { HelpButton } from '../components/GuideModal'; type StatusFilter = 'ALL' | 'COMPLETED' | 'FAILED' | 'STARTED' | 'STOPPED'; @@ -24,6 +25,29 @@ const POLLING_INTERVAL_MS = 5000; const RECENT_LIMIT = 50; const PAGE_SIZE = 50; +const EXECUTIONS_GUIDE = [ + { + title: '작업 필터', + content: '상단의 드롭다운에서 조회할 작업을 선택할 수 있습니다.\n여러 작업을 동시에 선택할 수 있으며, 단축 버튼으로 빠르게 필터링할 수 있습니다.\n• 전체: 모든 작업 표시\n• AIS 제외: AIS 관련 작업을 제외하고 표시\n• AIS만: AIS 관련 작업만 표시', + }, + { + title: '상태 필터', + content: '완료 / 실패 / 실행중 / 중지됨 버튼으로 상태별 필터링이 가능합니다.\n"전체"를 선택하면 모든 상태의 실행 이력을 볼 수 있습니다.', + }, + { + title: '날짜 검색', + content: '시작일과 종료일을 지정하여 특정 기간의 실행 이력을 조회할 수 있습니다.\n"검색" 버튼을 클릭하면 조건에 맞는 결과가 표시됩니다.\n"초기화" 버튼으로 검색 조건을 제거하고 최신 이력으로 돌아갑니다.', + }, + { + title: '실행 중인 작업 제어', + content: '실행 중인 작업의 행에서 "중지" 또는 "강제 종료" 버튼을 사용할 수 있습니다.\n• 중지: 현재 Step 완료 후 안전하게 종료\n• 강제 종료: 즉시 중단 (데이터 정합성 주의)', + }, + { + title: '실패 로그 확인', + content: '상태가 "FAILED"인 행을 클릭하면 실패 상세 정보를 확인할 수 있습니다.\n종료 코드(Exit Code)와 에러 메시지로 실패 원인을 파악하세요.\n상태가 "COMPLETED"이지만 실패 건수가 있으면 경고 아이콘이 표시됩니다.', + }, +]; + export default function Executions() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -53,6 +77,8 @@ export default function Executions() { // F9: 실패 로그 뷰어 const [failLogTarget, setFailLogTarget] = useState(null); + const [guideOpen, setGuideOpen] = useState(false); + const { showToast } = useToastContext(); useEffect(() => { @@ -243,7 +269,10 @@ export default function Executions() {
{/* 헤더 */}
-

실행 이력

+
+

실행 이력

+ setGuideOpen(true)} /> +

배치 작업 실행 이력을 조회하고 관리합니다.

@@ -610,6 +639,13 @@ export default function Executions() { onCancel={() => setAbandonTarget(null)} /> + setGuideOpen(false)} + pageTitle="실행 이력" + sections={EXECUTIONS_GUIDE} + /> + {/* F9: 실패 로그 뷰어 모달 */} ('recent'); const [viewMode, setViewMode] = useState('table'); + const [guideOpen, setGuideOpen] = useState(false); + // Execute modal (individual card) const [executeModalOpen, setExecuteModalOpen] = useState(false); const [targetJob, setTargetJob] = useState(''); @@ -162,7 +184,10 @@ export default function Jobs() {
{/* Header */}
-

배치 작업 목록

+
+

배치 작업 목록

+ setGuideOpen(true)} /> +
총 {jobs.length}개 작업 @@ -489,6 +514,13 @@ export default function Jobs() {
)} + setGuideOpen(false)} + /> + {/* Execute Modal (custom with date params) */} {executeModalOpen && (
(); const navigate = useNavigate(); @@ -138,6 +166,7 @@ export default function RecollectDetail() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [displayNames, setDisplayNames] = useState([]); + const [guideOpen, setGuideOpen] = useState(false); useEffect(() => { batchApi.getDisplayNames().then(setDisplayNames).catch(() => {}); @@ -214,9 +243,12 @@ export default function RecollectDetail() {
-

- 재수집 #{history.historyId} -

+
+

+ 재수집 #{history.historyId} +

+ setGuideOpen(true)} /> +

{displayNameMap[history.apiKey] || history.apiKeyName || history.apiKey} · {history.jobName}

@@ -407,6 +439,13 @@ export default function RecollectDetail() {
)}
+ + setGuideOpen(false)} + />
); } diff --git a/frontend/src/pages/Recollects.tsx b/frontend/src/pages/Recollects.tsx index 16ba5db..c700f27 100644 --- a/frontend/src/pages/Recollects.tsx +++ b/frontend/src/pages/Recollects.tsx @@ -15,6 +15,7 @@ import StatusBadge from '../components/StatusBadge'; import InfoModal from '../components/InfoModal'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import GuideModal, { HelpButton } from '../components/GuideModal'; type StatusFilter = 'ALL' | 'COMPLETED' | 'FAILED' | 'STARTED'; @@ -147,6 +148,28 @@ export default function Recollects() { // 실패 로그 모달 const [failLogTarget, setFailLogTarget] = useState(null); + // 가이드 모달 + const [guideOpen, setGuideOpen] = useState(false); + +const RECOLLECTS_GUIDE = [ + { + title: '재수집이란?', + content: '재수집은 특정 기간의 데이터를 다시 수집하는 기능입니다.\n수집 누락이나 데이터 오류가 발생했을 때 사용합니다.\n자동 재수집은 시스템이 실패 건을 자동으로 재시도하며, 수동 재수집은 사용자가 직접 요청합니다.', + }, + { + title: '마지막 수집 완료 일시', + content: '각 API별 마지막 수집 완료 일시를 보여줍니다.\n이 정보를 참고하여 재수집이 필요한 기간을 판단할 수 있습니다.', + }, + { + title: '재수집 기간 관리', + content: '1. "재수집 기간 관리" 영역에서 수집할 작업을 선택합니다\n2. 수집 기간(시작~종료)을 설정합니다\n3. 재수집 사유를 입력합니다 (선택)\n4. "재수집 요청" 버튼을 클릭합니다\n\n기간 설정 시 기존 수집 기간과 중복되면 경고가 표시됩니다.', + }, + { + title: '이력 조회', + content: '작업 선택, 상태 필터, 날짜 범위로 재수집 이력을 검색할 수 있습니다.\n상태: 완료(COMPLETED) / 실패(FAILED) / 실행중(STARTED)\n각 행을 클릭하면 상세 화면으로 이동합니다.', + }, +]; + // 수집 기간 관리 패널 const [periodPanelOpen, setPeriodPanelOpen] = useState(false); const [selectedPeriodKey, setSelectedPeriodKey] = useState(''); @@ -405,7 +428,10 @@ export default function Recollects() {
{/* 헤더 */}
-

재수집 이력

+
+

재수집 이력

+ setGuideOpen(true)} /> +

배치 재수집 실행 이력을 조회하고 관리합니다.

@@ -1035,6 +1061,13 @@ export default function Recollects() {
)} + + setGuideOpen(false)} + />
); } diff --git a/frontend/src/pages/Schedules.tsx b/frontend/src/pages/Schedules.tsx index 83e1b0d..8366301 100644 --- a/frontend/src/pages/Schedules.tsx +++ b/frontend/src/pages/Schedules.tsx @@ -6,6 +6,7 @@ import ConfirmModal from '../components/ConfirmModal'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; import { getNextExecutions } from '../utils/cronPreview'; +import GuideModal, { HelpButton } from '../components/GuideModal'; type ScheduleMode = 'new' | 'existing'; type ScheduleViewMode = 'card' | 'table'; @@ -79,9 +80,31 @@ function getTriggerStateStyle(state: string | null): string { } } +const SCHEDULES_GUIDE = [ + { + title: '스케줄이란?', + content: '스케줄은 배치 작업을 자동으로 실행하는 설정입니다.\nCron 표현식으로 실행 주기를 지정하면 해당 시간에 자동 실행됩니다.\n활성화된 스케줄만 자동 실행되며, 비활성화하면 일시 중지됩니다.', + }, + { + title: '스케줄 등록/수정', + content: '"+ 새 스케줄" 버튼 또는 기존 스케줄의 "편집" 버튼을 클릭하면 설정 팝업이 열립니다.\n1. 작업 선택: 자동 실행할 배치 작업을 선택합니다\n2. Cron 표현식: 실행 주기를 설정합니다 (프리셋 버튼으로 간편 설정 가능)\n3. 설명: 스케줄에 대한 메모를 입력합니다 (선택)\n\n"다음 5회 실행 예정" 미리보기로 설정이 올바른지 확인하세요.', + }, + { + title: 'Cron 표현식', + content: 'Cron 표현식은 "초 분 시 일 월 요일" 6자리로 구성됩니다.\n예시:\n• 0 0/15 * * * ? → 매 15분마다\n• 0 0 0 * * ? → 매일 자정\n• 0 0 12 * * ? → 매일 정오\n• 0 0 0 ? * MON → 매주 월요일 자정\n\n프리셋 버튼을 활용하면 직접 입력하지 않아도 됩니다.', + }, + { + title: '스케줄 관리', + content: '• 편집: 스케줄 설정(Cron, 설명)을 수정합니다\n• 활성화/비활성화: 자동 실행을 켜거나 끕니다\n• 삭제: 스케줄을 완전히 제거합니다\n\n상태 표시:\n• 활성 (초록): 정상 동작 중\n• 비활성 (회색): 일시 중지 상태\n• NORMAL: 트리거 정상\n• PAUSED: 트리거 일시 중지\n• BLOCKED: 이전 실행이 아직 진행 중\n• ERROR: 트리거 오류 발생', + }, +]; + export default function Schedules() { const { showToast } = useToastContext(); + // Guide modal state + const [guideOpen, setGuideOpen] = useState(false); + // Form state const [jobs, setJobs] = useState([]); const [selectedJob, setSelectedJob] = useState(''); @@ -389,14 +412,17 @@ export default function Schedules() { {/* Schedule List */}
-

- 등록된 스케줄 - {schedules.length > 0 && ( - - ({schedules.length}개) - - )} -

+
+

+ 등록된 스케줄 + {schedules.length > 0 && ( + + ({schedules.length}개) + + )} +

+ setGuideOpen(true)} /> +
); } diff --git a/frontend/src/pages/Timeline.tsx b/frontend/src/pages/Timeline.tsx index 2ce2e21..d0b95c6 100644 --- a/frontend/src/pages/Timeline.tsx +++ b/frontend/src/pages/Timeline.tsx @@ -8,6 +8,7 @@ import { getStatusColor } from '../components/StatusBadge'; import StatusBadge from '../components/StatusBadge'; import LoadingSpinner from '../components/LoadingSpinner'; import EmptyState from '../components/EmptyState'; +import GuideModal, { HelpButton } from '../components/GuideModal'; type ViewType = 'day' | 'week' | 'month'; @@ -70,9 +71,31 @@ function isRunning(status: string): boolean { return status === 'STARTED' || status === 'STARTING'; } +const TIMELINE_GUIDE = [ + { + title: '타임라인이란?', + content: '타임라인은 배치 작업의 실행 스케줄과 결과를 시각적으로 보여주는 화면입니다.\n세로축은 작업 목록, 가로축은 시간대를 나타냅니다.\n각 셀의 색상으로 실행 상태를 한눈에 파악할 수 있습니다.', + }, + { + title: '보기 모드', + content: '3가지 보기 모드를 제공합니다.\n• Day: 하루 단위 (시간대별 상세 보기)\n• Week: 일주일 단위\n• Month: 한 달 단위\n\n이전/다음 버튼으로 기간을 이동하고, "오늘" 버튼으로 현재 날짜로 돌아옵니다.', + }, + { + title: '색상 범례', + content: '각 셀의 색상은 실행 상태를 나타냅니다.\n• 초록색: 완료 (성공적으로 실행됨)\n• 빨간색: 실패 (오류 발생)\n• 파란색: 실행 중 (현재 진행 중)\n• 보라색: 예정 (아직 실행 전)\n• 회색: 없음 (해당 시간대에 실행 기록 없음)', + }, + { + title: '상세 보기', + content: '셀 위에 마우스를 올리면 툴팁으로 작업명, 기간, 상태 등 요약 정보를 보여줍니다.\n셀을 클릭하면 하단에 상세 패널이 열리며, 해당 시간대의 실행 이력 목록을 확인할 수 있습니다.\n"상세" 링크를 클릭하면 실행 상세 화면으로 이동합니다.', + }, +]; + export default function Timeline() { const { showToast } = useToastContext(); + // Guide modal state + const [guideOpen, setGuideOpen] = useState(false); + const [view, setView] = useState('day'); const [currentDate, setCurrentDate] = useState(() => new Date()); const [periodLabel, setPeriodLabel] = useState(''); @@ -254,6 +277,9 @@ export default function Timeline() { > 새로고침 + + {/* Help */} + setGuideOpen(true)} />
@@ -476,6 +502,12 @@ export default function Timeline() { )}
)} + setGuideOpen(false)} + pageTitle="타임라인" + sections={TIMELINE_GUIDE} + />
); }