EENE Dashboard upload to Gitea

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-17 16:59:34 +09:00
parent cf72281c6d
commit b3f2da203b
138 changed files with 13013 additions and 1929 deletions

View File

@@ -0,0 +1,75 @@
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import { PrismaClient } from '@prisma/client';
import { getProjectRoot } from '../src/lib/projectPaths';
const prisma = new PrismaClient();
/** hr-data legacy 상시업무 — 허브 대분류 셸로 대체, 시드·DB에서 제거 */
const LEGACY_ROUTINE_TITLES = [
'H/W, S/W',
'시설관리',
'배움터',
'인재채용',
'학습 지원',
'채용 운영',
];
async function deleteLegacyRoutineTasks() {
for (const title of LEGACY_ROUTINE_TITLES) {
const task = await prisma.task.findFirst({
where: { title, taskType: { in: ['기반업무', '상시업무'] } },
});
if (!task) {
console.log(` skip (not found): ${title}`);
continue;
}
await prisma.task.delete({ where: { id: task.id } });
console.log(` 🗑 deleted: ${title}`);
}
const remaining = await prisma.task.count({
where: { taskType: { in: ['기반업무', '상시업무'] } },
});
if (remaining > 0) {
const extras = await prisma.task.findMany({
where: { taskType: { in: ['기반업무', '상시업무'] } },
select: { title: true },
});
for (const t of extras) {
await prisma.task.deleteMany({ where: { title: t.title, taskType: { in: ['기반업무', '상시업무'] } } });
console.log(` 🗑 deleted (extra routine): ${t.title}`);
}
}
}
function patchHrData() {
const seedPath = path.join(getProjectRoot(), 'data', 'seed', 'hr-data.json');
const data = JSON.parse(fs.readFileSync(seedPath, 'utf-8')) as {
PROJECTS: { name: string; priority?: string; [key: string]: unknown }[];
};
const before = data.PROJECTS.length;
data.PROJECTS = data.PROJECTS.filter((p) => p.priority !== '상시');
fs.writeFileSync(seedPath, JSON.stringify(data, null, 2), 'utf-8');
console.log(` ✓ hr-data.json: removed ${before - data.PROJECTS.length} legacy 상시 entries (${data.PROJECTS.length} projects left)`);
}
async function main() {
console.log('🧹 Delete all legacy routine tasks from DB ...');
await deleteLegacyRoutineTasks();
console.log('📝 Remove priority:상시 from hr-data.json ...');
patchHrData();
console.log('Done. 상시업무는 허브에서 대분류 클릭 시 새 셸로 생성됩니다.');
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -102,7 +102,7 @@ async function importViaApi(adminId: string, memberId: string) {
async function main() {
const tasks = mapAllHrProjects();
console.log(`📦 HR_Dashboard${tasks.length} tasks mapped`);
console.log(`📦 data/seed/hr-data.json${tasks.length} tasks mapped`);
let adminId: string;
let memberId: string;

View File

@@ -0,0 +1,46 @@
/**
* 4분면 보드 section 정렬 — 조직문화(EX) 프로젝트 재배치
* npx tsx scripts/migrate-board-sections.ts
*/
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const EX_TITLE = /회사생활|C\.E\.L|조직문화|복리후생|문화\s*진단|직원\s*소통/i;
async function main() {
const tasks = await prisma.task.findMany({
select: { id: true, title: true, section: true },
});
let moved = 0;
for (const task of tasks) {
if (task.section === '조직문화') continue;
if (!EX_TITLE.test(task.title.trim())) continue;
await prisma.task.update({
where: { id: task.id },
data: { section: '조직문화', category: '조직문화' },
});
moved += 1;
console.log(` → 조직문화: ${task.title}`);
}
const col = await prisma.columnConfig.findUnique({ where: { key: '운영관리' } });
if (col && (col.title === '운영관리' || col.title === '운영관리 부문' || col.titleEn === 'Operations')) {
await prisma.columnConfig.update({
where: { key: '운영관리' },
data: { title: '총무관리', titleEn: 'GA' },
});
console.log(' → column 운영관리 title → 총무관리');
}
console.log(`✅ migrate-board-sections complete (${moved} tasks moved)`);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,59 @@
import 'dotenv/config';
import { Prisma, PrismaClient } from '@prisma/client';
import { migrateDescriptionToPeriodEntries } from '../src/lib/milestonePeriods';
const prisma = new PrismaClient();
async function main() {
const milestones = await prisma.milestone.findMany({
where: {
description: { not: null },
NOT: { description: '' },
},
select: {
id: true,
title: true,
description: true,
startDate: true,
dueDate: true,
periodEntries: true,
},
});
let migrated = 0;
let skipped = 0;
for (const m of milestones) {
const { periodEntries, migrated: didMigrate } = migrateDescriptionToPeriodEntries({
id: m.id,
periodEntries: m.periodEntries,
startDate: m.startDate,
dueDate: m.dueDate,
description: m.description,
});
if (!didMigrate || !periodEntries) {
skipped += 1;
continue;
}
await prisma.milestone.update({
where: { id: m.id },
data: {
periodEntries: periodEntries as Prisma.InputJsonValue,
description: null,
},
});
migrated += 1;
console.log(`${m.title} → 기간1 note (${periodEntries.length}건)`);
}
console.log(`\nDone. migrated=${migrated}, skipped=${skipped}, scanned=${milestones.length}`);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,28 @@
/**
* 로컬 DB가 비어 있을 때만 seed 실행 (최초 1회용)
*/
import 'dotenv/config';
import { execSync } from 'child_process';
import path from 'path';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const users = await prisma.user.count();
if (users > 0) {
console.log('✓ Local DB has data — skip initial seed');
return;
}
console.log('📦 Empty local DB — loading initial sample data...');
const backendRoot = path.resolve(__dirname, '..');
execSync('tsx prisma/seed.ts', { stdio: 'inherit', cwd: backendRoot });
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,45 @@
/**
* 팀원이 0명일 때만 hr-data.json TEAM → team_members (업무는 건드리지 않음)
*/
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import { mapHrTeamMembers } from '../prisma/mapHrProjects';
const prisma = new PrismaClient();
async function main() {
const count = await prisma.teamMember.count();
if (count > 0) {
console.log('✓ Team members exist — skip TEAM seed');
return;
}
const teamParsed = mapHrTeamMembers();
if (teamParsed.length === 0) {
console.log('✓ hr-data.json TEAM empty — skip');
return;
}
for (const tm of teamParsed) {
await prisma.teamMember.create({
data: {
name: tm.name,
rank: tm.rank,
role: tm.role,
cell: tm.cell,
photoUrl: tm.photoUrl,
sortOrder: tm.sortOrder,
isActive: true,
},
});
}
console.log(`✅ Team members seeded: ${teamParsed.length}명 (hr-data.json TEAM)`);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,198 @@
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { PrismaClient } from '@prisma/client';
import { getProjectRoot, getUploadDir, getTeamUploadDir } from '../src/lib/projectPaths';
const prisma = new PrismaClient();
const TEAM_ORDER = ['조태희', '최근혜', '류원준', '주완기', '정성호'];
function fileHash(filePath: string): string {
const buf = fs.readFileSync(filePath);
return crypto.createHash('md5').update(buf).digest('hex');
}
function resolveUploadPath(relative: string): string {
const clean = relative.replace(/^\//, '').replace(/^uploads\//, '');
return path.join(getUploadDir(), clean);
}
async function syncTeamPhotos() {
const teamDir = getTeamUploadDir();
if (!fs.existsSync(teamDir)) return;
const pngs = fs
.readdirSync(teamDir)
.filter((f) => /\.(png|jpe?g|webp|gif)$/i.test(f))
.map((f) => path.join(teamDir, f));
const byHash = new Map<string, string>();
const duplicates: string[] = [];
for (const filePath of pngs) {
const hash = fileHash(filePath);
const existing = byHash.get(hash);
if (existing) {
const keep =
fs.statSync(existing).birthtimeMs <= fs.statSync(filePath).birthtimeMs
? existing
: filePath;
duplicates.push(keep === existing ? filePath : existing);
byHash.set(hash, keep);
} else {
byHash.set(hash, filePath);
}
}
for (const dup of duplicates) {
fs.unlinkSync(dup);
console.log(` 🗑 duplicate removed: ${path.basename(dup)}`);
}
const unique = [...byHash.values()].sort(
(a, b) => fs.statSync(a).birthtimeMs - fs.statSync(b).birthtimeMs,
);
const members = await prisma.teamMember.findMany();
const byName = new Map(members.map((m) => [m.name, m]));
for (let i = 0; i < TEAM_ORDER.length; i++) {
const name = TEAM_ORDER[i];
const member = byName.get(name);
if (!member) continue;
const photoPath = unique[i];
const photoUrl = photoPath
? `/uploads/team/${path.basename(photoPath)}`
: null;
await prisma.teamMember.update({
where: { id: member.id },
data: { photoUrl, sortOrder: i },
});
console.log(`${name}${photoUrl ?? '(none)'}`);
}
for (const member of members) {
if (TEAM_ORDER.includes(member.name)) continue;
const url = member.photoUrl;
if (!url) continue;
const abs = resolveUploadPath(url);
if (!fs.existsSync(abs)) {
await prisma.teamMember.update({
where: { id: member.id },
data: { photoUrl: null },
});
console.log(` ✓ cleared broken photo: ${member.name}`);
}
}
}
async function clearBrokenTeamPhotoUrls() {
const members = await prisma.teamMember.findMany();
for (const member of members) {
if (!member.photoUrl) continue;
if (member.photoUrl.startsWith('http') || member.photoUrl.startsWith('data:')) continue;
const abs = resolveUploadPath(member.photoUrl);
if (!fs.existsSync(abs)) {
await prisma.teamMember.update({
where: { id: member.id },
data: { photoUrl: null },
});
console.log(` ✓ cleared missing file: ${member.name} (${member.photoUrl})`);
}
}
}
async function pruneOrphanUploads() {
const referenced = new Set<string>();
const dbFiles = await prisma.file.findMany({ select: { path: true, filename: true } });
for (const f of dbFiles) {
if (f.path) referenced.add(path.normalize(f.path));
referenced.add(path.join(getUploadDir(), f.filename));
}
const members = await prisma.teamMember.findMany({ select: { photoUrl: true } });
for (const m of members) {
if (!m.photoUrl || m.photoUrl.startsWith('http')) continue;
referenced.add(resolveUploadPath(m.photoUrl));
}
const uploadRoot = getUploadDir();
const walk = (dir: string) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.name === '.gitkeep') continue;
if (entry.isDirectory()) {
walk(full);
continue;
}
const norm = path.normalize(full);
if (!referenced.has(norm)) {
fs.unlinkSync(full);
console.log(` 🗑 orphan: ${path.relative(getProjectRoot(), full)}`);
}
}
};
walk(uploadRoot);
}
async function patchHrDataTeamPhotos() {
const seedPath = path.join(getProjectRoot(), 'data', 'seed', 'hr-data.json');
if (!fs.existsSync(seedPath)) return;
const data = JSON.parse(fs.readFileSync(seedPath, 'utf-8')) as {
TEAM?: { name: string; photo?: string }[];
};
if (!Array.isArray(data.TEAM)) return;
const members = await prisma.teamMember.findMany();
const byName = new Map(members.map((m) => [m.name, m]));
let changed = false;
for (const entry of data.TEAM) {
const parsed = entry.name.match(/^(\S+)/);
const name = parsed?.[1];
const member = name ? byName.get(name) : null;
const nextPhoto = member?.photoUrl ?? undefined;
if (entry.photo !== nextPhoto) {
if (nextPhoto) entry.photo = nextPhoto;
else delete entry.photo;
changed = true;
}
}
if (changed) {
fs.writeFileSync(seedPath, JSON.stringify(data, null, 2), 'utf-8');
console.log(' ✓ hr-data.json TEAM photos synced to uploads/team paths');
}
}
async function main() {
console.log('📷 Sync team photos from uploads/team/ ...');
await syncTeamPhotos();
console.log('🔍 Clear broken photo URLs ...');
await clearBrokenTeamPhotoUrls();
console.log('🧹 Remove orphan uploads (not in DB) ...');
await pruneOrphanUploads();
console.log('📝 Update hr-data.json TEAM photo paths ...');
await patchHrDataTeamPhotos();
console.log('Done.');
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());