Files
eene_dashboard/backend/scripts/sync-team-photos.ts
EENE Dashboard b3f2da203b EENE Dashboard upload to Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:59:34 +09:00

199 lines
5.7 KiB
TypeScript

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());