- schema: MathUnit enum (common/prob_stat/calculus/geometry) - User: focusUnits(Json), bojHandle(VarChar32) - ProblemSet: mathUnit optional + unique(year,examType,subjectName,mathUnit) - migration: phase10_math_units_and_ps - scripts: wipe-non-math (dry/apply), kice-import refactor (math only, 30문항을 공통22 + 확통/미적/기하 8씩으로 분할 → 2년×4 = 8 ProblemSet) - parsing strategy/ocr-fix/seed/me/study-logs: 비수학 참조 전면 제거 - data/kice, backend/uploads/problems: 비수학 폴더 삭제 - Plans.md: Phase 10 계획 확정 Phase 10A DoD 통과 — prisma validate/format OK, nest build OK, kice-import dry-run 2년 각 46문항 확인, wipe dry-run 8 set/260 problems 확인.
217 lines
6.3 KiB
TypeScript
217 lines
6.3 KiB
TypeScript
import * as fs from 'fs';
|
|
import { PrismaClient, Prisma } from '@prisma/client';
|
|
|
|
interface CliOptions {
|
|
dryRun: boolean;
|
|
backupPath?: string;
|
|
}
|
|
|
|
interface TableCounts {
|
|
problemSets: number;
|
|
problems: number;
|
|
passages: number;
|
|
studyLogs: number;
|
|
reviewSchedules: number;
|
|
assignments: number;
|
|
assignmentSubmissions: number;
|
|
}
|
|
|
|
interface ProblemSetStats extends TableCounts {
|
|
id: number;
|
|
year: number;
|
|
subjectName: string;
|
|
examType: string;
|
|
title: string;
|
|
problemIds: number[];
|
|
}
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
function parseArgs(): CliOptions {
|
|
const options: CliOptions = { dryRun: true };
|
|
for (const arg of process.argv.slice(2)) {
|
|
if (arg === '--apply') options.dryRun = false;
|
|
else if (arg === '--dry-run') options.dryRun = true;
|
|
else if (arg.startsWith('--backup-path=')) options.backupPath = arg.split('=')[1];
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function emptyCounts(): TableCounts {
|
|
return {
|
|
problemSets: 0,
|
|
problems: 0,
|
|
passages: 0,
|
|
studyLogs: 0,
|
|
reviewSchedules: 0,
|
|
assignments: 0,
|
|
assignmentSubmissions: 0,
|
|
};
|
|
}
|
|
|
|
function addCounts(target: TableCounts, delta: TableCounts): TableCounts {
|
|
target.problemSets += delta.problemSets;
|
|
target.problems += delta.problems;
|
|
target.passages += delta.passages;
|
|
target.studyLogs += delta.studyLogs;
|
|
target.reviewSchedules += delta.reviewSchedules;
|
|
target.assignments += delta.assignments;
|
|
target.assignmentSubmissions += delta.assignmentSubmissions;
|
|
return target;
|
|
}
|
|
|
|
async function loadProblemSetStats(): Promise<ProblemSetStats[]> {
|
|
const sets = await prisma.problemSet.findMany({
|
|
where: { subjectName: { not: '수학' } },
|
|
select: {
|
|
id: true,
|
|
year: true,
|
|
subjectName: true,
|
|
examType: true,
|
|
title: true,
|
|
problems: { select: { id: true } },
|
|
passages: { select: { id: true } },
|
|
assignments: { select: { id: true } },
|
|
},
|
|
});
|
|
|
|
const stats: ProblemSetStats[] = [];
|
|
for (const set of sets) {
|
|
const problemIds = set.problems.map((p) => p.id);
|
|
const assignmentIds = set.assignments.map((a) => a.id);
|
|
|
|
const [assignmentSubmissionCount, studyLogCount, reviewScheduleCount] = await Promise.all([
|
|
assignmentIds.length
|
|
? prisma.assignmentSubmission.count({ where: { assignmentId: { in: assignmentIds } } })
|
|
: Promise.resolve(0),
|
|
problemIds.length
|
|
? prisma.studyLog.count({ where: { problemId: { in: problemIds } } })
|
|
: Promise.resolve(0),
|
|
problemIds.length
|
|
? prisma.reviewSchedule.count({ where: { studyLog: { problemId: { in: problemIds } } } })
|
|
: Promise.resolve(0),
|
|
]);
|
|
|
|
stats.push({
|
|
...emptyCounts(),
|
|
id: set.id,
|
|
title: set.title,
|
|
year: set.year,
|
|
subjectName: set.subjectName,
|
|
examType: set.examType,
|
|
problemIds,
|
|
problems: problemIds.length,
|
|
passages: set.passages.length,
|
|
assignments: set.assignments.length,
|
|
assignmentSubmissions: assignmentSubmissionCount,
|
|
studyLogs: studyLogCount,
|
|
reviewSchedules: reviewScheduleCount,
|
|
problemSets: 1,
|
|
});
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
function printStats(stats: ProblemSetStats[]): void {
|
|
if (stats.length === 0) {
|
|
console.log('No non-math problem sets found.');
|
|
return;
|
|
}
|
|
|
|
const totals = emptyCounts();
|
|
console.log(`Target problem sets: ${stats.length}`);
|
|
for (const set of stats) {
|
|
addCounts(totals, set);
|
|
console.log(
|
|
`- ${set.year} ${set.subjectName} (${set.examType}, id=${set.id}): problems=${set.problems}, passages=${set.passages}, studyLogs=${set.studyLogs}, reviewSchedules=${set.reviewSchedules}, assignments=${set.assignments}, assignmentSubmissions=${set.assignmentSubmissions}`,
|
|
);
|
|
}
|
|
console.log('\nPlanned deletions (rows):');
|
|
console.log(JSON.stringify(totals, null, 2));
|
|
}
|
|
|
|
async function deleteProblemSet(
|
|
tx: Prisma.TransactionClient,
|
|
set: ProblemSetStats,
|
|
): Promise<TableCounts> {
|
|
const totals = emptyCounts();
|
|
if (set.problemIds.length > 0) {
|
|
const reviewCount = await tx.reviewSchedule.deleteMany({
|
|
where: { studyLog: { problemId: { in: set.problemIds } } },
|
|
});
|
|
totals.reviewSchedules += reviewCount.count;
|
|
const studyLogCount = await tx.studyLog.deleteMany({ where: { problemId: { in: set.problemIds } } });
|
|
totals.studyLogs += studyLogCount.count;
|
|
}
|
|
|
|
const assignmentSubmissionCount = await tx.assignmentSubmission.deleteMany({
|
|
where: { assignment: { problemSetId: set.id } },
|
|
});
|
|
totals.assignmentSubmissions += assignmentSubmissionCount.count;
|
|
|
|
const assignmentCount = await tx.assignment.deleteMany({ where: { problemSetId: set.id } });
|
|
totals.assignments += assignmentCount.count;
|
|
|
|
const passageCount = await tx.passage.deleteMany({ where: { problemSetId: set.id } });
|
|
totals.passages += passageCount.count;
|
|
|
|
if (set.problemIds.length > 0) {
|
|
const problemCount = await tx.problem.deleteMany({ where: { id: { in: set.problemIds } } });
|
|
totals.problems += problemCount.count;
|
|
}
|
|
|
|
await tx.problemSet.delete({ where: { id: set.id } });
|
|
totals.problemSets += 1;
|
|
|
|
return totals;
|
|
}
|
|
|
|
async function applyDeletes(stats: ProblemSetStats[]): Promise<TableCounts> {
|
|
const totals = emptyCounts();
|
|
if (stats.length === 0) return totals;
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
for (const set of stats) {
|
|
const deleted = await deleteProblemSet(tx, set);
|
|
addCounts(totals, deleted);
|
|
console.log(`Deleted ${set.year} ${set.subjectName} (id=${set.id})`);
|
|
}
|
|
});
|
|
return totals;
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs();
|
|
if (!options.dryRun) {
|
|
if (!options.backupPath) {
|
|
throw new Error('`--backup-path=<path>` is required when running with --apply');
|
|
}
|
|
if (!fs.existsSync(options.backupPath)) {
|
|
throw new Error(`Backup path not found: ${options.backupPath}`);
|
|
}
|
|
console.log(`Using backup file at ${options.backupPath}`);
|
|
} else if (options.backupPath) {
|
|
console.warn('`--backup-path` is ignored during dry-run');
|
|
}
|
|
|
|
const stats = await loadProblemSetStats();
|
|
printStats(stats);
|
|
|
|
if (options.dryRun) {
|
|
await prisma.$disconnect();
|
|
return;
|
|
}
|
|
|
|
const totals = await applyDeletes(stats);
|
|
console.log('\nDeletion results (rows removed):');
|
|
console.log(JSON.stringify(totals, null, 2));
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch(async (error) => {
|
|
console.error(error);
|
|
await prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|