import fs from 'fs'; import path from 'path'; import type { Priority, TaskStatus } from '@prisma/client'; import { getHrSeedPath, getUploadDir } from '../src/lib/projectPaths'; 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; owners?: string[]; } export interface HrTeamEntry { name: string; photo?: string; } export interface ParsedHrTeamMember { name: string; rank: string | null; role: string | null; cell: string | null; photoUrl: string | null; sortOrder: number; } 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; 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 = { 인사관리: '인사관리', 성장지원: '학습성장', 운영관리: '운영관리', 운영지원: '운영관리', 전산관리: '운영관리', }; const STATUS_MAP: Record = { 진행: 'IN_PROGRESS', 진행중: 'IN_PROGRESS', 대기: 'TODO', 완료: 'DONE', }; const PHASE_PROGRESS: Record = { 완료: 100, 진행중: 50, 미착수: 0, }; export function defaultHrDataPath(): string { return getHrSeedPath(); } 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); } export function loadHrTeam(filePath = defaultHrDataPath()): HrTeamEntry[] { const raw = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(raw) as { TEAM?: HrTeamEntry[] }; return data.TEAM ?? []; } /** "조태희 수석(팀장)" → name / rank / role */ export function parseHrTeamLabel(label: string, sortOrder: number): ParsedHrTeamMember { const s = label.trim(); const withRole = s.match(/^(.+?)\s+(.+?)\((.+)\)$/); if (withRole) { const role = withRole[3].trim(); return { name: withRole[1].trim(), rank: withRole[2].trim(), role, cell: role === '팀장' ? null : 'HR', photoUrl: null, sortOrder, }; } const plain = s.match(/^(.+?)\s+(.+)$/); if (plain) { return { name: plain[1].trim(), rank: plain[2].trim(), role: null, cell: 'HR', photoUrl: null, sortOrder, }; } return { name: s, rank: null, role: null, cell: 'HR', photoUrl: null, sortOrder }; } export function mapHrTeamMembers(filePath = defaultHrDataPath()): ParsedHrTeamMember[] { return loadHrTeam(filePath).map((entry, i) => { const parsed = parseHrTeamLabel(entry.name, i); const photo = resolveTeamPhotoPath(entry.photo?.trim() || null); return { ...parsed, photoUrl: photo, }; }); } /** seed용 — 파일이 프로젝트 uploads/ 에 실제 있을 때만 경로 사용 */ export function resolveTeamPhotoPath(photo: string | null): string | null { if (!photo?.trim()) return null; const trimmed = photo.trim(); if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith('data:')) return null; const uploadDir = getUploadDir(); const relative = trimmed.replace(/^\//, '').replace(/^uploads\//, ''); const abs = path.join(uploadDir, relative); if (fs.existsSync(abs)) { return trimmed.startsWith('/') ? trimmed : `/uploads/${relative}`; } return null; } /** PM·담당자 문자열 → team_members.name 매칭용 */ export function normalizePersonName(value: string): string { return value.trim().replace(/\s+/g, ''); } 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 mapBoardSection(p: HrProject): string { const name = p.name.trim(); if (/회사생활|C\.E\.L|조직문화|복리후생|문화\s*진단|직원\s*소통/i.test(name)) { return '조직문화'; } return mapSection(p.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: mapBoardSection(p), section: mapBoardSection(p), taskType, progress: mapProgress(p.progress), issueNote: pickIssueNote(p), startDate: parseDate(p.startDate), dueDate: parseDate(p.endDate), 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) .filter((p) => p.priority !== '상시') .map((p) => mapHrProjectToTask(p, quarter)); }