From 388d99d05fbf06bb94de4c45442e09e9bbf975b6 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Tue, 31 Mar 2026 15:16:14 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20UI=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81=20-=202=EB=8B=A8=20?= =?UTF-8?q?=ED=83=AD=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B9=B4=EB=93=9C=20=EB=86=92=EC=9D=B4=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 2단 탭 레이아웃 (섹션 탭 slate-900 + 서브 탭 언더라인) - 섹션 탭에서 다른 섹션으로 직접 이동 가능 - 메인화면 카드 CSS Grid 전환 (높이 자동 동기화) - h-screen 고정 + 탭/콘텐츠 영역 분리 - 중앙 정렬 (메인 섹션 + 서브 섹션) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 54 +++++---- frontend/src/components/Navbar.tsx | 183 ++++++++++++++++++----------- frontend/src/theme/base.css | 13 +- 3 files changed, 156 insertions(+), 94 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96c6356..416167e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,26 +25,40 @@ function AppLayout() { const isMainMenu = location.pathname === '/'; return ( -
-
- - }> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - -
+
+ {/* 메인 화면: 전체화면, 섹션 페이지: 탭 + 스크롤 콘텐츠 */} + {isMainMenu ? ( +
+ }> + + } /> + + +
+ ) : ( + <> +
+ +
+
+ }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + +
+ + )}
); diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index adf72d7..564edcf 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,49 +1,75 @@ -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useThemeContext } from '../contexts/ThemeContext'; -interface NavSection { - key: string; - title: string; - paths: string[]; - items: { path: string; label: string; icon: string }[]; +interface MenuItem { + id: string; + label: string; + path: string; } -const sections: NavSection[] = [ +interface MenuSection { + id: string; + label: string; + shortLabel: string; + icon: React.ReactNode; + defaultPath: string; + children: MenuItem[]; +} + +const MENU_STRUCTURE: MenuSection[] = [ { - key: 'collector', - title: 'S&P Collector', - paths: ['/dashboard', '/jobs', '/executions', '/recollects', '/schedules', '/schedule-timeline'], - items: [ - { path: '/dashboard', label: '대시보드', icon: '📊' }, - { path: '/executions', label: '실행 이력', icon: '📋' }, - { path: '/recollects', label: '재수집 이력', icon: '🔄' }, - { path: '/jobs', label: '작업', icon: '⚙️' }, - { path: '/schedules', label: '스케줄', icon: '🕐' }, - { path: '/schedule-timeline', label: '타임라인', icon: '📅' }, + id: 'collector', + label: 'S&P Collector', + shortLabel: 'Collector', + icon: ( + + + + ), + defaultPath: '/dashboard', + children: [ + { id: 'dashboard', label: '대시보드', path: '/dashboard' }, + { id: 'executions', label: '실행 이력', path: '/executions' }, + { id: 'recollects', label: '재수집 이력', path: '/recollects' }, + { id: 'jobs', label: '작업 관리', path: '/jobs' }, + { id: 'schedules', label: '스케줄', path: '/schedules' }, + { id: 'timeline', label: '타임라인', path: '/schedule-timeline' }, ], }, { - key: 'bypass', - title: 'S&P Bypass', - paths: ['/bypass-config'], - items: [ - { path: '/bypass-config', label: 'Bypass API', icon: '🔗' }, + id: 'bypass', + label: 'S&P Bypass', + shortLabel: 'Bypass', + icon: ( + + + + ), + defaultPath: '/bypass-config', + children: [ + { id: 'bypass-config', label: 'Bypass API', path: '/bypass-config' }, ], }, { - key: 'risk', - title: 'S&P Risk & Compliance', - paths: ['/screening-guide', '/risk-compliance-history'], - items: [ - { path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' }, - { path: '/risk-compliance-history', label: 'Change History', icon: '📜' }, + id: 'risk', + label: 'S&P Risk & Compliance', + shortLabel: 'Risk & Compliance', + icon: ( + + + + ), + defaultPath: '/screening-guide', + children: [ + { id: 'screening-guide', label: 'Screening Guide', path: '/screening-guide' }, + { id: 'risk-compliance-history', label: 'Change History', path: '/risk-compliance-history' }, ], }, ]; -function getCurrentSection(pathname: string): NavSection | null { - for (const section of sections) { - if (section.paths.some((p) => pathname === p || pathname.startsWith(p + '/'))) { +function getCurrentSection(pathname: string): MenuSection | null { + for (const section of MENU_STRUCTURE) { + if (section.children.some((c) => pathname === c.path || pathname.startsWith(c.path + '/'))) { return section; } } @@ -52,56 +78,75 @@ function getCurrentSection(pathname: string): NavSection | null { export default function Navbar() { const location = useLocation(); + const navigate = useNavigate(); const { theme, toggle } = useThemeContext(); const currentSection = getCurrentSection(location.pathname); - // 메인 화면에서는 Navbar 숨김 + // 메인 화면에서는 숨김 if (!currentSection) return null; - const isActive = (path: string) => { - if (path === '/dashboard') return location.pathname === '/dashboard'; + const isActivePath = (path: string) => { return location.pathname === path || location.pathname.startsWith(path + '/'); }; return ( - +
); } diff --git a/frontend/src/theme/base.css b/frontend/src/theme/base.css index 7148406..de7d284 100644 --- a/frontend/src/theme/base.css +++ b/frontend/src/theme/base.css @@ -27,16 +27,19 @@ body { /* Main Menu Cards */ .gc-cards { padding: 2rem 0; - display: flex; - justify-content: center; - align-items: stretch; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 1fr; gap: 2rem; width: 80%; margin: 0 auto; } -.gc-cards > * { - flex: 1 1 0; +@media (max-width: 768px) { + .gc-cards { + grid-template-columns: 1fr; + width: 90%; + } } .gc-card { -- 2.45.2 From 27972126755c6ba58941ab64f8b6ab9955d91e88 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Tue, 31 Mar 2026 15:18:29 +0900 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/RELEASE-NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 6817940..c9dee1d 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,12 @@ ## [Unreleased] +### 추가 +- 공통 UI 피드백 반영 (#121) + - 2단 탭 네비게이션 (섹션 탭 + 서브 탭) + - 섹션 간 직접 이동 + - 메인화면 카드 높이 동기화 (CSS Grid) + ## [2026-03-31] ### 추가 -- 2.45.2 From ba7c5af5f17d90fbac3362c709f84c8e5962f840 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Tue, 31 Mar 2026 15:52:23 +0900 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20S&P=20Collector=20=EB=8B=A4=ED=81=AC?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EB=AF=B8=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EB=94=94=EC=9E=90=EC=9D=B8=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 실행이력상세/재수집이력상세 API 호출 로그 다크모드 적용 - 개별 호출 로그 (ApiLogSection) 필터/테이블 다크모드 적용 - 작업관리 스케줄 라벨 rounded-full 및 디자인 통일 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/ApiLogSection.tsx | 22 ++++++------ frontend/src/pages/ExecutionDetail.tsx | 42 +++++++++++------------ frontend/src/pages/Jobs.tsx | 8 ++--- frontend/src/pages/RecollectDetail.tsx | 42 +++++++++++------------ 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/ApiLogSection.tsx b/frontend/src/components/ApiLogSection.tsx index 4da7cb3..7f89b6d 100644 --- a/frontend/src/components/ApiLogSection.tsx +++ b/frontend/src/components/ApiLogSection.tsx @@ -71,11 +71,11 @@ export default function ApiLogSection({ stepExecutionId, summary }: ApiLogSectio className={`px-2.5 py-1 text-xs rounded-full font-medium transition-colors ${ status === key ? key === 'ERROR' - ? 'bg-red-100 text-red-700' + ? 'bg-red-500/15 text-red-500' : key === 'SUCCESS' - ? 'bg-emerald-100 text-emerald-700' - : 'bg-blue-100 text-blue-700' - : 'bg-gray-100 text-gray-500 hover:bg-gray-200' + ? 'bg-emerald-500/15 text-emerald-500' + : 'bg-blue-500/15 text-blue-500' + : 'bg-wing-card text-wing-muted hover:bg-wing-hover' }`} > {label} ({count.toLocaleString()}) @@ -92,7 +92,7 @@ export default function ApiLogSection({ stepExecutionId, summary }: ApiLogSectio <>
- + @@ -104,24 +104,24 @@ export default function ApiLogSection({ stepExecutionId, summary }: ApiLogSectio - + {logData.content.map((log, idx) => { const isError = (log.statusCode != null && log.statusCode >= 400) || log.errorMessage; return ( - + - -
# URI에러
{page * 10 + idx + 1}
- + {log.requestUri}
{log.httpMethod}{log.httpMethod} + {log.responseTimeMs?.toLocaleString() ?? '-'} + {log.responseCount?.toLocaleString() ?? '-'} diff --git a/frontend/src/pages/ExecutionDetail.tsx b/frontend/src/pages/ExecutionDetail.tsx index f861986..68c2b5c 100644 --- a/frontend/src/pages/ExecutionDetail.tsx +++ b/frontend/src/pages/ExecutionDetail.tsx @@ -96,32 +96,32 @@ function StepCard({ step, jobName, jobExecutionId }: StepCardProps) { {/* API 호출 정보: apiLogSummary가 있으면 개별 로그 리스트, 없으면 기존 apiCallInfo 요약 */} {step.apiLogSummary ? ( -
+

API 호출 정보

-
+

{step.apiLogSummary.totalCalls.toLocaleString()}

총 호출

-
+

{step.apiLogSummary.successCount.toLocaleString()}

성공

-
+

0 ? 'text-red-500' : 'text-wing-text'}`}> {step.apiLogSummary.errorCount.toLocaleString()}

에러

-
+

{Math.round(step.apiLogSummary.avgResponseMs).toLocaleString()}

평균(ms)

-
+

{step.apiLogSummary.maxResponseMs.toLocaleString()}

최대(ms)

-
+

{step.apiLogSummary.minResponseMs.toLocaleString()}

최소(ms)

@@ -132,7 +132,7 @@ function StepCard({ step, jobName, jobExecutionId }: StepCardProps) { )}
) : step.apiCallInfo && ( -
+

API 호출 정보

@@ -163,7 +163,7 @@ function StepCard({ step, jobName, jobExecutionId }: StepCardProps) { )} {step.exitMessage && ( -
+

Exit Message

{step.exitMessage} @@ -468,7 +468,7 @@ function FailedRecordsToggle({ records, jobName, jobExecutionId }: { records: Fa }; return ( -

+