feat: quarter board theme, hub column, and team panel UX

Apply preview-style 4-dept layout with center hub, PM/assignee team status linking, task type label updates, and remove task keywords.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-08 22:09:46 +09:00
parent 525a4fc1f2
commit cf72281c6d
28 changed files with 4743 additions and 314 deletions

View File

@@ -0,0 +1,90 @@
import { useCallback, useEffect, useState } from 'react';
import { migrateScheduleItem, quarterDateBounds } from './hubSchedule';
const STORAGE_KEY = 'eene-quarter-hub-config-v1';
export interface HubScheduleItem {
id: string;
/** YYYY-MM-DD */
date: string;
text: string;
}
export interface HubConfig {
sloganTitle: string;
sloganLines: string[];
scheduleTitle: string;
scheduleItems: HubScheduleItem[];
routineLabels: string[];
}
export const DEFAULT_HUB_CONFIG: HubConfig = {
sloganTitle: '분기 슬로건',
sloganLines: ['인사 · 육성 · 문화 · 총무', '개선과제', '정상 추진'],
scheduleTitle: '분기 주요 일정',
scheduleItems: [
{ id: '1', date: '2026-04-01', text: '상반기 채용·온보딩' },
{ id: '2', date: '2026-05-15', text: '조직문화 진단·리더십 교육' },
{ id: '3', date: '2026-06-20', text: '분기 성과 점검·평가' },
],
routineLabels: ['채용', '교육', '소통', '시설', '자산', '행정'],
};
function migrateConfig(raw: Record<string, unknown>): HubConfig {
const { year } = quarterDateBounds('2026-Q2');
const scheduleItems = Array.isArray(raw.scheduleItems)
? (raw.scheduleItems as (HubScheduleItem & { month?: string })[]).map((item) =>
migrateScheduleItem(item, year),
)
: DEFAULT_HUB_CONFIG.scheduleItems;
return {
sloganTitle: (raw.sloganTitle as string) ?? DEFAULT_HUB_CONFIG.sloganTitle,
sloganLines: (raw.sloganLines as string[]) ?? DEFAULT_HUB_CONFIG.sloganLines,
scheduleTitle: (raw.scheduleTitle as string) ?? DEFAULT_HUB_CONFIG.scheduleTitle,
scheduleItems,
routineLabels: (raw.routineLabels as string[]) ?? DEFAULT_HUB_CONFIG.routineLabels,
};
}
function loadConfig(): HubConfig {
if (typeof window === 'undefined') return DEFAULT_HUB_CONFIG;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_HUB_CONFIG;
return migrateConfig({ ...DEFAULT_HUB_CONFIG, ...JSON.parse(raw) });
} catch {
return DEFAULT_HUB_CONFIG;
}
}
function saveConfig(config: HubConfig) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
}
export function useHubConfig() {
const [config, setConfigState] = useState<HubConfig>(loadConfig);
const setConfig = useCallback((patch: Partial<HubConfig> | ((prev: HubConfig) => HubConfig)) => {
setConfigState((prev) => {
const next = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
saveConfig(next);
return next;
});
}, []);
const resetConfig = useCallback(() => {
localStorage.removeItem(STORAGE_KEY);
setConfigState(DEFAULT_HUB_CONFIG);
}, []);
useEffect(() => {
const onStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) setConfigState(loadConfig());
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);
return { config, setConfig, resetConfig };
}