EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
75
backend/scripts/cleanup-legacy-routine.ts
Normal file
75
backend/scripts/cleanup-legacy-routine.ts
Normal 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());
|
||||
@@ -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;
|
||||
|
||||
46
backend/scripts/migrate-board-sections.ts
Normal file
46
backend/scripts/migrate-board-sections.ts
Normal 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());
|
||||
59
backend/scripts/migrate-milestone-period-notes.ts
Normal file
59
backend/scripts/migrate-milestone-period-notes.ts
Normal 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());
|
||||
28
backend/scripts/seed-if-empty.ts
Normal file
28
backend/scripts/seed-if-empty.ts
Normal 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());
|
||||
45
backend/scripts/seed-team-if-empty.ts
Normal file
45
backend/scripts/seed-team-if-empty.ts
Normal 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());
|
||||
198
backend/scripts/sync-team-photos.ts
Normal file
198
backend/scripts/sync-team-photos.ts
Normal 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());
|
||||
Reference in New Issue
Block a user