Files
kolaytercih/src/lib/rapor-havuzu.ts
bilalgursen be4b2c6000
All checks were successful
Deploy / deploy (push) Successful in 15m6s
refactor: update Dockerfile and authentication flow for improved user experience
- Updated Dockerfile to create a cache directory for Next.js runtime, ensuring proper permissions and ownership.
- Enhanced the authentication flow by updating the magic link expiration time from 5 to 15 minutes, improving user experience and reducing login issues.
- Added user-friendly error handling for invalid or expired magic links, providing clear feedback on the login page.
- Adjusted email templates to reflect the new 15-minute validity period for magic links.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 02:05:14 +03:00

194 lines
5.9 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 {
searchByRank,
type Program,
type PuanTuruKey,
type UniturGrubu,
PUAN_TURLERI,
} from "./db";
import { kategoriEslesir } from "./kategoriler";
export type Dilim = "hayal" | "dengeli" | "garanti";
export interface AdayProgram extends Program {
dilim: Dilim;
}
export interface RaporParams {
sira: number;
tur: PuanTuruKey;
// Sihirbaz seçimleri (yeni akış)
kategoriler?: string[];
iller?: string[];
universiteTipi?: UniturGrubu | "farketmez";
oncelikler?: string[];
// Eski akış alanları (geriye uyum — eski kayıtlı raporlar)
il?: string;
notlar?: string;
}
export interface HavuzSonuc {
havuz: AdayProgram[];
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi
gevsetildi: ("kategori" | "il")[];
}
const HEDEF = 24;
const LIMIT = 60;
export type DilimHedef = Record<Dilim, number>;
// Listenin ideal iskeleti: 5 hayal / 13 dengeli / 6 garanti = 24 tercih.
export const IDEAL_HEDEF: DilimHedef = { hayal: 5, dengeli: 13, garanti: 6 };
/**
* Havuzun gerçek dilim dağılımına uyarlanmış liste hedefi. İdeal 5/13/6'dan
* başlar; bir dilimde yeterli aday yoksa eksik, kapasitesi kalan dilimlere
* dağıtılır (öncelik: dengeli → garanti → hayal — omurga önce, sigorta sonra,
* en riskli dilim en son). Havuz 24'ün altındaysa hedef havuzun tamamıdır.
* Deterministiktir: aynı havuz her zaman aynı hedefi üretir.
*/
export function dilimHedefiHesapla(havuz: AdayProgram[]): DilimHedef {
const mevcut: DilimHedef = { hayal: 0, dengeli: 0, garanti: 0 };
for (const p of havuz) mevcut[p.dilim]++;
const toplamHedef = Math.min(HEDEF, havuz.length);
const hedef: DilimHedef = {
hayal: Math.min(IDEAL_HEDEF.hayal, mevcut.hayal),
dengeli: Math.min(IDEAL_HEDEF.dengeli, mevcut.dengeli),
garanti: Math.min(IDEAL_HEDEF.garanti, mevcut.garanti),
};
const oncelik: Dilim[] = ["dengeli", "garanti", "hayal"];
let eksik = toplamHedef - hedef.hayal - hedef.dengeli - hedef.garanti;
while (eksik > 0) {
let dagitildi = false;
for (const d of oncelik) {
if (eksik <= 0) break;
if (mevcut[d] > hedef[d]) {
hedef[d]++;
eksik--;
dagitildi = true;
}
}
if (!dagitildi) break;
}
return hedef;
}
// Prompt'a giden nihai havuz: dilim başına aday sayısı. Arama LIMIT ile geniş
// yapılır (filtre/gevşetme zenginliği için), filtre sonrası buraya kırpılır —
// 20×3=60 program ≈ 8k token; 180 program 24k token tutuyor ve lokal/ücretsiz
// modellerde prefill + isabet maliyeti yüksek (ölçüm: 6 Ağu 2026).
const PROMPT_LIMIT_DILIM = 20;
/** Dilim sırasını koruyarak her dilimden en fazla n aday bırakır. */
function dilimBasinaKirp(havuz: AdayProgram[], n: number): AdayProgram[] {
const sayac: Record<Dilim, number> = { hayal: 0, dengeli: 0, garanti: 0 };
return havuz.filter((p) => ++sayac[p.dilim] <= n);
}
function dilimEkle(r: {
hayal: Program[];
dengeli: Program[];
garanti: Program[];
}): AdayProgram[] {
return [
...r.hayal.map((p) => ({ ...p, dilim: "hayal" as const })),
...r.dengeli.map((p) => ({ ...p, dilim: "dengeli" as const })),
...r.garanti.map((p) => ({ ...p, dilim: "garanti" as const })),
];
}
/**
* Deterministik aday havuzu: Claude bu havuzdan SEÇER, asla uydurmaz.
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) göre filtreler; havuz
* 24'ün altına düşerse kademeli gevşetir (önce il, sonra kategori) ki liste
* her zaman kurulabilsin.
*/
export function havuzOlustur(params: RaporParams): HavuzSonuc {
const uniturGrubu =
params.universiteTipi && params.universiteTipi !== "farketmez"
? params.universiteTipi
: undefined;
const kategoriler = params.kategoriler ?? [];
const iller = [
...(params.il ? [params.il] : []),
...(params.iller ?? []),
].filter((v, i, a) => v && a.indexOf(v) === i);
const kategoriFiltre = (havuz: AdayProgram[]) =>
kategoriler.length > 0
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler))
: havuz;
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
const tamHam = dilimEkle(
searchByRank(params.sira, params.tur, {
iller: iller.length > 0 ? iller : undefined,
uniturGrubu,
limitPerBucket: LIMIT,
}),
);
const tamFiltreli = kategoriFiltre(tamHam);
if (tamFiltreli.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM),
gevsetildi: [],
};
}
// 2. Kategori gevşet: aynı illerdeki kategori-dışı programlarla doldur
if (kategoriler.length > 0 && tamHam.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
gevsetildi: ["kategori"],
};
}
// 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
const gevsetildi: ("kategori" | "il")[] = [];
if (iller.length > 0) gevsetildi.push("il");
const ilsizHam = dilimEkle(
searchByRank(params.sira, params.tur, {
uniturGrubu,
limitPerBucket: LIMIT,
}),
);
const ilsizFiltreli = kategoriFiltre(ilsizHam);
if (ilsizFiltreli.length >= HEDEF) {
return {
havuz: dilimBasinaKirp(ilsizFiltreli, PROMPT_LIMIT_DILIM),
gevsetildi,
};
}
// 4. Hem il hem kategori gevşet: elde ne varsa
if (kategoriler.length > 0) gevsetildi.push("kategori");
return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi };
}
/** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */
export function havuzuKompaktJson(havuz: AdayProgram[]): string {
return JSON.stringify(
havuz.map((p) => ({
id: p.id,
isim: p.isim,
uni: p.universite,
unitur: p.unitur,
il: p.il,
dilim: p.dilim,
sira: {
y2025: p.sira2025,
y2024: p.sira2024,
y2023: p.sira2023,
y2022: p.sira2022,
},
kontenjan2025: p.kontenjan2025,
yerlesen2025: p.yerlesen2025,
})),
);
}
export function turLabel(tur: PuanTuruKey): string {
return PUAN_TURLERI[tur];
}