From ce40b7c5b3158cdb105dce7ea25518fb195aaebf Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 17:00:39 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat(veri):=20Program=20tipi=20y=C4=B1ls?= =?UTF-8?q?=C4=B1z=20=E2=80=94=20db=20e=C5=9Fleyicisi,=20lib=20ve=20snapsh?= =?UTF-8?q?ot=20ilkYil=20(P-B=20a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Program artık siraGecmisi (YILLAR ile hizalı) + efektifSira + siraSon/puanSon/ kontenjanSon/yerlesenSon taşır. db.ts kolonları YILLAR'dan üretip takma adla seçer, programSatiri eşleyicisi ham satırı Program'a çevirir; SELECT_COLS'un üç tüketicisi de eşleyiciden geçer. EFEKTIF_SIRA veri-yillari'dan gelir. - trend: ILK_YIL tek kaynaktan, opsiyonel ilkYil parametresi - rapor snapshot'ına opsiyonel ilkYil yazılır (eski kayıtlar: okurken ILK_YIL) - rapor promptu son 4 yılı göndermeye devam eder, anahtarlar dinamik - tadımlık "son N yıl" filtresi genel: N != VERI_YIL_SAYISI ise servis edilmez - veri-kapsami VERI_YIL_SAYISI'nı veri-yillari'dan türetir Bileşenler bir sonraki commit'te yeni tipe geçer (ikisi birlikte derlenir). Co-Authored-By: Claude Fable 5.1 --- .../liste/hooks/use-tercih-profili.ts | 3 +- src/features/liste/liste-queries.ts | 14 +++-- src/features/liste/liste-store.ts | 9 ++-- src/features/rapor/types/rapor.ts | 10 +++- src/lib/ai/rapor.ts | 25 +++++---- src/lib/db.ts | 49 ++++++++++++++--- src/lib/katalog.ts | 54 +++++++++++-------- src/lib/rapor-havuzu.ts | 26 ++++++--- src/lib/rapor-maske.ts | 2 +- src/lib/risk.ts | 10 ++-- src/lib/tadimlik-havuzu.ts | 42 ++++++++++++--- src/lib/trend.ts | 23 ++++---- src/lib/veri-kapsami.ts | 8 +-- src/types/yokatlas.ts | 26 +++++---- 14 files changed, 195 insertions(+), 106 deletions(-) diff --git a/src/features/liste/hooks/use-tercih-profili.ts b/src/features/liste/hooks/use-tercih-profili.ts index f76fb90..3771ac7 100644 --- a/src/features/liste/hooks/use-tercih-profili.ts +++ b/src/features/liste/hooks/use-tercih-profili.ts @@ -28,8 +28,7 @@ export type BekleyenProgram = { | "il" | "unitur" | "tur" - | "sira2025" - | "sira2024" + | "efektifSira" >; kaynak: ManuelKaynak; adaySira?: number; diff --git a/src/features/liste/liste-queries.ts b/src/features/liste/liste-queries.ts index 569f366..d716a2e 100644 --- a/src/features/liste/liste-queries.ts +++ b/src/features/liste/liste-queries.ts @@ -8,7 +8,8 @@ import type { UniturGrubu, } from "@/types/yokatlas"; -// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin. +// Tabloda kullanılmayan alanlar (fakulte, puanSon, siraSon) client'a +// serileşmesin. // Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri // üzerinden derleyici burada da karar vermeye zorlar. export function programOzetle(p: Program): ProgramListeSatiri { @@ -20,13 +21,10 @@ export function programOzetle(p: Program): ProgramListeSatiri { il: p.il, tur: p.tur, sure: p.sure, - sira2025: p.sira2025, - sira2024: p.sira2024, - sira2023: p.sira2023, - sira2022: p.sira2022, - sira2021: p.sira2021, - kontenjan2025: p.kontenjan2025, - yerlesen2025: p.yerlesen2025, + siraGecmisi: p.siraGecmisi, + efektifSira: p.efektifSira, + kontenjanSon: p.kontenjanSon, + yerlesenSon: p.yerlesenSon, }; } diff --git a/src/features/liste/liste-store.ts b/src/features/liste/liste-store.ts index 0216cd3..0b30f43 100644 --- a/src/features/liste/liste-store.ts +++ b/src/features/liste/liste-store.ts @@ -158,10 +158,8 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[] return liste; } -export function efektifSiraAl( - p: Pick, -): number | null { - return p.sira2025 ?? p.sira2024 ?? null; +export function efektifSiraAl(p: Pick): number | null { + return p.efektifSira ?? null; } /** Program satırını manuel liste kaydına çevirir. */ @@ -174,8 +172,7 @@ export function programdanManuelTercih( | "il" | "unitur" | "tur" - | "sira2025" - | "sira2024" + | "efektifSira" >, adaySira?: number | null, kaynak: ManuelKaynak = "sonuc", diff --git a/src/features/rapor/types/rapor.ts b/src/features/rapor/types/rapor.ts index 85045e3..385348d 100644 --- a/src/features/rapor/types/rapor.ts +++ b/src/features/rapor/types/rapor.ts @@ -34,8 +34,13 @@ export type RaporSonuc = { // ayıklamak için (bkz. features/rapor/rapor-queries.ts) devMock?: boolean; // Render için havuzdan zenginleştirilmiş program bilgisi - // efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok - // siraGecmisi: [2021..2025] taban sıralamaları — deterministik trend oku + // efektifSira: üretim anındaki son yılın sırası, yoksa bir önceki yılınki — + // eski kayıtlı raporlarda yok + // siraGecmisi: [ilkYil..] taban sıralamaları — deterministik trend oku. + // Uzunluk üretim anındaki veri penceresidir (eski kayıtlarda 5 eleman); + // okuyan taraf diziyi doldurmaz, yılı `ilkYil + indeks` ile bulur. + // ilkYil: dizinin ilk elemanının yılı — eski kayıtlarda yok (okurken + // lib/veri-yillari ILK_YIL) programlar: Record< string, { @@ -45,6 +50,7 @@ export type RaporSonuc = { unitur: string | null; efektifSira?: number | null; siraGecmisi?: (number | null)[]; + ilkYil?: number; } >; }; diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 708fbc9..13b6adf 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -26,6 +26,7 @@ import { riskNotuMetni, } from "../risk"; import { trendOzetiMetni } from "../trend"; +import { ILK_YIL } from "../veri-yillari"; // Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu // ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti @@ -141,29 +142,27 @@ function normalizeEt( function trendOzetiUret(p: AdayProgram): string { // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts) - return trendOzetiMetni([ - p.sira2021, - p.sira2022, - p.sira2023, - p.sira2024, - p.sira2025, - ]); + return trendOzetiMetni(p.siraGecmisi); } function riskNotuUret(p: AdayProgram, adaySira: number): string { - return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira)); + return riskNotuMetni(riskHesapla(p.efektifSira, adaySira)); } /** Havuz programını render için gereken bilgiye indirger. */ function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] { - const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; + const gecmis = p.siraGecmisi; + const gecmisVar = gecmis.some((s) => s != null); return { isim: p.isim, universite: p.universite, il: p.il, unitur: p.unitur, - efektifSira: p.sira2025 ?? p.sira2024, - siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined, + efektifSira: p.efektifSira, + siraGecmisi: gecmisVar ? gecmis : undefined, + // Dizinin çapa yılı snapshot'a yazılır: kayıt, veri penceresi sonradan + // uzasa da kendi yıllarını bilir (eski kayıtlarda yok → okurken ILK_YIL). + ilkYil: gecmisVar ? ILK_YIL : undefined, }; } @@ -183,7 +182,7 @@ function mockRapor( // Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder const hedef = dilimHedefiHesapla(havuz); const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti; - const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0; + const efektif = (p: AdayProgram) => p.efektifSira ?? 0; const dilimden = (dilim: AdayProgram["dilim"], adet: number) => havuz .filter((p) => p.dilim === dilim) @@ -293,7 +292,7 @@ export async function raporUret( const havuzMap = new Map(havuz.map((p) => [p.id, p])); const efektif = (id: string) => { const p = havuzMap.get(id)!; - return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER; + return p.efektifSira ?? Number.MAX_SAFE_INTEGER; }; let secim: Record | null = null; diff --git a/src/lib/db.ts b/src/lib/db.ts index b6195d1..704b7c0 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -3,6 +3,7 @@ import path from "node:path"; import Database from "better-sqlite3"; import { KATEGORILER, kategoriBul } from "./kategoriler"; +import { EFEKTIF_SIRA_SQL, SON_YIL, YILLAR } from "./veri-yillari"; import { PUAN_TURLERI, type DilimKey, @@ -27,12 +28,46 @@ export function getDb(): Database.Database { return globalForDb.yokatlasDb; } +// Yıl ekli kolonlar yalnız burada, `YILLAR`'dan üretilerek adlandırılır; DB +// dışına yılsız `Program` çıkar. SELECT_COLS ile okunan her satır +// `programSatiri` eşleyicisinden geçmelidir (ham satır `Program` değildir). export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure, - sira2025, sira2024, sira2023, sira2022, sira2021, - puan2025, kontenjan2025, yerlesen2025`; + ${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")}, + puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon`; -// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer -export const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)"; +// Son yılın sıralaması yoksa (az yerleşen/yeni program) bir önceki yıla düşer. +// İfade indeks sözleşmesidir; tek kaynağı lib/veri-yillari.ts. +export const EFEKTIF_SIRA = EFEKTIF_SIRA_SQL; + +/** SELECT_COLS ile okunan ham satır: sıra kolonları `s` takma adıyla gelir. */ +export type ProgramHamSatiri = Omit< + Program, + "siraGecmisi" | "efektifSira" | "siraSon" +> & + Record<`s${number}`, number | null>; + +/** Ham DB satırını yılsız `Program`'a çevirir. */ +export function programSatiri(row: ProgramHamSatiri): Program { + const siraGecmisi = YILLAR.map((yil) => row[`s${yil}`] ?? null); + const siraSon = siraGecmisi[siraGecmisi.length - 1] ?? null; + return { + id: row.id, + isim: row.isim, + universite: row.universite, + unitur: row.unitur, + il: row.il, + fakulte: row.fakulte, + tur: row.tur, + sure: row.sure, + siraGecmisi, + // EFEKTIF_SIRA ile aynı kural: son yıl, yoksa bir önceki yıl + efektifSira: siraSon ?? siraGecmisi[siraGecmisi.length - 2] ?? null, + siraSon, + puanSon: row.puanSon, + kontenjanSon: row.kontenjanSon, + yerlesenSon: row.yerlesenSon, + }; +} /** * Kullanıcının başarı sıralamasına göre programları üç dilime ayırır. @@ -112,18 +147,18 @@ export function dilimAra( // 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 + const satirlar = db .prepare( `SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?` ) - .all(...args, limit, offset) as Program[]; + .all(...args, limit, offset) as ProgramHamSatiri[]; const toplam = ( db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as { adet: number; } ).adet; - return { programlar, toplam }; + return { programlar: satirlar.map(programSatiri), toplam }; } export function searchByRank( diff --git a/src/lib/katalog.ts b/src/lib/katalog.ts index a2ebbf8..ae4339c 100644 --- a/src/lib/katalog.ts +++ b/src/lib/katalog.ts @@ -1,5 +1,11 @@ import "server-only"; -import { getDb, SELECT_COLS, EFEKTIF_SIRA } from "./db"; +import { + getDb, + programSatiri, + SELECT_COLS, + EFEKTIF_SIRA, + type ProgramHamSatiri, +} from "./db"; import type { Program } from "@/types/yokatlas"; import { kategoriBul, type Kategori } from "./kategoriler"; import { uniAdiNormalize } from "./harita"; @@ -24,7 +30,7 @@ export type BolumOzet = { kategoriler: Kategori[]; lisans: boolean; onlisans: boolean; - /** MIN(COALESCE(sira2025, sira2024)) — en iyi (en küçük) taban sıralaması */ + /** MIN(efektif sıra) — en iyi (en küçük) taban sıralaması */ enIyiSira: number | null; }; @@ -131,13 +137,15 @@ export function getBolumBySlug(slug: string): BolumDetay | null { if (!kayit) return null; const placeholders = kayit.hamIsimler.map(() => "?").join(","); - const programlar = getDb() - .prepare( - `SELECT ${SELECT_COLS} FROM programs - WHERE isim IN (${placeholders}) - ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, - ) - .all(...kayit.hamIsimler) as (Program & { onlisans?: number })[]; + const programlar = ( + getDb() + .prepare( + `SELECT ${SELECT_COLS} FROM programs + WHERE isim IN (${placeholders}) + ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, + ) + .all(...kayit.hamIsimler) as ProgramHamSatiri[] + ).map(programSatiri); // onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans) const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT"); @@ -156,14 +164,14 @@ export function getBolumBySlug(slug: string): BolumDetay | null { if (p.unitur === "DEVLET") devlet += 1; else if (p.unitur) vakif += 1; else bilinmeyenTur += 1; - const efektif = p.sira2025 ?? p.sira2024; + const efektif = p.efektifSira; if (efektif != null) { if (minSira == null || efektif < minSira) minSira = efektif; if (maxSira == null || efektif > maxSira) maxSira = efektif; } - if (p.puan2025 != null) { - if (minPuan == null || p.puan2025 < minPuan) minPuan = p.puan2025; - if (maxPuan == null || p.puan2025 > maxPuan) maxPuan = p.puan2025; + if (p.puanSon != null) { + if (minPuan == null || p.puanSon < minPuan) minPuan = p.puanSon; + if (maxPuan == null || p.puanSon > maxPuan) maxPuan = p.puanSon; } } @@ -357,13 +365,15 @@ export function getUniversiteBySlug(slug: string): UniDetay | null { if (!kayit) return null; const placeholders = kayit.hamAdlar.map(() => "?").join(","); - const programlar = getDb() - .prepare( - `SELECT ${SELECT_COLS} FROM programs - WHERE universite IN (${placeholders}) - ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, - ) - .all(...kayit.hamAdlar) as Program[]; + const programlar = ( + getDb() + .prepare( + `SELECT ${SELECT_COLS} FROM programs + WHERE universite IN (${placeholders}) + ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, + ) + .all(...kayit.hamAdlar) as ProgramHamSatiri[] + ).map(programSatiri); const gruplar = new Map(); let minSira: number | null = null; @@ -373,11 +383,11 @@ export function getUniversiteBySlug(slug: string): UniDetay | null { const liste = gruplar.get(anahtar); if (liste) liste.push(p); else gruplar.set(anahtar, [p]); - const efektif = p.sira2025 ?? p.sira2024; + const efektif = p.efektifSira; if (efektif != null && (minSira == null || efektif < minSira)) { minSira = efektif; } - toplamKontenjan += p.kontenjan2025 ?? 0; + toplamKontenjan += p.kontenjanSon ?? 0; } const ozet = getAllUniversiteler().find((u) => u.slug === slug)!; diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts index 5453485..7efe92f 100644 --- a/src/lib/rapor-havuzu.ts +++ b/src/lib/rapor-havuzu.ts @@ -8,6 +8,7 @@ import { } from "@/types/yokatlas"; import type { RaporParams } from "@/features/rapor/types/rapor"; import { kategoriEslesir } from "./kategoriler"; +import { SON_YIL, YILLAR } from "./veri-yillari"; export type Dilim = "hayal" | "dengeli" | "garanti"; @@ -155,6 +156,13 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc { return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi }; } +// Prompt'a veri penceresinin tamamı değil son 4 yılı gider (yeniden eskiye): +// pencere uzadıkça satır başına token sayısı sabit kalır. +const PROMPT_YIL_SAYISI = 4; +const PROMPT_YIL_INDEKSLERI = YILLAR.map((_, i) => i) + .slice(-PROMPT_YIL_SAYISI) + .reverse(); + /** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */ export function havuzuKompaktJson(havuz: AdayProgram[]): string { return JSON.stringify( @@ -165,14 +173,16 @@ export function havuzuKompaktJson(havuz: AdayProgram[]): string { 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, + // Anahtarlar yıl taşır (y, kontenjan): model rakamın hangi + // yıla ait olduğunu anahtardan okur. + sira: Object.fromEntries( + PROMPT_YIL_INDEKSLERI.map((i) => [ + `y${YILLAR[i]}`, + p.siraGecmisi[i] ?? null, + ]), + ), + [`kontenjan${SON_YIL}`]: p.kontenjanSon, + [`yerlesen${SON_YIL}`]: p.yerlesenSon, })), ); } diff --git a/src/lib/rapor-maske.ts b/src/lib/rapor-maske.ts index ffb70ad..8842b6a 100644 --- a/src/lib/rapor-maske.ts +++ b/src/lib/rapor-maske.ts @@ -44,7 +44,7 @@ export function raporMaskele(rapor: RaporSonuc): MaskeliRapor { // kilitli metinler istemciye hiç ulaşmaz. uyarilar: rapor.uyarilar?.slice(0, 1) ?? [], kilitliUyariSayisi: Math.max(0, (rapor.uyarilar?.length ?? 0) - 1), - // Program adı, üniversite ve 2021–2025 taban sıralamaları ham veridir. + // Program adı, üniversite ve geçmiş yılların taban sıralamaları ham veridir. programlar: rapor.programlar, // Kapsam (kaç program bulundu / dağılım uyarlandı mı) dürüstlük bilgisi: // paketsiz kullanıcı da görür, maskelenmez. diff --git a/src/lib/risk.ts b/src/lib/risk.ts index 132e176..e025ac0 100644 --- a/src/lib/risk.ts +++ b/src/lib/risk.ts @@ -32,9 +32,10 @@ export const RISK_PIN_RENK: Record = { }; /** - * Deterministik risk: programın efektif taban sıralaması (COALESCE 2025→2024) - * adayın sıralamasıyla kıyaslanır. Eşikler db.ts'teki dilim aralıklarıyla - * birebir aynı (dengeli: [sira, 1.4×sira], garanti: (1.4×sira, ∞)). + * Deterministik risk: programın efektif taban sıralaması (son yılın sırası, + * yoksa bir önceki yılınki) adayın sıralamasıyla kıyaslanır. Eşikler + * db.ts'teki dilim aralıklarıyla birebir aynı (dengeli: [sira, 1.4×sira], + * garanti: (1.4×sira, ∞)). */ export function riskHesapla( efektifSira: number | null | undefined, @@ -72,7 +73,8 @@ export function dilimdenFarkliRiskEtiketi( * Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık). * Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın * sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez. - * "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor. + * "Geçen yıl" da denmez: efektif sıra, son yılın verisi yoksa bir önceki + * yılınkine düşüyor. */ export function riskNotuMetni(risk: RiskSeviyesi | null): string { switch (risk) { diff --git a/src/lib/tadimlik-havuzu.ts b/src/lib/tadimlik-havuzu.ts index 7b01b7b..90071fb 100644 --- a/src/lib/tadimlik-havuzu.ts +++ b/src/lib/tadimlik-havuzu.ts @@ -10,6 +10,7 @@ import type { Dilim } from "./rapor-havuzu"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; import { trendOzetiMetni } from "./trend"; +import { VERI_YIL_SAYISI } from "./veri-yillari"; /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ export const GENEL_KATEGORI = "genel"; @@ -54,16 +55,41 @@ export type TadimlikTip = "genel" | "devlet" | "vakif"; * gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya * göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık / * ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe - * servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı - * kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe - * kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı - * eşlemez). + * servis edilmez. Kalıp küçük harfledir: metin önce Türkçe kurallarıyla + * küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı eşlemez). */ const YASAK_GEREKCE_DILI = - /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/; + /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil/; + +// Bayat batch kalıbı: gerekçe "son N yıl" diyorsa N, verinin bugün kapsadığı +// yıl sayısı (VERI_YIL_SAYISI) olmalı. Veri penceresi uzayınca eski batch'in +// "son 5 yıl"ı da, daha eskisinin "son 4 yıl"ı da kendiliğinden takılır. +const SON_N_YIL = /son (\d+|[a-zçğıöşü]+) yıl/g; +const SAYI_SOZCUKLERI: Record = { + bir: 1, + iki: 2, + üç: 3, + dört: 4, + beş: 5, + altı: 6, + yedi: 7, + sekiz: 8, + dokuz: 9, + on: 10, +}; + +function bayatYilSayisiVar(kucukMetin: string): boolean { + for (const [, ham] of kucukMetin.matchAll(SON_N_YIL)) { + const n = /^\d+$/.test(ham) ? Number(ham) : SAYI_SOZCUKLERI[ham]; + // Sayı olmayan sözcük ("son birkaç yıl") yıl sayısı iddiası değildir + if (n != null && n !== VERI_YIL_SAYISI) return true; + } + return false; +} export function gerekceServisEdilebilir(gerekce: string): boolean { - return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR")); + const kucuk = gerekce.toLocaleLowerCase("tr-TR"); + return !YASAK_GEREKCE_DILI.test(kucuk) && !bayatYilSayisiVar(kucuk); } /** @@ -108,10 +134,12 @@ export async function tadimlikSec( // aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı // saf fonksiyondan programın sıra geçmişinden okuma anında üretilir — // metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz. + // Snapshot'ın sıra dizisi üretildiği günün penceresidir (eski + // satırlarda 5 eleman); yıllar dizinin kendi çapasından okunur. riskNotu: riskNotuMetni( riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), ), - trendOzeti: trendOzetiMetni(program.siraGecmisi), + trendOzeti: trendOzetiMetni(program.siraGecmisi, program.ilkYil), program, }; } diff --git a/src/lib/trend.ts b/src/lib/trend.ts index 4e3c8bd..8221cda 100644 --- a/src/lib/trend.ts +++ b/src/lib/trend.ts @@ -4,12 +4,7 @@ // düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil, // serideki dolu yıllardan gelir. -/** - * Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar - * (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu - * çapa değişmez. - */ -const ILK_VERI_YILI = 2021; +import { ILK_YIL } from "./veri-yillari"; /** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ function siraKisa(n: number): string { @@ -21,7 +16,10 @@ function siraKisa(n: number): string { } /** - * [2021..] taban sıralaması dizisinden tek cümlelik trend özeti. + * [ilkYil..] taban sıralaması dizisinden tek cümlelik trend özeti. Dizi + * `ilkYil`'a demirlidir (varsayılan: veri penceresinin ilk yılı); yeni veri + * yılı sona eklenir, bu yüzden eski kayıtların daha kısa dizileri de doğru + * yılları verir. `ilkYil`, snapshot kendi çapasını taşıyorsa ondan gelir. * * Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar * (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı @@ -30,22 +28,23 @@ function siraKisa(n: number): string { */ export function trendOzetiMetni( gecmis: readonly (number | null | undefined)[] | undefined, + ilkYil: number = ILK_YIL, ): string { const seri: Array<[number, number]> = []; (gecmis ?? []).forEach((sira, i) => { - if (sira != null) seri.push([ILK_VERI_YILI + i, sira]); + if (sira != null) seri.push([ilkYil + i, sira]); }); if (seri.length < 2) { return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; } - const [ilkYil, ilkSira] = seri[0]; + const [basYil, ilkSira] = seri[0]; const [sonYil, sonSira] = seri[seri.length - 1]; const oran = (sonSira - ilkSira) / ilkSira; if (oran <= -0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; + return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; } if (oran >= 0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; + return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; } - return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; + return `${basYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; } diff --git a/src/lib/veri-kapsami.ts b/src/lib/veri-kapsami.ts index 937f77a..f3a7f47 100644 --- a/src/lib/veri-kapsami.ts +++ b/src/lib/veri-kapsami.ts @@ -1,6 +1,6 @@ /** - * Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında - * sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu - * sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle. + * Taban sıralaması geçmişinin kapsadığı yıl sayısı; "son N yıl" diyen tüm + * metinler bu sabitten okur. Değer yıl kapsamının tek kaynağından + * (lib/veri-yillari.ts) türetilir — yeni veri yılı eklenince burası değişmez. */ -export const VERI_YIL_SAYISI = 5; +export { VERI_YIL_SAYISI } from "./veri-yillari"; diff --git a/src/types/yokatlas.ts b/src/types/yokatlas.ts index 55b1c9e..6e7d61b 100644 --- a/src/types/yokatlas.ts +++ b/src/types/yokatlas.ts @@ -11,14 +11,16 @@ export type Program = { 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; + /** Taban sıralamaları; `YILLAR` (lib/veri-yillari) ile indeks hizalı. */ + siraGecmisi: (number | null)[]; + /** Son yılın sırası; o yıl verisi yoksa bir önceki yılınki. */ + efektifSira: number | null; + // "Son" = en son yerleştirme yılı (SON_YIL). Alan adları bilinçli yılsız: + // yeni veri yılı yüklenince tip ve tüketiciler değişmez. + siraSon: number | null; + puanSon: number | null; + kontenjanSon: number | null; + yerlesenSon: number | null; }; export const PUAN_TURLERI = { @@ -42,11 +44,15 @@ export type RankResults = { }; /** - * /sonuc ham tablosunun satırı: tablo `fakulte` ve `puan2025`'i hiç + * /sonuc ham tablosunun satırı: tablo `fakulte`, `puanSon` ve `siraSon`'u + * (dizinin son elemanı; tablo `efektifSira` + `siraGecmisi` ile çalışır) hiç * kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en * uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır. */ -export type ProgramListeSatiri = Omit; +export type ProgramListeSatiri = Omit< + Program, + "fakulte" | "puanSon" | "siraSon" +>; export type RankResultsListe = { hayal: ProgramListeSatiri[]; From ceeb70375d14bfbb6476f147db699f5da58f2882 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 17:00:39 +0300 Subject: [PATCH 2/3] =?UTF-8?q?feat(veri):=20bile=C5=9Fenler=20y=C4=B1ls?= =?UTF-8?q?=C4=B1z=20Program'a=20ve=20veri-yillari=20s=C3=B6zl=C3=BC=C4=9F?= =?UTF-8?q?=C3=BCne=20ge=C3=A7ti=20(P-B=20b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SiraSerisi = { siraGecmisi, ilkYil? }; yardımcılar dizinin kendi son iki elemanıyla çalışır, yıl ilkYil + indeks. raporSiraSerisi diziyi doldurmaz: 5 elemanlı eski raporlar 2024→2025 trendini göstermeye devam eder. - Tablo başlıkları "Son ${VERI_YIL_SAYISI} yıl"; rapor listesi başlığı kaydın kendi pencere uzunluğunu söyler (eski rapor: 5). - siraYiliEtiketi SON_YIL/ONCEKI_YIL'dan; bölüm/üniversite sayfa metinleri ve metadata SON_YIL, TERCIH_YILI, BASLIK_YILI sözlüğünden. Co-Authored-By: Claude Fable 5.1 --- src/components/program-liste-verileri.tsx | 62 ++++++++++--------- .../katalog/components/bolum-icerik.tsx | 32 ++++++---- .../components/bolum-program-listesi.tsx | 25 ++++---- src/features/katalog/components/sayfalama.tsx | 18 ++++-- .../katalog/components/universite-icerik.tsx | 26 ++++---- .../components/universite-program-tablosu.tsx | 13 ++-- .../liste/components/program-ekle-butonu.tsx | 3 +- .../liste/components/program-tablosu.tsx | 13 ++-- .../rapor/components/rapor-listesi.tsx | 22 +++++-- .../rapor/components/tadimlik-satiri.tsx | 2 +- 10 files changed, 127 insertions(+), 89 deletions(-) diff --git a/src/components/program-liste-verileri.tsx b/src/components/program-liste-verileri.tsx index a91029e..a4dde16 100644 --- a/src/components/program-liste-verileri.tsx +++ b/src/components/program-liste-verileri.tsx @@ -1,30 +1,37 @@ import { TrendingDown, TrendingUp } from "lucide-react"; -import type { Program } from "@/types/yokatlas"; +import { ILK_YIL } from "@/lib/veri-yillari"; -/** Taban sıra sütunlarının yapısal alt kümesi: hem DB programı hem de - * rapordaki kaydedilmiş sıra geçmişi (bkz. raporSiraSerisi) bu tipe uyar. */ -export type SiraSerisi = Pick< - Program, - "sira2021" | "sira2022" | "sira2023" | "sira2024" | "sira2025" ->; +/** Taban sıra geçmişinin yapısal alt kümesi: hem DB programı hem de rapordaki + * kaydedilmiş sıra geçmişi (bkz. raporSiraSerisi) bu tipe uyar. Dizi `ilkYil`'a + * (yoksa veri penceresinin ilk yılına) demirlidir: eleman yılı + * `ilkYil + indeks`. Uzunluk sabit DEĞİL — eski kayıtlı raporlar daha kısa + * dizi taşır; yardımcılar dizinin kendi son iki elemanıyla çalışır. */ +export type SiraSerisi = { + siraGecmisi: readonly (number | null)[]; + ilkYil?: number; +}; -/** Rapordaki [2021..2025] dizisini sütunlu seriye çevirir. */ -export function raporSiraSerisi(gecmis?: (number | null)[]): SiraSerisi { - const g = gecmis ?? []; - return { - sira2021: g[0] ?? null, - sira2022: g[1] ?? null, - sira2023: g[2] ?? null, - sira2024: g[3] ?? null, - sira2025: g[4] ?? null, - }; +/** Rapordaki kayıtlı diziyi seriye çevirir. Dizi DOLDURULMAZ: 5 elemanlı eski + * kayıt 5 elemanlı kalır, "son iki yıl" kaydın kendi son iki yılıdır. */ +export function raporSiraSerisi( + gecmis?: (number | null)[], + ilkYil?: number, +): SiraSerisi { + return { siraGecmisi: gecmis ?? [], ilkYil }; +} + +/** Serinin son iki yılı: [bir önceki, son]. */ +function sonIkiSira(program: SiraSerisi): [number | null, number | null] { + const g = program.siraGecmisi; + return [g[g.length - 2] ?? null, g[g.length - 1] ?? null]; } export function programEtkinSira(program: SiraSerisi): number | null { - return program.sira2025 ?? program.sira2024; + const [onceki, son] = sonIkiSira(program); + return son ?? onceki; } -/** 2021–2025 taban sıralamasını sonuç listesindeki kompakt çizgiyle gösterir. */ +/** Taban sıralaması geçmişini sonuç listesindeki kompakt çizgiyle gösterir. */ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) { // Satır başına render edilen bileşen: filter/map/min/max/map/map'in 6 ayrı // geçişi tek döngüde toplanır (js-combine-iterations, js-min-max-loop) @@ -32,14 +39,10 @@ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) { let min = Infinity; let max = -Infinity; const ozetParcalari: string[] = []; - for (const [yil, sira] of [ - [2021, program.sira2021], - [2022, program.sira2022], - [2023, program.sira2023], - [2024, program.sira2024], - [2025, program.sira2025], - ] as const) { + const ilkYil = program.ilkYil ?? ILK_YIL; + for (const [indeks, sira] of program.siraGecmisi.entries()) { if (sira == null) continue; + const yil = ilkYil + indeks; yillar.push([yil, sira]); if (sira < min) min = sira; if (sira > max) max = sira; @@ -89,12 +92,13 @@ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) { /** Son iki yılın tabanı %10'dan fazla değiştiğinde yönü işaretler. */ export function ProgramTabanTrendi({ program }: { program: SiraSerisi }) { - if (program.sira2025 == null || program.sira2024 == null) return null; + const [onceki, son] = sonIkiSira(program); + if (son == null || onceki == null) return null; - const oran = (program.sira2024 - program.sira2025) / program.sira2024; + const oran = (onceki - son) / onceki; if (Math.abs(oran) < 0.1) return null; - const gecis = `${program.sira2024.toLocaleString("tr-TR")} → ${program.sira2025.toLocaleString("tr-TR")}`; + const gecis = `${onceki.toLocaleString("tr-TR")} → ${son.toLocaleString("tr-TR")}`; if (oran > 0) { return ( n.toLocaleString("tr-TR"); @@ -65,20 +71,20 @@ function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) { let minSira: number | null = null; let maxSira: number | null = null; let verisiz = 0; - // Sırası 2025'te olmayıp 2024'ten gelen satırlar: aralık metni yılı dürüstçe - // söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir). - let yalniz2024 = 0; + // Sırası son yılda olmayıp bir önceki yıldan gelen satırlar: aralık metni + // yılı dürüstçe söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir). + let yalnizOnceki = 0; for (const p of dilim) { - const efektif = p.sira2025 ?? p.sira2024; + const efektif = p.efektifSira; if (efektif == null) { verisiz += 1; continue; } - if (p.sira2025 == null) yalniz2024 += 1; + if (p.siraSon == null) yalnizOnceki += 1; if (minSira == null || efektif < minSira) minSira = efektif; if (maxSira == null || efektif > maxSira) maxSira = efektif; } - return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalniz2024 }; + return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalnizOnceki }; } /** için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */ @@ -99,13 +105,13 @@ function siraAraligiMetni({ minSira, maxSira, verisiz, - yalniz2024, + yalnizOnceki, }: ReturnType<typeof bolumSayfaDilimi>): string | null { if (dilim.length === 0) return null; if (minSira == null || maxSira == null) { return "taban verisi olmayan programlar"; } - const yil = siraYiliEtiketi(yalniz2024, dilim.length - verisiz); + const yil = siraYiliEtiketi(yalnizOnceki, dilim.length - verisiz); const aralik = minSira === maxSira ? `${yil} taban sırası ${sayi(minSira)} olan programlar` @@ -134,8 +140,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata { const meta = sayfaMetadata({ title: baslik, description: aralik - ? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (2025; 2025'te verisi olmayanlarda 2024) — taban puanı, başarı sıralaması ve kontenjan.` - : `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`, + ? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (${SON_YIL}; o yıl verisi olmayanlarda ${ONCEKI_YIL}) — taban puanı, başarı sıralaması ve kontenjan.` + : `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın ${SON_YIL} yerleştirme verileri.`, path: `/bolum/${slug}/sayfa/${sayfa}`, openGraph: { images: ustSegmentOgGorseli( @@ -151,8 +157,8 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata { ? ` En iyi taban sıralaması ${sayi(stats.minSira)}.` : ""; return sayfaMetadata({ - title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları 2026`, - description: `${bolum.ad} 2025 yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} 2026 tercih rehberi.`, + title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`, + description: `${bolum.ad} ${SON_YIL} yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} ${TERCIH_YILI} tercih rehberi.`, path: `/bolum/${slug}`, }); } @@ -230,7 +236,7 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { </h1> {ilkSayfa ? ( <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base"> - {sayi(tumProgramlar.length)} üniversite programının 2025 taban + {sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve kontenjanlarını incele. </p> ) : ( diff --git a/src/features/katalog/components/bolum-program-listesi.tsx b/src/features/katalog/components/bolum-program-listesi.tsx index 2051159..f95057a 100644 --- a/src/features/katalog/components/bolum-program-listesi.tsx +++ b/src/features/katalog/components/bolum-program-listesi.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { ChevronDown, ChevronUp } from "lucide-react"; import type { Program } from "@/types/yokatlas"; import { trBaslikDuzeni } from "@/lib/slug"; +import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { Sayfalama, SAYFA_BOYU } from "./sayfalama"; import { ProgramSiraGecmisi, @@ -24,14 +25,14 @@ import { const sayi = (n: number) => n.toLocaleString("tr-TR"); -type SiralamaAlani = "sira2025" | "puan2025" | "kontenjan2025"; +type SiralamaAlani = "siraSon" | "puanSon" | "kontenjanSon"; type SiralamaYonu = "artan" | "azalan"; export type BolumProgrami = Program & { universiteSlug: string | null }; const ALAN_ETIKETLERI: Record<SiralamaAlani, string> = { - sira2025: "Sıralama", - puan2025: "Puan", - kontenjan2025: "Kontenjan", + siraSon: "Sıralama", + puanSon: "Puan", + kontenjanSon: "Kontenjan", }; function SiralamaDugmesi({ @@ -157,11 +158,11 @@ export function BolumProgramListesi({ <TableRow> <TableHead>Üniversite</TableHead> <TableHead className="hidden text-right md:table-cell"> - Son 5 yıl + Son {VERI_YIL_SAYISI} yıl </TableHead> <TableHead className="hidden text-right lg:table-cell"> <SiralamaDugmesi - alan="puan2025" + alan="puanSon" aktifAlan={aktifAlan} yon={yon} onSirala={sirala} @@ -169,7 +170,7 @@ export function BolumProgramListesi({ </TableHead> <TableHead className="hidden text-right sm:table-cell"> <SiralamaDugmesi - alan="kontenjan2025" + alan="kontenjanSon" aktifAlan={aktifAlan} yon={yon} onSirala={sirala} @@ -177,7 +178,7 @@ export function BolumProgramListesi({ </TableHead> <TableHead className="text-right"> <SiralamaDugmesi - alan="sira2025" + alan="siraSon" aktifAlan={aktifAlan} yon={yon} onSirala={sirala} @@ -243,17 +244,17 @@ export function BolumProgramListesi({ <ProgramSiraGecmisi program={p} /> </TableCell> <TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell"> - {p.puan2025 != null - ? p.puan2025.toFixed(2).replace(".", ",") + {p.puanSon != null + ? p.puanSon.toFixed(2).replace(".", ",") : "—"} </TableCell> <TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell"> - {p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"} + {p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"} </TableCell> <TableCell className="text-right text-xs tabular-nums text-slate-500"> <span className="inline-flex items-center gap-1"> <ProgramTabanTrendi program={p} /> - {p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"} + {p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"} </span> </TableCell> <TableCell className="pr-3 text-right"> diff --git a/src/features/katalog/components/sayfalama.tsx b/src/features/katalog/components/sayfalama.tsx index a7e56d3..bf2c546 100644 --- a/src/features/katalog/components/sayfalama.tsx +++ b/src/features/katalog/components/sayfalama.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import Link from "next/link"; import { ChevronLeft, ChevronRight } from "lucide-react"; +import { ONCEKI_YIL, SON_YIL } from "@/lib/veri-yillari"; /** Programatik tablo sayfaları için satır dilimi boyutu. */ export const SAYFA_BOYU = 50; @@ -13,10 +14,19 @@ export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] { return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU); } -/** Aralıktaki sıraların hangi yıldan geldiği: hepsi 2025 / hepsi 2024 / karışık. */ -export function siraYiliEtiketi(yalniz2024: number, siraliSayi: number): string { - if (yalniz2024 === 0) return "2025"; - return yalniz2024 === siraliSayi ? "2024" : "2024–2025"; +/** + * Aralıktaki sıraların hangi yıldan geldiği: hepsi son yıl / hepsi bir önceki + * yıl / karışık. `yalnizOnceki`: sırası son yılda olmayıp bir önceki yıldan + * gelen satır sayısı; `siraliSayi`: sırası olan toplam satır. + */ +export function siraYiliEtiketi( + yalnizOnceki: number, + siraliSayi: number, +): string { + if (yalnizOnceki === 0) return String(SON_YIL); + return yalnizOnceki === siraliSayi + ? String(ONCEKI_YIL) + : `${ONCEKI_YIL}–${SON_YIL}`; } /** diff --git a/src/features/katalog/components/universite-icerik.tsx b/src/features/katalog/components/universite-icerik.tsx index 50821ab..90da09d 100644 --- a/src/features/katalog/components/universite-icerik.tsx +++ b/src/features/katalog/components/universite-icerik.tsx @@ -34,6 +34,7 @@ import { UniLogo } from "@/components/uni-logo"; import { uniLogoYolu } from "@/lib/uni-logolar"; import { Skeleton } from "@/components/ui/skeleton"; import type { Program } from "@/types/yokatlas"; +import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari"; const sayi = (n: number) => n.toLocaleString("tr-TR"); @@ -91,13 +92,13 @@ function fakulteOzeti({ if (adlar.length === 0) return null; if (adlar.length === 1) { const siralar = dilim - .map((p) => p.sira2025 ?? p.sira2024) + .map((p) => p.efektifSira) .filter((n): n is number => n != null); if (siralar.length === 0) return adlar[0]; const min = Math.min(...siralar); const max = Math.max(...siralar); const yil = siraYiliEtiketi( - dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length, + dilim.filter((p) => p.siraSon == null && p.efektifSira != null).length, siralar.length, ); return min === max @@ -116,10 +117,10 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata { // farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider. const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa)); return sayfaMetadata({ - title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`, + title: `${uni.ad} Taban Puanları ${BASLIK_YILI} (Sayfa ${sayfa})`, description: birimler - ? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.` - : `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`, + ? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.` + : `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri.`, path: `/universite/${slug}/sayfa/${sayfa}`, openGraph: { images: ustSegmentOgGorseli( @@ -130,8 +131,8 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata { }); } return sayfaMetadata({ - title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları 2026`, - description: `${uni.ad} 2025 yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. 2026 tercih rehberi.`, + title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`, + description: `${uni.ad} ${SON_YIL} yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. ${TERCIH_YILI} tercih rehberi.`, path: `/universite/${slug}`, }); } @@ -241,7 +242,7 @@ export function UniversiteIcerik({ <div className="mt-4 flex items-center gap-4"> <UniLogo slug={slug} ad={uni.ad} boy="lg" /> <h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl"> - {uni.ad} Taban Puanları 2026 + {uni.ad} Taban Puanları {BASLIK_YILI} {ilkSayfa ? "" : ` — Sayfa ${sayfa}`} </h1> </div> @@ -250,7 +251,7 @@ export function UniversiteIcerik({ <> <p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600"> {uni.ad} - {uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, 2025 YKS + {uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, {SON_YIL} YKS yerleştirme sonuçlarına göre{" "} <strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı {uni.fakulteSayisi > 1 @@ -267,10 +268,11 @@ export function UniversiteIcerik({ {uni.stats.toplamKontenjan > 0 ? ( <> Toplam kontenjan {sayi(uni.stats.toplamKontenjan)}.</> ) : null}{" "} - Aşağıdaki tablolar 2026 tercihleri için rehber niteliğindedir;{" "} + Aşağıdaki tablolar {TERCIH_YILI} tercihleri için rehber + niteliğindedir;{" "} <em> - 2026 taban puanları ve sıralamaları, yerleştirme sonuçları - açıklandığında netleşir. + {TERCIH_YILI} taban puanları ve sıralamaları, yerleştirme + sonuçları açıklandığında netleşir. </em> </p> diff --git a/src/features/katalog/components/universite-program-tablosu.tsx b/src/features/katalog/components/universite-program-tablosu.tsx index 34f35b3..eb7408d 100644 --- a/src/features/katalog/components/universite-program-tablosu.tsx +++ b/src/features/katalog/components/universite-program-tablosu.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import type { Program } from "@/types/yokatlas"; +import { SON_YIL, VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { ProgramSiraGecmisi, ProgramTabanTrendi, @@ -48,10 +49,10 @@ export function UniversiteProgramTablosu({ <TableRow> <TableHead>Program</TableHead> <TableHead className="hidden text-right md:table-cell"> - Son 5 yıl + Son {VERI_YIL_SAYISI} yıl </TableHead> <TableHead className="hidden text-right lg:table-cell"> - 2025 puan + {SON_YIL} puan </TableHead> <TableHead className="hidden text-right sm:table-cell"> Kontenjan @@ -93,13 +94,13 @@ export function UniversiteProgramTablosu({ <ProgramSiraGecmisi program={p} /> </TableCell> <TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell"> - {p.puan2025 != null - ? p.puan2025.toFixed(2).replace(".", ",") + {p.puanSon != null + ? p.puanSon.toFixed(2).replace(".", ",") : "—"} </TableCell> <TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell"> - {p.kontenjan2025 != null - ? `${sayi(p.kontenjan2025)} kişi` + {p.kontenjanSon != null + ? `${sayi(p.kontenjanSon)} kişi` : "—"} </TableCell> <TableCell className="text-right text-xs tabular-nums text-slate-500"> diff --git a/src/features/liste/components/program-ekle-butonu.tsx b/src/features/liste/components/program-ekle-butonu.tsx index 95613cc..7cf0bd1 100644 --- a/src/features/liste/components/program-ekle-butonu.tsx +++ b/src/features/liste/components/program-ekle-butonu.tsx @@ -24,8 +24,7 @@ type ProgramOzet = Pick< | "il" | "unitur" | "tur" - | "sira2025" - | "sira2024" + | "efektifSira" >; const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme"; diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index e71c009..c889950 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -22,6 +22,7 @@ import type { } from "@/types/yokatlas"; import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "@/lib/risk"; import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug"; +import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { ProgramSiraGecmisi, ProgramTabanTrendi, @@ -239,7 +240,7 @@ export function ProgramTablosu({ <TableHead className="w-8" /> <TableHead>Program</TableHead> <TableHead className="hidden text-right md:table-cell"> - Son 5 yıl + Son {VERI_YIL_SAYISI} yıl </TableHead> <TableHead className="hidden text-right sm:table-cell"> Kontenjan @@ -357,19 +358,19 @@ function ProgramSatiri({ <TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell" title={ - p.kontenjan2025 != null && p.yerlesen2025 != null - ? `${p.kontenjan2025} kontenjan, ${p.yerlesen2025} yerleşen` + p.kontenjanSon != null && p.yerlesenSon != null + ? `${p.kontenjanSon} kontenjan, ${p.yerlesenSon} yerleşen` : undefined } > - {p.kontenjan2025 != null ? ( + {p.kontenjanSon != null ? ( <span className="inline-flex items-center gap-1.5"> - {p.yerlesen2025 != null && p.yerlesen2025 < p.kontenjan2025 ? ( + {p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? ( <span className="rounded bg-amber-50 px-1 py-px text-[10px] font-medium leading-4 text-amber-600"> boş kaldı </span> ) : null} - {p.kontenjan2025.toLocaleString("tr-TR")} kişi + {p.kontenjanSon.toLocaleString("tr-TR")} kişi </span> ) : ( "—" diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index b8af439..0a59dc7 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -2,7 +2,7 @@ // 24'lük listenin gövdesi. Görsel dil sonuç sayfasındaki ham program // tablosuyla birebir aynıdır (bkz. program-tablosu.tsx): aynı beyaz kart, -// aynı satır düzeni, aynı "Son 5 yıl" / "Taban sıra" sütunları. Buradaki fark +// aynı satır düzeni, aynı "Son N yıl" / "Taban sıra" sütunları. Buradaki fark // içerik: satır sırası tercih numarası, risk sütunu Yapay Zeka'dan gelir ve // satır açıldığında gerekçe/risk notu görünür. Program adı ve tabanlar herkese // açık; Yapay Zeka katmanı kilitliBaslangic sonrasında ücretlidir (metin @@ -27,6 +27,7 @@ import { TableRow, } from "@/components/ui/table"; import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug"; +import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import { DILIM_ETIKET, @@ -47,7 +48,7 @@ const RISK_STIL: Record<RiskSeviyesi, { nokta: string; icon: typeof Rocket }> = guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck }, }; -// Sütun sayısı: # · Program · Son 5 yıl · Risk · Taban sıra · detay oku +// Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku const SUTUN_SAYISI = 6; /** @@ -161,6 +162,19 @@ export function RaporListesi({ return iller.size === 1 ? [...iller][0] : null; }, [rapor]); + // Sütun başlığı kaydın KENDİ penceresini söyler: eski raporların sıra + // geçmişi üretildiği günün yıl sayısını taşır (dizi doldurulmaz), başlık + // çizgide olmayan bir yılı vaat etmesin. Geçmişi hiç olmayan kayıtta + // bugünkü pencereye düşer. + const yilSayisi = useMemo(() => { + let enUzun = 0; + for (const t of rapor.tercihler) { + const n = rapor.programlar[t.programId]?.siraGecmisi?.length ?? 0; + if (n > enUzun) enUzun = n; + } + return enUzun > 0 ? enUzun : VERI_YIL_SAYISI; + }, [rapor]); + const satir = (t: RaporSonuc["tercihler"][number], index: number) => { const p = rapor.programlar[t.programId]; const analizKilitli = index >= kilitIdx; @@ -168,7 +182,7 @@ export function RaporListesi({ (adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ?? dilimdenRisk(t.dilim); const stil = RISK_STIL[risk]; - const seri = raporSiraSerisi(p?.siraGecmisi); + const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil); const taban = p?.efektifSira ?? programEtkinSira(seri); const acikMi = acik.has(t.sira); const devlet = p?.unitur === "DEVLET"; @@ -366,7 +380,7 @@ export function RaporListesi({ <TableHead className="w-8 pl-4 pr-0">#</TableHead> <TableHead>Program</TableHead> <TableHead className="hidden text-right md:table-cell"> - Son 5 yıl + Son {yilSayisi} yıl </TableHead> <TableHead> <span className="hidden sm:inline">Yapay Zeka </span>Risk diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 592984a..bc72ea8 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -105,7 +105,7 @@ export function TadimlikSatiri({ const p = satir.program; const risk = riskHesapla(p.efektifSira, sira) ?? dilimdenRisk(satir.dilim); - const seri = raporSiraSerisi(p.siraGecmisi); + const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil); const taban = p.efektifSira ?? programEtkinSira(seri); const devlet = p.unitur === "DEVLET"; const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`; From 85ed972c68d37f72e01f2d2ddfd3d8b5024410fd Mon Sep 17 00:00:00 2001 From: bilalgursen <bilalgursen777@gmail.com> Date: Mon, 21 Sep 2026 17:00:39 +0300 Subject: [PATCH 3/3] =?UTF-8?q?feat(tadimlik):=20batch=20=C3=BCretimi=20y?= =?UTF-8?q?=C4=B1ls=C4=B1z=20alanlarla;=20snapshot'a=20ilkYil=20(P-B=20c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt ve mock metni "son ${VERI_YIL_SAYISI} yıl" der; program seçimi yerlesenSon'a göre; snapshot rapor.ts programBilgisi ile aynı şekilde siraGecmisi + ilkYil yazar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- scripts/tadimlik-uret.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/tadimlik-uret.ts b/scripts/tadimlik-uret.ts index 697c6dd..f27ed2c 100644 --- a/scripts/tadimlik-uret.ts +++ b/scripts/tadimlik-uret.ts @@ -38,6 +38,7 @@ import { } from "../src/lib/tadimlik-havuzu"; import { anahtarVar } from "../src/lib/ai/client"; import { yapilandirilmisUret } from "../src/lib/ai/cagri"; +import { ILK_YIL, VERI_YIL_SAYISI } from "../src/lib/veri-yillari"; // Next dışında koşuyoruz; .env dosyalarını elle yükle (mevcut env kazanır). function envYukle(dosya: string): void { @@ -72,8 +73,8 @@ KURALLAR (kesin): riskli" etiketleriyle konuş. - Programın "dilim" alanındaki "garanti" değeri teknik anahtardır; adaya dönük hiçbir metinde bu kelimeyi kullanma, o dilimi "güvenli" diye an. -- trendOzeti: son 5 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den - beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor"). +- trendOzeti: son ${VERI_YIL_SAYISI} yılın taban sıralaması hareketi, TEK cümle + (ör. "2022'den beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor"). - Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`; function mockAktif(): boolean { @@ -87,27 +88,30 @@ function mockMetin(p: Program, temsiliSira: number, kategori: string | null) { riskNotu: "(Dev mock) Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.", trendOzeti: - "(Dev mock) Son 5 yılda taban sıralaması görece stabil seyretti.", + `(Dev mock) Son ${VERI_YIL_SAYISI} yılda taban sıralaması görece stabil seyretti.`, }; } /** rapor.ts programBilgisi ile aynı snapshot şekli. */ function programBilgisi(p: Program) { - const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; + const gecmis = p.siraGecmisi; + const gecmisVar = gecmis.some((s) => s != null); return { isim: p.isim, universite: p.universite, il: p.il, unitur: p.unitur, - efektifSira: p.sira2025 ?? p.sira2024, - siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined, + efektifSira: p.efektifSira, + siraGecmisi: gecmisVar ? gecmis : undefined, + ilkYil: gecmisVar ? ILK_YIL : undefined, }; } /** * Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden - * (boşsa "garanti") kategoriye ve üniversite tipine uyan, 2025'te en çok - * yerleşen alan program. Tanınır/dolu programlar tadımlıkta daha ikna edici. + * (boşsa "garanti") kategoriye ve üniversite tipine uyan, son yerleştirme + * yılında en çok yerleşen alan program. Tanınır/dolu programlar tadımlıkta + * daha ikna edici. */ function programSec( temsiliSira: number, @@ -125,7 +129,7 @@ function programSec( : programlar; if (uygunlar.length === 0) continue; const secilen = [...uygunlar].sort( - (a, b) => (b.yerlesen2025 ?? -1) - (a.yerlesen2025 ?? -1), + (a, b) => (b.yerlesenSon ?? -1) - (a.yerlesenSon ?? -1), )[0]; return { program: secilen, dilim }; }