feat(schedule,dashboard): remove aside mini-calendar (compact month nav), hero inner scroll wrapper
- schedule: MiniCalendar removed per owner; AsideMonthNav keeps month cursor for hall widgets - dashboard hero: overflow moved to .kx-timeline-scroll wrapper so the section never shows scrollbars Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
aab5e8836e
commit
a9826df87b
@ -105,6 +105,7 @@ export function OrganizerDashboardPage() {
|
|||||||
{data.milestones.length === 0 ? (
|
{data.milestones.length === 0 ? (
|
||||||
<EmptyState title={t('dashboard.milestoneEmptyTitle')} description={t('dashboard.milestoneEmptyDesc')} />
|
<EmptyState title={t('dashboard.milestoneEmptyTitle')} description={t('dashboard.milestoneEmptyDesc')} />
|
||||||
) : (
|
) : (
|
||||||
|
<div className="kx-timeline-scroll">
|
||||||
<ol className="kx-timeline">
|
<ol className="kx-timeline">
|
||||||
{data.milestones.map((m, i) => (
|
{data.milestones.map((m, i) => (
|
||||||
<li key={`${m.label}-${i}`} className={`kx-timeline__node is-${milestoneClass(m.state)}`}>
|
<li key={`${m.label}-${i}`} className={`kx-timeline__node is-${milestoneClass(m.state)}`}>
|
||||||
@ -124,6 +125,7 @@ export function OrganizerDashboardPage() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -38,14 +38,18 @@
|
|||||||
/* 히어로 타임라인 */
|
/* 히어로 타임라인 */
|
||||||
.kx-dash__hero {
|
.kx-dash__hero {
|
||||||
padding: var(--space-5) var(--space-4);
|
padding: var(--space-5) var(--space-4);
|
||||||
/* 하단 짤림 방지(소유자 2026-07-24): overflow-x:auto 시 가로 스크롤바가
|
|
||||||
마지막 라벨 줄을 덮지 않도록 스크롤바 몫의 하단 여유를 확보. */
|
|
||||||
padding-bottom: calc(var(--space-5) + 14px);
|
|
||||||
border: var(--border-card);
|
border: var(--border-card);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
|
/* 섹션 자체는 스크롤바 금지(소유자 2026-07-24 "상하 스크롤바 안 생기도록") —
|
||||||
|
가로 스크롤은 내부 래퍼(.kx-timeline-scroll)가 전담, 섹션 높이는 내용에 맞춰 늘어난다. */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.kx-timeline-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-gutter: stable;
|
overflow-y: hidden;
|
||||||
|
/* 스크롤바가 생겨도 라벨을 덮지 않도록 래퍼 하단 여유 */
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
.kx-timeline {
|
.kx-timeline {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
CATEGORY_LABEL,
|
CATEGORY_LABEL,
|
||||||
EVENT_YEARS,
|
EVENT_YEARS,
|
||||||
STATUS_LABEL,
|
STATUS_LABEL,
|
||||||
eventDaysFor,
|
|
||||||
hallUtilizationFor,
|
hallUtilizationFor,
|
||||||
type EventCategory,
|
type EventCategory,
|
||||||
type EventStatus,
|
type EventStatus,
|
||||||
@ -60,7 +59,6 @@ function isDegradable(err: unknown): boolean {
|
|||||||
|
|
||||||
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
|
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
|
||||||
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
|
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
|
||||||
const TODAY = new Date(2026, 6, 11); // 2026-07-11 (currentDate)
|
|
||||||
const TODAY_YMD = '2026-07-11';
|
const TODAY_YMD = '2026-07-11';
|
||||||
|
|
||||||
const KNOWN_CATEGORIES = new Set<EventCategory>(['exhibition', 'culture', 'trade', 'tech']);
|
const KNOWN_CATEGORIES = new Set<EventCategory>(['exhibition', 'culture', 'trade', 'tech']);
|
||||||
@ -315,7 +313,7 @@ export function ExhibitionSchedulePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<aside className="kx-sched__aside">
|
<aside className="kx-sched__aside">
|
||||||
<MiniCalendar cursor={asideCursor} onCursor={setAsideCursor} holidays={holidays} />
|
<AsideMonthNav cursor={asideCursor} onCursor={setAsideCursor} />
|
||||||
<HallAvailability cursor={asideCursor} />
|
<HallAvailability cursor={asideCursor} />
|
||||||
<HallUtilization cursor={asideCursor} />
|
<HallUtilization cursor={asideCursor} />
|
||||||
</aside>
|
</aside>
|
||||||
@ -324,92 +322,28 @@ export function ExhibitionSchedulePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const WD = ['일', '월', '화', '수', '목', '금', '토'];
|
|
||||||
|
|
||||||
function MiniCalendar({
|
function AsideMonthNav({
|
||||||
cursor,
|
cursor,
|
||||||
onCursor,
|
onCursor,
|
||||||
holidays,
|
|
||||||
}: {
|
}: {
|
||||||
cursor: Date;
|
cursor: Date;
|
||||||
onCursor: (d: Date) => void;
|
onCursor: (d: Date) => void;
|
||||||
holidays?: Map<string, string>;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const year = cursor.getFullYear();
|
const year = cursor.getFullYear();
|
||||||
const month = cursor.getMonth();
|
const month = cursor.getMonth();
|
||||||
const firstDay = new Date(year, month, 1).getDay();
|
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 실 원천: `GET /api/events/calendar?year=&month=` — event 테이블 기반 월 점유 집계.
|
|
||||||
* 실패(NETWORK/NOT_FOUND/NOT_IMPLEMENTED) 시에만 크롤링 샘플(eventDaysFor)로 폴백.
|
|
||||||
* eventDays 키 포맷은 `${year}-${month+1}-${day}`(무패딩) — 셀 렌더와 정합.
|
|
||||||
*/
|
|
||||||
const query = useQuery<{ days: Set<string>; degraded: boolean }>({
|
|
||||||
queryKey: ['schedule-calendar', year, month],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
const rows = await scheduleApi.calendar(year, month + 1);
|
|
||||||
const days = new Set<string>();
|
|
||||||
for (const r of rows) days.add(`${year}-${month + 1}-${Number(r.date.slice(8, 10))}`);
|
|
||||||
return { days, degraded: false };
|
|
||||||
} catch (err) {
|
|
||||||
if (isDegradable(err)) return { days: eventDaysFor(year, month), degraded: true };
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const eventDays = query.data?.days ?? new Set<string>();
|
|
||||||
const degraded = query.data?.degraded ?? false;
|
|
||||||
|
|
||||||
const cells: (number | null)[] = [];
|
|
||||||
for (let i = 0; i < firstDay; i++) cells.push(null);
|
|
||||||
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
|
||||||
|
|
||||||
const isToday = (d: number) =>
|
|
||||||
TODAY.getFullYear() === year && TODAY.getMonth() === month && TODAY.getDate() === d;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="kx-card kx-cal" aria-label={t('schedule.miniCal')}>
|
<section className="kx-card kx-cal kx-cal--compact" aria-label={t('schedule.asideMonthPicker')}>
|
||||||
{/* 역할 명확화(소유자 2026-07-24): 미니 캘린더 = 아래 홀 위젯의 조회 월 커서 */}
|
|
||||||
<div className="kx-cal__role">
|
<div className="kx-cal__role">
|
||||||
<strong className="kx-cal__role-title">{t('schedule.asideMonthPicker')}</strong>
|
<strong className="kx-cal__role-title">{t('schedule.asideMonthPicker')}</strong>
|
||||||
<span className="kx-cal__role-hint">{t('schedule.asideMonthPickerHint')}</span>
|
<span className="kx-cal__role-hint">{t('schedule.asideMonthPickerHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kx-cal__head">
|
<div className="kx-cal__head">
|
||||||
<button className="kx-cal__nav" aria-label={t('schedule.prevMonth')} onClick={() => onCursor(new Date(year, month - 1, 1))}>‹</button>
|
<button className="kx-cal__nav" aria-label={t('schedule.prevMonth')} onClick={() => onCursor(new Date(year, month - 1, 1))}>‹</button>
|
||||||
<strong>{t('schedule.yearMonth', { y: year, m: month + 1 })}{degraded && <span className="kx-bi__degraded"> {t('schedule.offlineSample')}</span>}</strong>
|
<strong className="tnum">{t('schedule.yearMonth', { y: year, m: month + 1 })}</strong>
|
||||||
<button className="kx-cal__nav" aria-label={t('schedule.nextMonth')} onClick={() => onCursor(new Date(year, month + 1, 1))}>›</button>
|
<button className="kx-cal__nav" aria-label={t('schedule.nextMonth')} onClick={() => onCursor(new Date(year, month + 1, 1))}>›</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="kx-cal__grid" role="grid">
|
|
||||||
{WD.map((w, i) => (
|
|
||||||
<span key={w} className="kx-cal__wd" role="columnheader">{t(`common.weekday.${i}`)}</span>
|
|
||||||
))}
|
|
||||||
{cells.map((d, i) => {
|
|
||||||
if (d === null) return <span key={`e${i}`} className="kx-cal__cell is-empty" />;
|
|
||||||
const hasEvent = eventDays.has(`${year}-${month + 1}-${d}`);
|
|
||||||
const hkey = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
||||||
const holidayName = holidays?.get(hkey);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={d}
|
|
||||||
className={`kx-cal__cell tnum ${isToday(d) ? 'is-today' : ''} ${hasEvent ? 'has-event' : ''} ${holidayName ? 'is-holiday' : ''}`}
|
|
||||||
role="gridcell"
|
|
||||||
title={holidayName || undefined}
|
|
||||||
aria-label={
|
|
||||||
holidayName
|
|
||||||
? `${t('schedule.day', { d })} ${holidayName}`
|
|
||||||
: hasEvent
|
|
||||||
? t('schedule.dayHasEvent', { d })
|
|
||||||
: t('schedule.day', { d })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{d}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user