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:
90
frontend/src/lib/hubConfig.ts
Normal file
90
frontend/src/lib/hubConfig.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user