Files
reloop-v2/backend/scripts/wipe-non-math.ts
reloop d892d8298b feat(phase10A): KICE 비수학 제거 + 수학 단원 분리
- 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 확인.
2026-04-14 18:27:00 +09:00

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