- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/ SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu fonksiyonları barındırıyor - RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/ rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi - 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client, ai/cagri, credits) bilinçli eklenmedi - Davranış değişikliği yok; pnpm build yeşil Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
265 lines
9.0 KiB
TypeScript
265 lines
9.0 KiB
TypeScript
// server-only guard'ı bilinçli yok: scripts/tadimlik-uret.ts bu modülü düz
|
||
// node (tsx) altında import ediyor; server-only paketi orada fırlar.
|
||
import path from "node:path";
|
||
import Database from "better-sqlite3";
|
||
import { KATEGORILER, kategoriBul } from "./kategoriler";
|
||
import {
|
||
PUAN_TURLERI,
|
||
type DilimKey,
|
||
type Program,
|
||
type ProgramNetSatiri,
|
||
type PuanTuruKey,
|
||
type RankResults,
|
||
type SihirbazFacetleri,
|
||
type UniturGrubu,
|
||
} from "@/types/yokatlas";
|
||
|
||
// Dev'de hot-reload başına yeni bağlantı açılmasın diye global cache
|
||
const globalForDb = globalThis as unknown as { yokatlasDb?: Database.Database };
|
||
|
||
export function getDb(): Database.Database {
|
||
if (!globalForDb.yokatlasDb) {
|
||
globalForDb.yokatlasDb = new Database(
|
||
path.join(process.cwd(), "data", "yokatlas.db"),
|
||
{ readonly: true, fileMustExist: true }
|
||
);
|
||
}
|
||
return globalForDb.yokatlasDb;
|
||
}
|
||
|
||
export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure,
|
||
sira2025, sira2024, sira2023, sira2022, sira2021,
|
||
puan2025, kontenjan2025, yerlesen2025`;
|
||
|
||
// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer
|
||
export const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)";
|
||
|
||
/**
|
||
* Kullanıcının başarı sıralamasına göre programları üç dilime ayırır.
|
||
* Sıralamada küçük sayı daha iyidir; program taban sıralaması kullanıcının
|
||
* sıralamasından küçükse geçen yıl oraya yerleşmek daha iyi dereceyle mümkündü
|
||
* demektir (hayal), büyükse daha güvenli demektir (garanti).
|
||
*/
|
||
function uniturFiltreSql(grup?: UniturGrubu): { sql: string; args: string[] } {
|
||
if (grup === "devlet") return { sql: "AND unitur = ?", args: ["DEVLET"] };
|
||
if (grup === "vakif")
|
||
return { sql: "AND unitur IS NOT NULL AND unitur <> ?", args: ["DEVLET"] };
|
||
return { sql: "", args: [] };
|
||
}
|
||
|
||
/**
|
||
* Dilim sınırları. Pencereler çarpan bazlı DEĞİL, açık uçlu: hayal adayın
|
||
* üstündeki her şeyi, garanti 1.4× eşiğinin altındaki her şeyi kapsar.
|
||
* "Yakından uzağa" sıralama + limit/offset sayesinde uçtaki gerçekçi olmayan
|
||
* programlar ancak sayfalandıkça görünür; eski ×0.5/×3 pencereleri küçük
|
||
* sıralamalarda (ör. 87) hiç sonuç bırakmıyordu.
|
||
*/
|
||
export function dilimAraligi(
|
||
sira: number,
|
||
dilim: DilimKey
|
||
): { alt: number; ust: number | null } {
|
||
const dengeliUst = Math.round(sira * 1.4);
|
||
if (dilim === "hayal") return { alt: 1, ust: sira - 1 };
|
||
if (dilim === "dengeli") return { alt: sira, ust: dengeliUst };
|
||
return { alt: dengeliUst + 1, ust: null };
|
||
}
|
||
|
||
/**
|
||
* Tek dilimi "yakından uzağa" sıralı ve sayfalı getirir: hayal adayın
|
||
* sıralamasından yukarı (taban DESC), dengeli/garanti aşağı (taban ASC).
|
||
* toplam, limit'ten bağımsız olarak dilimdeki tüm eşleşme sayısıdır.
|
||
*/
|
||
export function dilimAra(
|
||
sira: number,
|
||
turKey: PuanTuruKey,
|
||
dilim: DilimKey,
|
||
opts: {
|
||
il?: string;
|
||
iller?: string[];
|
||
uniturGrubu?: UniturGrubu;
|
||
limit?: number;
|
||
offset?: number;
|
||
} = {}
|
||
): { programlar: Program[]; toplam: number } {
|
||
const db = getDb();
|
||
const tur = PUAN_TURLERI[turKey];
|
||
const onlisans = turKey === "tyt" ? 1 : 0;
|
||
const limit = opts.limit ?? 12;
|
||
const offset = opts.offset ?? 0;
|
||
|
||
// il (tekil, geriye uyumlu) veya iller (çoklu) — ikisi de verilirse birleşir
|
||
const illerListe = [
|
||
...(opts.il ? [opts.il] : []),
|
||
...(opts.iller ?? []),
|
||
].filter((v, i, a) => v && a.indexOf(v) === i);
|
||
const ilFiltre =
|
||
illerListe.length > 0
|
||
? `AND il IN (${illerListe.map(() => "?").join(",")})`
|
||
: "";
|
||
const unitur = uniturFiltreSql(opts.uniturGrubu);
|
||
|
||
const { alt, ust } = dilimAraligi(sira, dilim);
|
||
if (ust != null && ust < alt) return { programlar: [], toplam: 0 };
|
||
const siraKosulu =
|
||
ust != null ? `${EFEKTIF_SIRA} BETWEEN ? AND ?` : `${EFEKTIF_SIRA} >= ?`;
|
||
const siraArgs = ust != null ? [alt, ust] : [alt];
|
||
const args = [tur, onlisans, ...illerListe, ...unitur.args, ...siraArgs];
|
||
|
||
const base = `FROM programs
|
||
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql}
|
||
AND ${EFEKTIF_SIRA} IS NOT NULL
|
||
AND ${siraKosulu}`;
|
||
// hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa
|
||
const yon = dilim === "hayal" ? "DESC" : "ASC";
|
||
|
||
const programlar = db
|
||
.prepare(
|
||
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?`
|
||
)
|
||
.all(...args, limit, offset) as Program[];
|
||
const toplam = (
|
||
db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as {
|
||
adet: number;
|
||
}
|
||
).adet;
|
||
|
||
return { programlar, toplam };
|
||
}
|
||
|
||
export function searchByRank(
|
||
sira: number,
|
||
turKey: PuanTuruKey,
|
||
opts: {
|
||
il?: string;
|
||
iller?: string[];
|
||
uniturGrubu?: UniturGrubu;
|
||
limitPerBucket?: number;
|
||
} = {}
|
||
): RankResults {
|
||
const ortak = {
|
||
il: opts.il,
|
||
iller: opts.iller,
|
||
uniturGrubu: opts.uniturGrubu,
|
||
limit: opts.limitPerBucket ?? 12,
|
||
};
|
||
const hayal = dilimAra(sira, turKey, "hayal", ortak);
|
||
const dengeli = dilimAra(sira, turKey, "dengeli", ortak);
|
||
const garanti = dilimAra(sira, turKey, "garanti", ortak);
|
||
|
||
return {
|
||
hayal: hayal.programlar,
|
||
dengeli: dengeli.programlar,
|
||
garanti: garanti.programlar,
|
||
toplam: {
|
||
hayal: hayal.toplam,
|
||
dengeli: dengeli.toplam,
|
||
garanti: garanti.toplam,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Programa yerleşen son kişinin yıl bazlı netleri, yeniden eskiye sıralı.
|
||
* Veri scripts/detay.ts ile YÖK Atlas /api/netler/search'ten doldurulur.
|
||
*/
|
||
export function netlerGetir(programId: string): ProgramNetSatiri[] {
|
||
return getDb()
|
||
.prepare(
|
||
`SELECT yil, puanTuru, tabanPuan, obp,
|
||
tytTrkNet, tytSosNet, tytMatNet, tytFenNet,
|
||
aytMatNet, aytFizNet, aytKimNet, aytBioNet,
|
||
aytTdeNet, aytTrh1Net, aytCog1Net, aytTrh2Net, aytCog2Net,
|
||
aytDinNet, aytFelNet, ydtYdilNet
|
||
FROM netler WHERE program_id = ? ORDER BY yil DESC`
|
||
)
|
||
.all(programId) as ProgramNetSatiri[];
|
||
}
|
||
|
||
// Sihirbaz bubble'ları için erişilebilir program penceresi: hayal için
|
||
// gerçekçi alt sınırdan (sira*0.5) itibaren AÇIK uçlu — garanti tarafı artık
|
||
// sınırsız olduğundan üst kesim yok. Böylece 500B sıradaki aday Tıp bubble'ı
|
||
// görmez; 5B sıradaki görür.
|
||
//
|
||
// Kademeli (cascade) filtre: adımlar birbirini süzer. Kategoriler yalnızca
|
||
// pencereden; iller seçili kategorilerden; üniversite tipi kategori+il'den
|
||
// hesaplanır. Böylece "Mühendislik + İzmir" seçen aday 3. adımda yalnızca
|
||
// İzmir'deki mühendislik programlarının devlet/vakıf dağılımını görür.
|
||
export function rankWindowFacets(
|
||
sira: number,
|
||
turKey: PuanTuruKey,
|
||
secim: { kategoriler?: string[]; iller?: string[] } = {}
|
||
): SihirbazFacetleri {
|
||
const db = getDb();
|
||
const tur = PUAN_TURLERI[turKey];
|
||
const onlisans = turKey === "tyt" ? 1 : 0;
|
||
const altSinir = Math.round(sira * 0.5);
|
||
|
||
const satirlar = db
|
||
.prepare(
|
||
`SELECT isim, il, unitur FROM programs
|
||
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
|
||
AND ${EFEKTIF_SIRA} >= ?`
|
||
)
|
||
.all(tur, onlisans, altSinir) as {
|
||
isim: string;
|
||
il: string | null;
|
||
unitur: string | null;
|
||
}[];
|
||
|
||
// isim → kategori eşlemesi pahalı (12 regex); tekrar eden isimler için önbellek
|
||
const kategoriCache = new Map<string, string[]>();
|
||
const kategorileriGetir = (isim: string): string[] => {
|
||
let k = kategoriCache.get(isim);
|
||
if (!k) {
|
||
k = kategoriBul(isim);
|
||
kategoriCache.set(isim, k);
|
||
}
|
||
return k;
|
||
};
|
||
|
||
const seciliKategoriler = secim.kategoriler ?? [];
|
||
const kategoriUyar = (isim: string) =>
|
||
seciliKategoriler.length === 0 ||
|
||
kategorileriGetir(isim).some((k) => seciliKategoriler.includes(k));
|
||
const seciliIller = new Set(secim.iller ?? []);
|
||
const ilUyar = (il: string | null) =>
|
||
seciliIller.size === 0 || (il != null && seciliIller.has(il));
|
||
|
||
const kategoriSay = new Map<string, number>();
|
||
const ilSay = new Map<string, number>();
|
||
let devlet = 0;
|
||
let vakif = 0;
|
||
|
||
for (const { isim, il, unitur } of satirlar) {
|
||
// 1. adım: kategoriler yalnızca pencereye bağlı (filtre uygulanmaz)
|
||
for (const k of kategorileriGetir(isim)) {
|
||
kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + 1);
|
||
}
|
||
if (!kategoriUyar(isim)) continue;
|
||
// 2. adım: iller seçili kategorilerle süzülür
|
||
if (il) ilSay.set(il, (ilSay.get(il) ?? 0) + 1);
|
||
if (!ilUyar(il)) continue;
|
||
// 3. adım: üniversite tipi kategori + il ile süzülür
|
||
if (unitur === "DEVLET") devlet += 1;
|
||
else if (unitur) vakif += 1;
|
||
}
|
||
|
||
const kategoriler = KATEGORILER.map((ad) => ({
|
||
ad,
|
||
adet: kategoriSay.get(ad) ?? 0,
|
||
}))
|
||
.filter((k) => k.adet >= 3) // gürültüyü ele
|
||
.sort((a, b) => b.adet - a.adet);
|
||
|
||
const iller = [...ilSay.entries()]
|
||
.map(([il, adet]) => ({ il, adet }))
|
||
.sort((a, b) => b.adet - a.adet)
|
||
.slice(0, 15);
|
||
|
||
const uniturler: { grup: UniturGrubu; adet: number }[] = [];
|
||
if (devlet > 0) uniturler.push({ grup: "devlet", adet: devlet });
|
||
if (vakif > 0) uniturler.push({ grup: "vakif", adet: vakif });
|
||
|
||
return { kategoriler, iller, uniturler, toplam: satirlar.length };
|
||
}
|