EENE Dashboard upload to Gitea

This commit is contained in:
EENE Dashboard
2026-06-18 15:06:37 +09:00
parent d3548cf7ff
commit c31eca4b58
82 changed files with 126 additions and 48 deletions

View File

@@ -42,7 +42,7 @@ const HUB_CONFIG = {
],
routineLabels: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
routineLabels: ['채용 운영', '학습 지원', '운영 지원', '자산·시설', '문서·행정'],
};

View File

@@ -13,11 +13,22 @@ export const DEFAULT_HUB_CONFIG = {
{ id: '2', date: '2026-05-15', text: '조직문화 진단·리더십 교육' },
{ id: '3', date: '2026-06-20', text: '분기 성과 점검·평가' },
],
routineLabels: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
routineLabels: ['채용 운영', '학습 지원', '운영 지원', '자산·시설', '문서·행정'],
};
function migrateRoutineLabels(labels: string[]): string[] {
return labels.map((label) => {
if (label === '교육 운영') return '학습 지원';
if (label === '직원 소통') return '운영 지원';
return label;
});
}
function normalizeConfig(raw: Record<string, unknown>) {
const sloganTitle = (raw.sloganTitle as string) ?? DEFAULT_HUB_CONFIG.sloganTitle;
const routineLabels = Array.isArray(raw.routineLabels)
? migrateRoutineLabels(raw.routineLabels as string[])
: DEFAULT_HUB_CONFIG.routineLabels;
return {
sloganTitle: sloganTitle === '분기 슬로건' ? '분기 중점 과제' : sloganTitle,
sloganLines: Array.isArray(raw.sloganLines)
@@ -27,9 +38,7 @@ function normalizeConfig(raw: Record<string, unknown>) {
scheduleItems: Array.isArray(raw.scheduleItems)
? (raw.scheduleItems as typeof DEFAULT_HUB_CONFIG.scheduleItems)
: DEFAULT_HUB_CONFIG.scheduleItems,
routineLabels: Array.isArray(raw.routineLabels)
? (raw.routineLabels as string[])
: DEFAULT_HUB_CONFIG.routineLabels,
routineLabels,
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1645,7 +1645,7 @@
<div class="hub-routine-grid">
<button type="button" class="hub-routine-item">채용 운영</button>
<button type="button" class="hub-routine-item">교육 운영</button>
<button type="button" class="hub-routine-item">직원 소통</button>
<button type="button" class="hub-routine-item">운영 지원</button>
<button type="button" class="hub-routine-item">자산·시설</button>
<button type="button" class="hub-routine-item">문서·행정</button>
</div>

View File

@@ -100,7 +100,7 @@ const DUMMY_DEPARTMENTS: DeptBlock[] = [
const HUB_MESSAGE = '인사·육성·문화·총무 개선과제 정상 추진';
const ROUTINE_ITEMS = ['채용 운영', '교육 운영', '직원 소통', '자산·시설 관리', '문서·행정 지원'];
const ROUTINE_ITEMS = ['채용 운영', '교육 운영', '운영 지원', '자산·시설 관리', '문서·행정 지원'];
const FOCUS_ITEMS = ['핵심직무 채용 완료', '복지제도 개선안 확정', '안전보안 점검 강화'];

View File

@@ -46,7 +46,7 @@ const VARIANT_DEFAULTS = {
portfolioTab: '분기 전체',
},
routine: {
focusTitle: '업무별 타임라인',
focusTitle: '업무별 타임라인',
projectTitle: '상시 전체 일정',
portfolioTitle: '분기 전체 상시업무',
rowLabelHeader: '업무명',
@@ -140,9 +140,45 @@ export function MilestoneTimeline({
const chartRef = useRef<HTMLDivElement>(null);
const labelsScrollRef = useRef<HTMLDivElement>(null);
const rowRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const measureRef = useRef<HTMLSpanElement>(null);
const dragRef = useRef<{ startX: number; viewport: { start: Date; end: Date } } | null>(null);
const didPanRef = useRef(false);
const [isPanning, setIsPanning] = useState(false);
const [expandedBar, setExpandedBar] = useState<{
id: string;
widthPx: number;
leftPx: number;
} | null>(null);
const measureTitleWidth = (title: string) => {
const el = measureRef.current;
if (!el) return 0;
el.textContent = title;
return el.offsetWidth + 20;
};
const handleFocusBarEnter = (
row: { id: string; leftPct: number; widthPct: number },
title: string,
event: React.MouseEvent<HTMLButtonElement>,
) => {
const chart = chartRef.current;
if (!chart) return;
const btn = event.currentTarget;
const chartWidth = chart.clientWidth;
const leftPx = (row.leftPct / 100) * chartWidth;
const origWidthPx = btn.offsetWidth;
const neededWidthPx = measureTitleWidth(title);
if (neededWidthPx <= origWidthPx + 1) return;
const widthPx = Math.min(chartWidth, neededWidthPx);
const extra = widthPx - origWidthPx;
let expandedLeftPx = leftPx - extra / 2;
if (expandedLeftPx < 0) expandedLeftPx = 0;
if (expandedLeftPx + widthPx > chartWidth) expandedLeftPx = chartWidth - widthPx;
setExpandedBar({ id: row.id, widthPx, leftPx: expandedLeftPx });
};
const rowGroups = useMemo(() => {
if (!model) return [];
@@ -286,6 +322,7 @@ export function MilestoneTimeline({
setIsPanning(false);
dragRef.current = null;
didPanRef.current = false;
setExpandedBar(null);
}, [viewMode]);
const handleRowSelect = (milestoneId: string, taskId: string) => {
@@ -294,7 +331,7 @@ export function MilestoneTimeline({
onSelect?.(milestoneId);
};
const renderBar = (
const renderGanttBar = (
group: {
milestoneId: string;
title: string;
@@ -303,13 +340,12 @@ export function MilestoneTimeline({
},
isSelected: boolean,
taskId: string,
showGanttBar: boolean,
) =>
group.segments.map((row) => (
<button
key={row.id}
type="button"
className={`milestone-timeline__bar ${isSelected ? 'is-selected' : ''} ${showGanttBar ? 'is-gantt' : ''}`}
className={`milestone-timeline__bar is-gantt ${isSelected ? 'is-selected' : ''}`}
style={{ left: `${row.leftPct}%`, width: `${row.widthPct}%` }}
aria-label={group.title}
title={group.title}
@@ -317,12 +353,60 @@ export function MilestoneTimeline({
>
<span className="milestone-timeline__bar-track" />
<span className="milestone-timeline__bar-fill" style={{ width: `${row.progress}%` }} />
{showGanttBar && group.progress > 0 && (
{group.progress > 0 && (
<span className="milestone-timeline__bar-progress">{group.progress}%</span>
)}
</button>
));
const renderFocusBar = (
group: {
milestoneId: string;
title: string;
progress: number;
segments: Array<{ id: string; leftPct: number; widthPct: number; progress: number }>;
},
isSelected: boolean,
taskId: string,
) =>
group.segments.map((row) => {
const isExpanded = expandedBar?.id === row.id;
return (
<button
key={row.id}
type="button"
className={`milestone-timeline__bar${isSelected ? ' is-selected' : ''}${isExpanded ? ' is-expanded' : ''}`}
style={
isExpanded
? { left: expandedBar!.leftPx, width: expandedBar!.widthPx }
: { left: `${row.leftPct}%`, width: `${row.widthPct}%` }
}
aria-label={group.title}
title={group.title}
onMouseEnter={(e) => handleFocusBarEnter(row, group.title, e)}
onMouseLeave={() => setExpandedBar(null)}
onClick={() => handleRowSelect(group.milestoneId, taskId)}
>
<span className="milestone-timeline__bar-track" />
<span className="milestone-timeline__bar-fill" style={{ width: `${row.progress}%` }} />
<span className="milestone-timeline__bar-label-wrap" aria-hidden="true">
<span
className="milestone-timeline__bar-label milestone-timeline__bar-label--fill"
style={{ clipPath: `inset(0 ${100 - row.progress}% 0 0)` }}
>
{group.title}
</span>
<span
className="milestone-timeline__bar-label milestone-timeline__bar-label--track"
style={{ clipPath: `inset(0 0 0 ${row.progress}%)` }}
>
{group.title}
</span>
</span>
</button>
);
});
const renderTicks = () => (
<div className="milestone-timeline__ticks" aria-hidden="true">
{model!.ticks.map((tick) => (
@@ -362,15 +446,12 @@ export function MilestoneTimeline({
const renderChartHead = () => (
<div className="milestone-timeline__chart-head">
{renderTicks()}
<span className="milestone-timeline__viewport-range">
{model!.rangeStartLabel}~{model!.rangeEndLabel}
</span>
</div>
);
const renderPanHint = () => (
<p className="milestone-timeline__pan-hint">
드래그: 기간 · : 확대/ · {model!.rangeStartLabel}~{model!.rangeEndLabel}
드래그: 기간 · : 확대/
</p>
);
@@ -425,11 +506,6 @@ export function MilestoneTimeline({
<div className="milestone-timeline__body milestone-timeline__body--ticks-only">
{renderTicks()}
</div>
{model && (
<span className="milestone-timeline__viewport-range">
{model.rangeStartLabel}~{model.rangeEndLabel}
</span>
)}
</div>
</div>
@@ -478,11 +554,10 @@ export function MilestoneTimeline({
key={row.milestoneId}
className={`milestone-timeline__row${row.isCurrentTask ? ' is-current-task' : ' is-other-task'}`}
>
{renderBar(
{renderGanttBar(
row,
row.milestoneId === selectedId && row.isCurrentTask,
row.taskId,
true,
)}
</div>
),
@@ -530,7 +605,7 @@ export function MilestoneTimeline({
else rowRefs.current.delete(group.milestoneId);
}}
>
{renderBar(group, group.milestoneId === selectedId, ownerTaskId ?? '', true)}
{renderGanttBar(group, group.milestoneId === selectedId, ownerTaskId ?? '')}
</div>
))}
</div>
@@ -541,6 +616,7 @@ export function MilestoneTimeline({
</>
) : (
<div className="milestone-timeline__focus-layout">
<span ref={measureRef} className="milestone-timeline__measure" aria-hidden="true" />
{renderChartHead()}
<div
className={chartPannableClass}
@@ -551,7 +627,7 @@ export function MilestoneTimeline({
<div className="milestone-timeline__rows">
{projectRowGroups.map((group) => (
<div key={group.milestoneId} className="milestone-timeline__row">
{renderBar(group, group.milestoneId === selectedId, ownerTaskId ?? '', false)}
{renderFocusBar(group, group.milestoneId === selectedId, ownerTaskId ?? '')}
</div>
))}
</div>

View File

@@ -31,7 +31,7 @@ export const DEFAULT_HUB_CONFIG: HubConfig = {
{ id: '2', date: '2026-05-15', text: '조직문화 진단·리더십 교육' },
{ id: '3', date: '2026-06-20', text: '분기 성과 점검·평가' },
],
routineLabels: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
routineLabels: ['채용 운영', '학습 지원', '운영 지원', '자산·시설', '문서·행정'],
};
function migrateRoutineLabels(raw: unknown): string[] {
@@ -40,7 +40,7 @@ function migrateRoutineLabels(raw: unknown): string[] {
if (labels.length === ROUTINE_CATEGORIES.length && labels.every((label, i) => label === ROUTINE_CATEGORIES[i])) {
return [...ROUTINE_CATEGORIES];
}
const legacyFull = ['채용 운영', '교육 운영', '직원 소통', '자산·시설', '문서·행정'];
const legacyFull = ['채용 운영', '교육 운영', '직원 소통', '운영 지원', '자산·시설', '문서·행정'];
if (labels.length === legacyFull.length && labels.every((label, i) => label === legacyFull[i])) {
return [...ROUTINE_CATEGORIES];
}
@@ -51,6 +51,9 @@ function migrateRoutineLabels(raw: unknown): string[] {
if (labels.some((label) => label === '교육 운영')) {
return labels.map((label) => (label === '교육 운영' ? '학습 지원' : label));
}
if (labels.some((label) => label === '직원 소통')) {
return labels.map((label) => (label === '직원 소통' ? '운영 지원' : label));
}
return labels.length > 0 ? labels : [...ROUTINE_CATEGORIES];
}

View File

@@ -100,6 +100,7 @@ export function fmtPeriodPickerLabel(
if (entry.startDate && entry.dueDate) return `${fmt(entry.startDate)}~${fmt(entry.dueDate)}`;
if (entry.dueDate) return fmt(entry.dueDate);
if (entry.startDate) return `${fmt(entry.startDate)}~`;
if (!entry.startDate && !entry.dueDate) return '상시';
return `기간 ${index + 1}`;
}
@@ -133,7 +134,11 @@ export function fmtMilestonePeriodSummary(
): string {
const periods = parseMilestonePeriods(milestone);
if (periods.length === 0) return '';
if (periods.length === 1) return fmtPeriodRange(periods[0]) || '';
if (periods.length === 1) {
const only = periods[0];
if (!only.startDate && !only.dueDate) return '상시';
return fmtPeriodRange(only) || '상시';
}
const last = periods[periods.length - 1];
const lastLabel = fmtPeriodRange(last);
return lastLabel ? `${lastLabel}${periods.length - 1}` : `기간 ${periods.length}`;

View File

@@ -2,7 +2,7 @@
export const ROUTINE_CATEGORIES = [
'채용 운영',
'학습 지원',
'직원 소통',
'운영 지원',
'자산·시설',
'문서·행정',
] as const;
@@ -16,8 +16,10 @@ const LEGACY_CATEGORY_ALIASES: Record<string, RoutineCategory> = {
'교육 운영': '학습 지원',
'학습 지원': '학습 지원',
: '학습 지원',
: '직원 소통',
'직원 소통': '직원 소통',
: '운영 지원',
'직원 소통': '운영 지원',
'운영 지원': '운영 지원',
: '운영 지원',
: '자산·시설',
: '자산·시설',
'자산·시설': '자산·시설',
@@ -27,7 +29,6 @@ const LEGACY_CATEGORY_ALIASES: Record<string, RoutineCategory> = {
'문서·행정 지원': '문서·행정',
: '채용 운영',
: '학습 지원',
: '자산·시설',
: '문서·행정',
};

View File

@@ -1007,22 +1007,6 @@
margin: 0 4px;
}
.milestone-timeline--portfolio .milestone-timeline__viewport-range {
padding: 0 8px 2px;
color: var(--detail-text-muted);
font-size: var(--mt-font-min);
font-weight: var(--mt-font-min-weight);
text-align: right;
}
.milestone-timeline__viewport-range {
padding: 0 8px 2px;
color: var(--detail-text-muted);
font-size: var(--mt-font-min);
font-weight: var(--mt-font-min-weight);
text-align: right;
}
.milestone-timeline--portfolio .milestone-timeline__portfolio-scroll {
flex: 1;
min-height: 0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB