From c77cba7c5b83fafc7d3cda57e90012eb883caf2f Mon Sep 17 00:00:00 2001 From: zio Date: Sun, 12 Jul 2026 23:56:59 +0900 Subject: [PATCH] feat(web): home calendar holiday markers + unify public header into single frame - HomePage: wire fetchHolidaySet, is-holiday cell class + holiday name label - home.css: is-holiday red daynum, kx-home__cal-holiday-name, neutral-100 grid lines - PublicShell: absorb 3-track switcher into single top header (COEX 1-frame), drop dark topbar - public.css: remove .kxp-topbar styles, tracktabs bar/menu variants Co-Authored-By: Claude Opus 4.8 --- src/frontend/src/screens/home/HomePage.tsx | 14 ++ src/frontend/src/screens/home/home.css | 18 ++- .../src/screens/public/PublicShell.tsx | 135 +++++++++--------- src/frontend/src/screens/public/public.css | 114 +++++---------- 4 files changed, 133 insertions(+), 148 deletions(-) diff --git a/src/frontend/src/screens/home/HomePage.tsx b/src/frontend/src/screens/home/HomePage.tsx index 384e972..9d93cf4 100644 --- a/src/frontend/src/screens/home/HomePage.tsx +++ b/src/frontend/src/screens/home/HomePage.tsx @@ -35,6 +35,7 @@ import { type CalEvent, type CalStatus, } from './homeCalendar'; +import { fetchHolidaySet } from '../admin/holidayApi'; import { HomeCharts } from './HomeCharts'; import './home.css'; @@ -634,6 +635,14 @@ function HomeCalendar() { queryKey: ['home-hall-centers'], queryFn: () => homeApi.hallCenters(), }); + // 공휴일 세트(붉은 날짜 + 공휴일명) — 커서 연도±1 창. 실패해도 달력 정상(빈 세트). + const holidayQ = useQuery({ + queryKey: ['home-cal-holidays', cursor.getFullYear()], + queryFn: () => + fetchHolidaySet(`${cursor.getFullYear() - 1}-01-01`, `${cursor.getFullYear() + 1}-12-31`), + retry: false, + }); + const holidays = holidayQ.data ?? new Map(); const allEvents = eventsQuery.data ?? []; const filtered = useMemo( @@ -750,12 +759,17 @@ function HomeCalendar() { cell.inMonth ? '' : 'is-out', cell.isToday ? 'is-today' : '', cell.dow === 0 ? 'is-sun' : cell.dow === 6 ? 'is-sat' : '', + holidays.get(cell.key) ? 'is-holiday' : '', ] .filter(Boolean) .join(' ')} onClick={() => setPopupDay(cell.key)} + title={holidays.get(cell.key) || undefined} > {cell.day} + {holidays.get(cell.key) && ( + {holidays.get(cell.key)} + )} {wk.hiddenByCol[cell.dow] > 0 && ( {t('home.calendar.more', { n: wk.hiddenByCol[cell.dow] })} diff --git a/src/frontend/src/screens/home/home.css b/src/frontend/src/screens/home/home.css index 90fbf34..01643bf 100644 --- a/src/frontend/src/screens/home/home.css +++ b/src/frontend/src/screens/home/home.css @@ -481,7 +481,7 @@ } .kx-home__cal-week { position: relative; - border-bottom: var(--border-card); + border-bottom: 1px solid var(--color-neutral-100); } .kx-home__cal-week:last-child { border-bottom: none; @@ -492,7 +492,7 @@ } .kx-home__cal-cell { min-height: 84px; - border-right: var(--border-card); + border-right: 1px solid var(--color-neutral-100); padding: 4px 6px; text-align: left; background: var(--color-white); @@ -523,6 +523,20 @@ .kx-home__cal-cell.is-sat .kx-home__cal-daynum { color: var(--color-primary-600); } +.kx-home__cal-cell.is-holiday .kx-home__cal-daynum { + color: var(--color-error); + font-weight: var(--fw-bold); +} +.kx-home__cal-holiday-name { + font-size: var(--fs-nano); + font-weight: var(--fw-semibold); + color: var(--color-error); + line-height: 1.1; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .kx-home__cal-cell.is-today .kx-home__cal-daynum { background: var(--color-primary-600); color: #fff; diff --git a/src/frontend/src/screens/public/PublicShell.tsx b/src/frontend/src/screens/public/PublicShell.tsx index f8a8eca..ae1e0e0 100644 --- a/src/frontend/src/screens/public/PublicShell.tsx +++ b/src/frontend/src/screens/public/PublicShell.tsx @@ -1,6 +1,7 @@ /* - * 공개 홍보 사이트 공통 셸 — 헤더 GNB + 푸터. (AppShell 미사용, 비로그인 공개 영역) - * design.md §3B: 전통 페이지 내비 + SEO·다국어 토글(react-i18next LanguageSwitcher). + * 공개 홍보 사이트 공통 셸 — 단일 상단 헤더(1 프레임) + 푸터. (AppShell 미사용, 비로그인 공개 영역) + * COEX 벤치마크(docs/analysis/coex-website.md): 상단 트랙 스위처 + 유틸을 하나의 상단 바로 통합. + * design.md §3B/§3C-0: 3-트랙 스위처(role=tablist·로빙 탭인덱스)·GNB·다국어 토글을 단일 헤더로 흡수. */ import { useRef, useState, type KeyboardEvent, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -28,22 +29,20 @@ const TRACK_TABS: { key: PublicTrack; to: string; i18n: string }[] = [ ]; /** - * 상단 3-트랙 스위처 (§3C-0). role=tablist·로빙 탭인덱스·좌우/Home/End 키 이동. + * 3-트랙 탭 (§3C-0) — 단일 헤더 내부에 흡수. role=tablist·로빙 탭인덱스·좌우/Home/End 키 이동. * 관문(SCR-T0)은 track 미지정(미선택), 서브홈(T1~T3)은 해당 탭 활성(aria-current). */ -function TrackSwitcher({ track }: { track?: PublicTrack }) { +function TrackTabs({ track, variant }: { track?: PublicTrack; variant: 'bar' | 'menu' }) { const { t } = useTranslation(); - const navigate = useNavigate(); - const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const refs = useRef<(HTMLAnchorElement | null)[]>([]); - // 로빙 탭인덱스 기준 인덱스(활성 탭·없으면 첫 탭). const activeIdx = TRACK_TABS.findIndex((it) => it.key === track); const baseIdx = activeIdx >= 0 ? activeIdx : 0; function onKeyDown(e: KeyboardEvent, i: number) { let next = i; - if (e.key === 'ArrowRight') next = (i + 1) % TRACK_TABS.length; - else if (e.key === 'ArrowLeft') next = (i - 1 + TRACK_TABS.length) % TRACK_TABS.length; + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (i + 1) % TRACK_TABS.length; + else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') + next = (i - 1 + TRACK_TABS.length) % TRACK_TABS.length; else if (e.key === 'Home') next = 0; else if (e.key === 'End') next = TRACK_TABS.length - 1; else return; @@ -52,52 +51,31 @@ function TrackSwitcher({ track }: { track?: PublicTrack }) { } return ( -
-
- - {t('track.shell.wordmark')} - - -
- {TRACK_TABS.map((it, i) => { - const isActive = it.key === track; - return ( - { - refs.current[i] = el; - }} - to={it.to} - role="tab" - className={`kxp-tracktab${isActive ? ' is-active' : ''}`} - aria-selected={isActive} - aria-current={isActive ? 'page' : undefined} - tabIndex={i === baseIdx ? 0 : -1} - onKeyDown={(e) => onKeyDown(e, i)} - > - {t(it.i18n)} - - ); - })} -
- -
- - {isAuthenticated ? ( - - ) : ( - - {t('track.shell.login')} - - )} -
-
+
+ {TRACK_TABS.map((it, i) => { + const isActive = it.key === track; + return ( + { + refs.current[i] = el; + }} + to={it.to} + role="tab" + className={`kxp-tracktab${isActive ? ' is-active' : ''}`} + aria-selected={isActive} + aria-current={isActive ? 'page' : undefined} + tabIndex={i === baseIdx ? 0 : -1} + onKeyDown={(e) => onKeyDown(e, i)} + > + {t(it.i18n)} + + ); + })}
); } @@ -125,6 +103,8 @@ export function PublicShell({ brand = 'KINTEX', }: PublicShellProps) { const { t } = useTranslation(); + const navigate = useNavigate(); + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const [menuOpen, setMenuOpen] = useState(false); const ctaLabel = cta ?? t('shell.cta.register'); // §3C-V 비주얼·모션 레이어 — 공개 트랙(.kxp) 하위에만 스코프 주입. @@ -133,13 +113,17 @@ export function PublicShell({ return (
- + {/* ── 단일 상단 헤더 (1 프레임 · COEX 통합 top bar) ── */}
+ +
-
-
{search && (
@@ -169,9 +151,19 @@ export function PublicShell({ - - {t('shell.login')} - + {isAuthenticated ? ( + + ) : ( + + {t('shell.login')} + + )} {ctaLabel} @@ -191,14 +183,25 @@ export function PublicShell({ {menuOpen && (