EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -26,6 +27,21 @@ export interface HrProject {
|
||||
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 {
|
||||
@@ -59,6 +75,8 @@ const SECTION_MAP: Record<string, string> = {
|
||||
인사관리: '인사관리',
|
||||
성장지원: '학습성장',
|
||||
운영관리: '운영관리',
|
||||
운영지원: '운영관리',
|
||||
전산관리: '운영관리',
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, TaskStatus> = {
|
||||
@@ -75,7 +93,7 @@ const PHASE_PROGRESS: Record<string, number> = {
|
||||
};
|
||||
|
||||
export function defaultHrDataPath(): string {
|
||||
return path.resolve(__dirname, '../../../HR_Dashboard/data.json');
|
||||
return getHrSeedPath();
|
||||
}
|
||||
|
||||
export function loadHrProjects(filePath = defaultHrDataPath()): HrProject[] {
|
||||
@@ -84,6 +102,72 @@ export function loadHrProjects(filePath = defaultHrDataPath()): 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);
|
||||
@@ -94,6 +178,14 @@ 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';
|
||||
@@ -186,8 +278,8 @@ export function mapHrProjectToTask(p: HrProject, quarter = '2026-Q2'): MappedTas
|
||||
status: mapStatus(p.status, isRoutine),
|
||||
priority: mapPriority(p.priority),
|
||||
quarter,
|
||||
category: mapSection(p.category),
|
||||
section: mapSection(p.category),
|
||||
category: mapBoardSection(p),
|
||||
section: mapBoardSection(p),
|
||||
taskType,
|
||||
progress: mapProgress(p.progress),
|
||||
issueNote: pickIssueNote(p),
|
||||
@@ -204,5 +296,7 @@ export function mapHrProjectToTask(p: HrProject, quarter = '2026-Q2'): MappedTas
|
||||
}
|
||||
|
||||
export function mapAllHrProjects(filePath?: string, quarter = '2026-Q2'): MappedTask[] {
|
||||
return loadHrProjects(filePath).map((p) => mapHrProjectToTask(p, quarter));
|
||||
return loadHrProjects(filePath)
|
||||
.filter((p) => p.priority !== '상시')
|
||||
.map((p) => mapHrProjectToTask(p, quarter));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
ALTER TABLE "milestones" ADD COLUMN IF NOT EXISTS "pmMemberId" TEXT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "milestone_assignees" (
|
||||
"milestoneId" TEXT NOT NULL,
|
||||
"memberId" TEXT NOT NULL,
|
||||
CONSTRAINT "milestone_assignees_pkey" PRIMARY KEY ("milestoneId","memberId")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "milestone_assignees_memberId_idx" ON "milestone_assignees"("memberId");
|
||||
CREATE INDEX IF NOT EXISTS "milestones_pmMemberId_idx" ON "milestones"("pmMemberId");
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "milestones" ADD CONSTRAINT "milestones_pmMemberId_fkey"
|
||||
FOREIGN KEY ("pmMemberId") REFERENCES "team_members"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "milestone_assignees" ADD CONSTRAINT "milestone_assignees_milestoneId_fkey"
|
||||
FOREIGN KEY ("milestoneId") REFERENCES "milestones"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "milestone_assignees" ADD CONSTRAINT "milestone_assignees_memberId_fkey"
|
||||
FOREIGN KEY ("memberId") REFERENCES "team_members"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "issueEntries" JSONB;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE IF NOT EXISTS "hub_configs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"config" JSONB NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "hub_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "milestones" ADD COLUMN IF NOT EXISTS "subtitle" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "milestones" ADD COLUMN "periodEntries" JSONB;
|
||||
@@ -46,6 +46,8 @@ model TeamMember {
|
||||
|
||||
pmTasks Task[] @relation("PmTasks")
|
||||
taskAssignees TaskAssignee[]
|
||||
milestonePmTasks Milestone[] @relation("MilestonePm")
|
||||
milestoneAssignees MilestoneAssignee[]
|
||||
|
||||
@@index([cell])
|
||||
@@index([isActive])
|
||||
@@ -72,7 +74,8 @@ model Task {
|
||||
tag String? // Growth | Policy | Performance | Culture | Asset | Space | Safety | Environment
|
||||
taskType String? // 상시업무 | 프로젝트
|
||||
progress Int @default(0)
|
||||
issueNote String?
|
||||
issueNote String?
|
||||
issueEntries Json?
|
||||
startDate DateTime?
|
||||
dueDate DateTime?
|
||||
showDate Boolean @default(true)
|
||||
@@ -200,24 +203,42 @@ model Milestone {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
title String
|
||||
subtitle String?
|
||||
description String?
|
||||
startDate DateTime?
|
||||
dueDate DateTime?
|
||||
periodEntries Json?
|
||||
progress Int @default(0)
|
||||
links String? // JSON: [{ "label": string, "url": string }]
|
||||
completedAt DateTime?
|
||||
order Int @default(0)
|
||||
pmMemberId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
details TaskDetail[]
|
||||
files File[]
|
||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
pmMember TeamMember? @relation("MilestonePm", fields: [pmMemberId], references: [id])
|
||||
milestoneAssignees MilestoneAssignee[]
|
||||
details TaskDetail[]
|
||||
files File[]
|
||||
|
||||
@@index([taskId])
|
||||
@@index([pmMemberId])
|
||||
@@map("milestones")
|
||||
}
|
||||
|
||||
model MilestoneAssignee {
|
||||
milestoneId String
|
||||
memberId String
|
||||
|
||||
milestone Milestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
|
||||
member TeamMember @relation(fields: [memberId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([milestoneId, memberId])
|
||||
@@index([memberId])
|
||||
@@map("milestone_assignees")
|
||||
}
|
||||
|
||||
// ─── 컬럼 설정 ───────────────────────────────────────────────
|
||||
|
||||
model ColumnConfig {
|
||||
@@ -231,6 +252,16 @@ model ColumnConfig {
|
||||
@@map("column_configs")
|
||||
}
|
||||
|
||||
// ─── 허브 설정 (분기 중점 과제·일정·상시 라벨) ─────────────────
|
||||
|
||||
model HubConfig {
|
||||
id String @id @default("default")
|
||||
config Json
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("hub_configs")
|
||||
}
|
||||
|
||||
// ─── 감사 로그 ───────────────────────────────────────────────
|
||||
|
||||
model AuditLog {
|
||||
|
||||
@@ -1,67 +1,273 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { mapAllHrProjects } from './mapHrProjects';
|
||||
|
||||
import {
|
||||
|
||||
loadHrProjects,
|
||||
|
||||
mapAllHrProjects,
|
||||
|
||||
mapHrProjectToTask,
|
||||
|
||||
mapHrTeamMembers,
|
||||
|
||||
normalizePersonName,
|
||||
|
||||
} from './mapHrProjects';
|
||||
|
||||
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
|
||||
const HUB_CONFIG = {
|
||||
|
||||
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: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Seeding database...');
|
||||
|
||||
console.log('🌱 Seeding database from data/seed/hr-data.json ...');
|
||||
|
||||
|
||||
|
||||
const adminPw = await bcrypt.hash('admin1234!', 12);
|
||||
|
||||
const memberPw = await bcrypt.hash('member1234!', 12);
|
||||
|
||||
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
|
||||
where: { email: 'admin@eene.com' },
|
||||
|
||||
update: {},
|
||||
|
||||
create: { email: 'admin@eene.com', password: adminPw, name: '관리자', role: 'ADMIN', department: 'EENE' },
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const member = await prisma.user.upsert({
|
||||
|
||||
where: { email: 'member@eene.com' },
|
||||
|
||||
update: { name: '정성호' },
|
||||
|
||||
create: { email: 'member@eene.com', password: memberPw, name: '정성호', role: 'MEMBER', department: 'EENE' },
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
console.log('✅ Users ready');
|
||||
|
||||
const mapped = mapAllHrProjects();
|
||||
|
||||
|
||||
await prisma.milestoneAssignee.deleteMany({});
|
||||
|
||||
await prisma.taskAssignee.deleteMany({});
|
||||
|
||||
await prisma.file.deleteMany({});
|
||||
|
||||
await prisma.taskDetail.deleteMany({});
|
||||
|
||||
await prisma.milestone.deleteMany({});
|
||||
|
||||
await prisma.kpiMetric.deleteMany({});
|
||||
|
||||
await prisma.task.deleteMany({});
|
||||
|
||||
for (const t of mapped) {
|
||||
const { milestones, detailContent, ...taskData } = t;
|
||||
const task = await prisma.task.create({
|
||||
await prisma.teamMember.deleteMany({});
|
||||
|
||||
|
||||
|
||||
const teamParsed = mapHrTeamMembers();
|
||||
|
||||
const memberIdByName = new Map<string, string>();
|
||||
|
||||
|
||||
|
||||
for (const tm of teamParsed) {
|
||||
|
||||
const created = await prisma.teamMember.create({
|
||||
|
||||
data: {
|
||||
...taskData,
|
||||
creatorId: admin.id,
|
||||
assigneeId: member.id,
|
||||
|
||||
name: tm.name,
|
||||
|
||||
rank: tm.rank,
|
||||
|
||||
role: tm.role,
|
||||
|
||||
cell: tm.cell,
|
||||
|
||||
photoUrl: tm.photoUrl,
|
||||
|
||||
sortOrder: tm.sortOrder,
|
||||
|
||||
isActive: true,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
for (const [order, ms] of milestones.entries()) {
|
||||
await prisma.milestone.create({
|
||||
data: { ...ms, taskId: task.id, order },
|
||||
});
|
||||
}
|
||||
memberIdByName.set(normalizePersonName(tm.name), created.id);
|
||||
|
||||
if (detailContent) {
|
||||
await prisma.taskDetail.create({
|
||||
data: {
|
||||
taskId: task.id,
|
||||
content: detailContent,
|
||||
updatedBy: admin.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Tasks created: ${mapped.length}개 (HR_Dashboard 데이터)`);
|
||||
|
||||
|
||||
console.log(`✅ Team members: ${teamParsed.length}명 (hr-data.json TEAM)`);
|
||||
|
||||
|
||||
|
||||
const hrProjects = loadHrProjects();
|
||||
|
||||
let taskCount = 0;
|
||||
|
||||
|
||||
|
||||
for (const hp of hrProjects) {
|
||||
if (hp.priority === '상시') continue;
|
||||
|
||||
const t = mapHrProjectToTask(hp);
|
||||
|
||||
const { milestones, detailContent, ...taskData } = t;
|
||||
|
||||
|
||||
|
||||
const pmKey = hp.pm?.trim() ? normalizePersonName(hp.pm) : '';
|
||||
|
||||
const pmMemberId = pmKey ? memberIdByName.get(pmKey) ?? null : null;
|
||||
|
||||
|
||||
|
||||
const ownerIds = [...new Set(
|
||||
|
||||
(hp.owners ?? [])
|
||||
|
||||
.map((o) => normalizePersonName(o))
|
||||
|
||||
.filter(Boolean)
|
||||
|
||||
.map((key) => memberIdByName.get(key))
|
||||
|
||||
.filter((id): id is string => !!id),
|
||||
|
||||
)];
|
||||
|
||||
|
||||
|
||||
const task = await prisma.task.create({
|
||||
|
||||
data: {
|
||||
|
||||
...taskData,
|
||||
|
||||
creatorId: admin.id,
|
||||
|
||||
assigneeId: member.id,
|
||||
|
||||
pmMemberId,
|
||||
|
||||
...(ownerIds.length > 0
|
||||
|
||||
? { taskAssignees: { create: ownerIds.map((memberId) => ({ memberId })) } }
|
||||
|
||||
: {}),
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
for (const [order, ms] of milestones.entries()) {
|
||||
|
||||
await prisma.milestone.create({
|
||||
|
||||
data: { ...ms, taskId: task.id, order },
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (detailContent) {
|
||||
|
||||
await prisma.taskDetail.create({
|
||||
|
||||
data: {
|
||||
|
||||
taskId: task.id,
|
||||
|
||||
content: detailContent,
|
||||
|
||||
updatedBy: admin.id,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
taskCount += 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
await prisma.hubConfig.upsert({
|
||||
|
||||
where: { id: 'default' },
|
||||
|
||||
update: { config: HUB_CONFIG },
|
||||
|
||||
create: { id: 'default', config: HUB_CONFIG },
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
console.log(`✅ Tasks: ${taskCount}개 (PROJECTS)`);
|
||||
|
||||
console.log('✅ Hub config reset (5 대분류 상시업무)');
|
||||
|
||||
console.log('🎉 Seeding complete!');
|
||||
|
||||
console.log(' → 브라우저에서 Ctrl+F5 후, 필요 시 DevTools에서 localStorage 허브 키 삭제');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user