Files
kolaytercih/src/lib/db.ts
bilalgursen cee6200237 feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle
- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası)
- iyzico ödeme entegrasyonu ve paket satın alma akışı
- kredi sistemi ve uygulama veritabanı şeması (drizzle)
- AI destekli rapor üretimi ve tercih sihirbazı
- rapor yazdırma sayfası ve site header/kullanıcı menüsü

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 02:13:39 +03:00

228 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from "node:path";
import Database from "better-sqlite3";
import { KATEGORILER, kategoriBul } from "./kategoriler";
export type Program = {
id: string;
isim: string;
universite: string;
unitur: string | null;
il: string | null;
fakulte: string | null;
tur: string;
sure: number | null;
sira2025: number | null;
sira2024: number | null;
sira2023: number | null;
sira2022: number | null;
sira2021: number | null;
puan2025: number | null;
kontenjan2025: number | null;
yerlesen2025: number | null;
};
export const PUAN_TURLERI = {
say: "SAYISAL",
ea: "EŞİT AĞIRLIK",
soz: "SÖZEL",
dil: "DİL",
tyt: "TYT",
} as const;
export type PuanTuruKey = keyof typeof PUAN_TURLERI;
export type RankResults = {
hayal: Program[];
dengeli: Program[];
garanti: Program[];
};
// Dev'de hot-reload başına yeni bağlantıılmasın diye global cache
const globalForDb = globalThis as unknown as { yokatlasDb?: Database.Database };
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;
}
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
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).
*/
export type UniturGrubu = "devlet" | "vakif";
// Vakıf sayılan unitur değerleri (bkz. DISTINCT unitur: VAKIF, VAKIF MYO,
// YURTDISI VAKIF). Devlet dışı her şey "vakıf" grubuna girer.
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: [] };
}
export function searchByRank(
sira: number,
turKey: PuanTuruKey,
opts: {
il?: string;
iller?: string[];
uniturGrubu?: UniturGrubu;
limitPerBucket?: number;
} = {}
): RankResults {
const db = getDb();
const tur = PUAN_TURLERI[turKey];
const onlisans = turKey === "tyt" ? 1 : 0;
const limitPerBucket = opts.limitPerBucket ?? 12;
// 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 ilArgs = [...illerListe, ...unitur.args];
const base = `FROM programs
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql}
AND ${EFEKTIF_SIRA} IS NOT NULL
AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`;
// hayal: kullanıcıdan daha iyi taban sıralaması (sınıra en yakın olanlar önce)
const hayal = db
.prepare(
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} DESC LIMIT ?`
)
.all(
tur,
onlisans,
...ilArgs,
Math.round(sira * 0.5),
sira - 1,
limitPerBucket
);
// dengeli: tabanı kullanıcının sıralaması civarında
const dengeli = db
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
.all(
tur,
onlisans,
...ilArgs,
sira,
Math.round(sira * 1.4),
limitPerBucket
);
// garanti: tabanı belirgin şekilde altında
const garanti = db
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
.all(
tur,
onlisans,
...ilArgs,
Math.round(sira * 1.4) + 1,
Math.round(sira * 3),
limitPerBucket
);
return {
hayal: hayal as Program[],
dengeli: dengeli as Program[],
garanti: garanti as Program[],
};
}
export type SihirbazFacetleri = {
kategoriler: { ad: string; adet: number }[];
iller: { il: string; adet: number }[];
uniturler: { grup: UniturGrubu; adet: number }[];
};
// Sihirbaz bubble'ları için erişilebilir program penceresi: hayal alt sınırından
// (sira*0.5) garanti üst sınırına (sira*3) kadar. Böylece 500B sıradaki aday Tıp
// bubble'ı görmez; 5B sıradaki görür.
export function rankWindowFacets(
sira: number,
turKey: PuanTuruKey
): SihirbazFacetleri {
const db = getDb();
const tur = PUAN_TURLERI[turKey];
const onlisans = turKey === "tyt" ? 1 : 0;
const altSinir = Math.round(sira * 0.5);
const ustSinir = Math.round(sira * 3);
const base = `FROM programs
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`;
// isim bazında say → JS'te kategoriye çevir (bir isim çok kategoriye girebilir)
const isimSayilari = db
.prepare(`SELECT isim, COUNT(*) AS adet ${base} GROUP BY isim`)
.all(tur, onlisans, altSinir, ustSinir) as {
isim: string;
adet: number;
}[];
const kategoriSay = new Map<string, number>();
for (const { isim, adet } of isimSayilari) {
for (const k of kategoriBul(isim)) {
kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + adet);
}
}
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 = (
db
.prepare(
`SELECT il, COUNT(*) AS adet ${base} AND il IS NOT NULL AND il <> ''
GROUP BY il ORDER BY adet DESC LIMIT 15`
)
.all(tur, onlisans, altSinir, ustSinir) as {
il: string;
adet: number;
}[]
).filter((r) => r.adet > 0);
const uniturSatirlari = db
.prepare(`SELECT unitur, COUNT(*) AS adet ${base} GROUP BY unitur`)
.all(tur, onlisans, altSinir, ustSinir) as {
unitur: string | null;
adet: number;
}[];
let devlet = 0;
let vakif = 0;
for (const { unitur, adet } of uniturSatirlari) {
if (unitur === "DEVLET") devlet += adet;
else if (unitur) vakif += adet;
}
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 };
}