Initial commit - EENE Dashboard

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-05-29 18:07:10 +09:00
commit 22366dde72
64 changed files with 10483 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'MANAGER', 'MEMBER');
-- CreateEnum
CREATE TYPE "TaskStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'REVIEW', 'DONE', 'CANCELLED');
-- CreateEnum
CREATE TYPE "Priority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" "Role" NOT NULL DEFAULT 'MEMBER',
"department" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tasks" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"status" "TaskStatus" NOT NULL DEFAULT 'TODO',
"priority" "Priority" NOT NULL DEFAULT 'MEDIUM',
"quarter" TEXT NOT NULL,
"category" TEXT,
"dueDate" TIMESTAMP(3),
"creatorId" TEXT NOT NULL,
"assigneeId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "task_details" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"updatedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "task_details_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "kpi_metrics" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"quarter" TEXT NOT NULL,
"target" DOUBLE PRECISION NOT NULL,
"actual" DOUBLE PRECISION NOT NULL DEFAULT 0,
"unit" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "kpi_metrics_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "files" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"originalName" TEXT NOT NULL,
"mimetype" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"path" TEXT NOT NULL,
"uploadedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "files_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"entity" TEXT NOT NULL,
"entityId" TEXT,
"details" JSONB,
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "tasks_quarter_idx" ON "tasks"("quarter");
-- CreateIndex
CREATE INDEX "tasks_status_idx" ON "tasks"("status");
-- CreateIndex
CREATE INDEX "tasks_assigneeId_idx" ON "tasks"("assigneeId");
-- CreateIndex
CREATE INDEX "task_details_taskId_idx" ON "task_details"("taskId");
-- CreateIndex
CREATE INDEX "kpi_metrics_quarter_idx" ON "kpi_metrics"("quarter");
-- CreateIndex
CREATE INDEX "files_taskId_idx" ON "files"("taskId");
-- CreateIndex
CREATE INDEX "audit_logs_userId_idx" ON "audit_logs"("userId");
-- CreateIndex
CREATE INDEX "audit_logs_createdAt_idx" ON "audit_logs"("createdAt");
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_details" ADD CONSTRAINT "task_details_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "task_details" ADD CONSTRAINT "task_details_updatedBy_fkey" FOREIGN KEY ("updatedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "kpi_metrics" ADD CONSTRAINT "kpi_metrics_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "files" ADD CONSTRAINT "files_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "files" ADD CONSTRAINT "files_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,9 @@
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "issueNote" TEXT,
ADD COLUMN "progress" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "section" TEXT,
ADD COLUMN "tag" TEXT,
ADD COLUMN "taskType" TEXT;
-- CreateIndex
CREATE INDEX "tasks_section_idx" ON "tasks"("section");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,193 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ─── 사용자 ──────────────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
password String
name String
role Role @default(MEMBER)
department String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdTasks Task[] @relation("CreatedTasks")
assignedTasks Task[] @relation("AssignedTasks")
taskDetails TaskDetail[]
uploadedFiles File[]
auditLogs AuditLog[]
@@map("users")
}
enum Role {
ADMIN
MANAGER
MEMBER
}
// ─── 업무 ────────────────────────────────────────────────────
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
quarter String // 예: "2026-Q2"
category String?
section String? // HR | 운영관리
tag String? // Growth | Policy | Performance | Culture | Asset | Space | Safety | Environment
taskType String? // 상시업무 | 프로젝트
progress Int @default(0)
issueNote String?
startDate DateTime?
dueDate DateTime?
creatorId String
assigneeId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
creator User @relation("CreatedTasks", fields: [creatorId], references: [id])
assignee User? @relation("AssignedTasks", fields: [assigneeId], references: [id])
details TaskDetail[]
kpiMetrics KpiMetric[]
files File[]
milestones Milestone[]
@@index([quarter])
@@index([status])
@@index([assigneeId])
@@index([section])
@@map("tasks")
}
enum TaskStatus {
TODO
IN_PROGRESS
REVIEW
DONE
CANCELLED
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
// ─── 업무 상세 / 진행 기록 ────────────────────────────────────
model TaskDetail {
id String @id @default(cuid())
taskId String
content String
updatedBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
author User @relation(fields: [updatedBy], references: [id])
@@index([taskId])
@@map("task_details")
}
// ─── KPI 지표 ────────────────────────────────────────────────
model KpiMetric {
id String @id @default(cuid())
taskId String
quarter String
target Float
actual Float @default(0)
unit String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@index([quarter])
@@map("kpi_metrics")
}
// ─── 파일 ────────────────────────────────────────────────────
model File {
id String @id @default(cuid())
taskId String
filename String // 저장된 파일명 (UUID)
originalName String // 원본 파일명
mimetype String
size Int
path String
uploadedBy String
createdAt DateTime @default(now())
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
uploader User @relation(fields: [uploadedBy], references: [id])
@@index([taskId])
@@map("files")
}
// ─── 마일스톤 (프로세스 단계) ─────────────────────────────────
model Milestone {
id String @id @default(cuid())
taskId String
title String
description String?
dueDate DateTime?
completedAt DateTime?
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@index([taskId])
@@map("milestones")
}
// ─── 컬럼 설정 ───────────────────────────────────────────────
model ColumnConfig {
key String @id // "HR" | "운영관리"
title String
titleEn String?
subtitle String?
cardOrder String? // JSON 배열: task id 순서
updatedAt DateTime @updatedAt
@@map("column_configs")
}
// ─── 감사 로그 ───────────────────────────────────────────────
model AuditLog {
id String @id @default(cuid())
userId String
action String // CREATE, UPDATE, DELETE, LOGIN 등
entity String // Task, User, File 등
entityId String?
details Json?
ipAddress String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([createdAt])
@@map("audit_logs")
}

145
backend/prisma/seed.ts Normal file
View File

@@ -0,0 +1,145 @@
import 'dotenv/config';
import bcrypt from 'bcrypt';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// ─── 사용자 ─────────────────────────────────────────────
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: {},
create: { email: 'member@eene.com', password: memberPw, name: '홍길동', role: 'MEMBER', department: 'EENE' },
});
console.log(`✅ Users ready`);
// ─── 기존 업무 삭제 후 재생성 ─────────────────────────
await prisma.kpiMetric.deleteMany({});
await prisma.taskDetail.deleteMany({});
await prisma.task.deleteMany({});
// ─── HR 부문 업무 ──────────────────────────────────────
const hrTasks = [
{
title: '사내 핵심역량 교육체계 수립 (HRD)',
description: '직급별/직무별 필수 역량 가이드라인 도출\n사내 강사 제도 양성 및 콘텐츠 기획 단계',
status: 'IN_PROGRESS' as const,
priority: 'HIGH' as const,
section: 'HR',
tag: 'Growth',
taskType: '프로젝트',
progress: 20,
issueNote: '[5.28] 사내 강사 풀 확보 및 보상안 검토',
},
{
title: '그룹 표준 취업규칙 개정안 (HRM)',
description: '유연근무제 및 근태 관리 프로세스 고도화\n노사협의회 안건 조율 및 근로조건 개선안 반영',
status: 'IN_PROGRESS' as const,
priority: 'HIGH' as const,
section: 'HR',
tag: 'Policy',
taskType: '프로젝트',
progress: 40,
issueNote: '[5.28] 가족사 간 피드백 이견 조율 진행',
},
{
title: '상반기 평가 지표(KPI) 보완',
description: '1분기 피드백 기반 부서별 KPI 정렬\n상반기 업적 평가 시뮬레이션 기획',
status: 'IN_PROGRESS' as const,
priority: 'MEDIUM' as const,
section: 'HR',
tag: 'Performance',
taskType: '상시업무',
progress: 70,
issueNote: null,
},
{
title: '가족사 시너지 조직문화 캠페인',
description: '임직원 만족도 조사(Engagement Survey) 설계',
status: 'TODO' as const,
priority: 'LOW' as const,
section: 'HR',
tag: 'Culture',
taskType: '프로젝트',
progress: 0,
issueNote: null,
},
];
// ─── 운영관리 부문 업무 ────────────────────────────────
const opsTasks = [
{
title: '全사 IT자산 수명주기 표준화 구축',
description: '자산 전수조사 데이터 기반 불용 기준 정립\n라이선스 최적화를 통한 비용 절감안 도출',
status: 'IN_PROGRESS' as const,
priority: 'HIGH' as const,
section: '운영관리',
tag: 'Asset',
taskType: '프로젝트',
progress: 60,
issueNote: null,
},
{
title: '기술센터 2F 세미나룸 브랜딩 기획',
description: '다목적 교육 공간 활용을 위한 운영 매뉴얼 수립\n공간 아이덴티티 반영 사인물(Signage) 기획',
status: 'REVIEW' as const,
priority: 'MEDIUM' as const,
section: '운영관리',
tag: 'Space',
taskType: '프로젝트',
progress: 80,
issueNote: null,
},
{
title: '중대재해법 대응 안전보건 매뉴얼 정비',
description: '현장 점검 기반 소방 및 MSDS 관리 체계 보완\n사내 안전사고 예방 가이드라인 표준화 기획',
status: 'IN_PROGRESS' as const,
priority: 'HIGH' as const,
section: '운영관리',
tag: 'Safety',
taskType: '상시업무',
progress: 30,
issueNote: null,
},
{
title: '공용 공간 인프라 개선',
description: '기술센터 로비 및 라운지 환경 정비 기획',
status: 'TODO' as const,
priority: 'LOW' as const,
section: '운영관리',
tag: 'Environment',
taskType: '프로젝트',
progress: 0,
issueNote: null,
},
];
for (const t of [...hrTasks, ...opsTasks]) {
await prisma.task.create({
data: {
...t,
quarter: '2026-Q2',
category: t.section,
creatorId: admin.id,
assigneeId: member.id,
},
});
}
console.log(`✅ Tasks created: ${hrTasks.length + opsTasks.length}`);
console.log('🎉 Seeding complete!');
}
main().catch(console.error).finally(() => prisma.$disconnect());