- 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>
228 lines
6.6 KiB
TypeScript
228 lines
6.6 KiB
TypeScript
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ı açı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 };
|
||
}
|