199 lines
5.7 KiB
TypeScript
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());
|