Files
eene_dashboard/backend/prisma/mapHrProjects.ts
EENE Dashboard 6066b5682d fix: production save errors and import HR dashboard data
Resolve invalid task creator IDs, fix API routing and file uploads on Vercel, and replace dummy seed data with HR_Dashboard import.
2026-06-05 22:08:56 +09:00

212 lines
6.0 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import type { Priority, TaskStatus } from '@prisma/client';
export interface HrProject {
idx?: number;
name: string;
pm?: string;
priority: string;
startDate?: string;
endDate?: string;
status?: string;
category: string;
summary?: string;
content?: string;
briefIntro?: string;
progress?: number;
progressLog?: string;
progressStatus?: string;
issues?: string;
statusText?: string;
isIssue?: boolean;
keywords?: string[];
refLinks?: { name: string; url: string }[];
referenceUrl?: string;
subPhases?: { name: string; status?: string; text?: string }[];
timelineItems?: { startDate?: string; endDate?: string; desc?: string }[];
showOnDashboard?: boolean;
}
export interface MappedTask {
title: string;
description: string | null;
status: TaskStatus;
priority: Priority;
quarter: string;
category: string;
section: string;
taskType: string;
progress: number;
issueNote: string | null;
startDate: Date | null;
dueDate: Date | null;
keywords: string | null;
showDate: boolean;
showDescription: boolean;
showStatus: boolean;
showIssue: boolean;
showProgress: boolean;
milestones: {
title: string;
description: string | null;
progress: number;
links: string | null;
}[];
detailContent: string | null;
}
const SECTION_MAP: Record<string, string> = {
: '인사관리',
: '학습성장',
: '운영지원',
: '전산관리',
};
const STATUS_MAP: Record<string, TaskStatus> = {
: 'IN_PROGRESS',
: 'IN_PROGRESS',
: 'TODO',
: 'DONE',
};
const PHASE_PROGRESS: Record<string, number> = {
완료: 100,
진행중: 50,
미착수: 0,
};
export function defaultHrDataPath(): string {
return path.resolve(__dirname, '../../../HR_Dashboard/data.json');
}
export function loadHrProjects(filePath = defaultHrDataPath()): HrProject[] {
const raw = fs.readFileSync(filePath, 'utf-8');
const data = JSON.parse(raw) as { PROJECTS?: HrProject[] };
return (data.PROJECTS ?? []).filter((p) => p.showOnDashboard !== false);
}
function parseDate(value?: string): Date | null {
if (!value?.trim()) return null;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? null : d;
}
function mapSection(category: string): string {
return SECTION_MAP[category] ?? category;
}
function mapStatus(status?: string, isRoutine = false): TaskStatus {
if (!status?.trim()) return isRoutine ? 'IN_PROGRESS' : 'TODO';
return STATUS_MAP[status] ?? 'IN_PROGRESS';
}
function mapPriority(priority: string): Priority {
if (priority === '상시') return 'MEDIUM';
if (priority === '높음') return 'HIGH';
if (priority === '낮음') return 'LOW';
return 'MEDIUM';
}
function mapProgress(value?: number): number {
if (value == null || Number.isNaN(value)) return 0;
if (value <= 1) return Math.round(value * 100);
return Math.min(100, Math.round(value));
}
function pickDescription(p: HrProject): string | null {
const candidates = [p.content, p.summary, p.briefIntro].map((v) => v?.trim()).filter(Boolean) as string[];
const best = candidates.find((v) => v.length > 2 && v !== '12');
return best ?? null;
}
function pickIssueNote(p: HrProject): string | null {
const parts: string[] = [];
if (p.isIssue && p.statusText?.trim()) parts.push(p.statusText.trim());
if (p.issues?.trim()) parts.push(p.issues.trim());
if (p.progressLog?.trim() && p.progressLog !== '이슈사항') parts.push(p.progressLog.trim());
return parts.length ? parts.join('\n') : null;
}
function buildLinks(p: HrProject): string | null {
const links: { label: string; url: string }[] = [];
for (const ref of p.refLinks ?? []) {
if (ref.url?.trim()) links.push({ label: ref.name || '참고자료', url: ref.url.trim() });
}
if (p.referenceUrl?.trim()) links.push({ label: '참고링크', url: p.referenceUrl.trim() });
return links.length ? JSON.stringify(links) : null;
}
function buildMilestones(p: HrProject): MappedTask['milestones'] {
const milestones: MappedTask['milestones'] = [];
for (const [i, phase] of (p.subPhases ?? []).entries()) {
milestones.push({
title: phase.name,
description: phase.text?.trim() || null,
progress: PHASE_PROGRESS[phase.status ?? ''] ?? 0,
links: null,
});
}
for (const item of p.timelineItems ?? []) {
if (!item.desc?.trim()) continue;
milestones.push({
title: item.desc.trim(),
description: [item.startDate, item.endDate].filter(Boolean).join(' ~ ') || null,
progress: 0,
links: null,
});
}
if (milestones.length === 0 && buildLinks(p)) {
milestones.push({
title: '참고자료',
description: null,
progress: 0,
links: buildLinks(p),
});
}
return milestones;
}
function buildDetailContent(p: HrProject): string | null {
const content = p.progressStatus?.trim() || p.progressLog?.trim();
if (!content || content === '이슈사항' || content === '12') return null;
return content;
}
export function mapHrProjectToTask(p: HrProject, quarter = '2026-Q2'): MappedTask {
const isRoutine = p.priority === '상시';
const taskType = isRoutine ? '기반업무' : '실행과제';
const visible = !isRoutine;
return {
title: p.name.trim(),
description: pickDescription(p),
status: mapStatus(p.status, isRoutine),
priority: mapPriority(p.priority),
quarter,
category: mapSection(p.category),
section: mapSection(p.category),
taskType,
progress: mapProgress(p.progress),
issueNote: pickIssueNote(p),
startDate: parseDate(p.startDate),
dueDate: parseDate(p.endDate),
keywords: p.keywords?.length ? p.keywords.join(', ') : null,
showDate: visible,
showDescription: visible,
showStatus: visible,
showIssue: visible,
showProgress: visible,
milestones: buildMilestones(p),
detailContent: buildDetailContent(p),
};
}
export function mapAllHrProjects(filePath?: string, quarter = '2026-Q2'): MappedTask[] {
return loadHrProjects(filePath).map((p) => mapHrProjectToTask(p, quarter));
}