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