merge: veri/2026-entegrasyon — sitedeki taban sıraları 2026 yerleştirme verisine geçti
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Yıl sözleşmesi (veri-yillari.ts, EFEKTIF_SIRA_SQL), P-A veri betikleri ve yıllık güncelleme runbook'u, yokatlas.db 2026 verisi (arşivli), okuyan taraf yılsız Program tipi, metinler + meraklısına şeması, yazdırma dipnotu. Denetim: kod-denetcisi 2026-09-21 — koşullu merge; yayın sonrası prod'da pnpm tadimlik --force, kayıtlı liste tabanı yenileme takip işi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@ next-env.d.ts
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# YÖK Atlas ham API çekimleri (scripts/refresh.ts; ~yıllık, büyük, yeniden üretilebilir)
|
||||
data/ham/
|
||||
|
||||
BIN
data/yokatlas.db
BIN
data/yokatlas.db
Binary file not shown.
230
docs/veri/yillik-guncelleme.md
Normal file
230
docs/veri/yillik-guncelleme.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Yıllık veri güncellemesi (YÖK Atlas → `data/yokatlas.db`)
|
||||
|
||||
Her yıl yerleştirme sonuçları açıklandıktan sonra **bir kez** yapılır. Bu sayfayı
|
||||
yukarıdan aşağıya izleyen biri, betiklerin içini bilmeden güncellemeyi
|
||||
tamamlayabilir. Toplam süre: yaklaşık **30 dakika** (çoğu bekleme ve gözle kontrol).
|
||||
|
||||
> Ürünün bütün değeri bu verinin doğruluğuna dayanır. Yanlış bir taban sıralaması,
|
||||
> bir öğrencinin yanlış tercih yapması demektir. Bu yüzden betikler şüphede
|
||||
> **durur**; "DURDU:" ile başlayan bir mesaj hata değil, emniyet kilididir.
|
||||
> Kilidi aşmaya çalışma — mesajı oku, aşağıdaki "Bir şey durursa" tablosuna bak.
|
||||
|
||||
Örneklerde yeni yıl `2027` diye yazıldı; o yıl hangisiyse onu yaz.
|
||||
|
||||
## 0. Ne zaman?
|
||||
|
||||
- ÖSYM yerleştirme sonuçlarını açıklar (genelde **Ağustos sonu**), YÖK Atlas verisini
|
||||
birkaç gün–birkaç hafta sonra günceller. Tarih her yıl değişir → **doğrulanmalı**.
|
||||
- Erken koşmanın zararı yok: YÖK Atlas henüz güncellenmediyse 3. adım
|
||||
"`… hâlâ 2026 verisi`" diyerek **hiçbir şey yazmadan** durur. Bir hafta sonra tekrar dene.
|
||||
- Dosya indirmen gerekmez. Veri, YÖK Atlas'ın herkese açık tercih kılavuzu
|
||||
servisinden (`https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search`) betikle çekilir:
|
||||
43 istek, aralarında yarım saniye — kaynağı yormaz. Betiği art arda defalarca koşma;
|
||||
ilk çekimden sonra hep kaydedilmiş ham dosyayı (`--ham`) kullan.
|
||||
|
||||
## 1. Hazırlık (5 dk)
|
||||
|
||||
```bash
|
||||
git switch -c veri/2027 # main'de çalışma
|
||||
```
|
||||
|
||||
`src/lib/veri-yillari.ts` dosyasında **tek satırı** değiştir:
|
||||
|
||||
```ts
|
||||
export const SON_YIL = 2027;
|
||||
```
|
||||
|
||||
Betikler ve site yılı yalnız buradan okur. Betiğe verdiğin `--yil` bununla aynı
|
||||
değilse betik durur (yanlış kolona yazmayı önler).
|
||||
|
||||
## 2. Kopya al — asıl dosyaya dokunma (1 dk)
|
||||
|
||||
Dev sunucusu açıksa kapat. Sonra:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/veri-2027
|
||||
sqlite3 data/yokatlas.db ".backup '/tmp/veri-2027/once.db'" # güncelleme ÖNCESİ hâl (geri dönüş + karşılaştırma)
|
||||
cp /tmp/veri-2027/once.db /tmp/veri-2027/yeni.db # üzerinde çalışılacak kopya
|
||||
```
|
||||
|
||||
Bundan sonraki her adım **`/tmp/veri-2027/yeni.db`** üzerinde koşar. `data/yokatlas.db`
|
||||
ancak 8. adımda, her şey yeşilken değişir.
|
||||
|
||||
## 3. Yeni yılı çek (3 dk)
|
||||
|
||||
```bash
|
||||
pnpm refresh --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Ne yapar: şemaya `sira2027/puan2027/kontenjan2027/yerlesen2027` kolonlarını ekler,
|
||||
**yıl kilidini** sınar, tüm kılavuzu çeker, ham hâlini `data/ham/2027-<tarih>.jsonl.gz`
|
||||
olarak saklar (git'e girmez), sonra tek seferde DB'ye yazar.
|
||||
|
||||
Görmen gerekenler:
|
||||
|
||||
```
|
||||
Yıl kilidi: basariSirasi1 = sira2026 → 480/480 (%100.0); eksiz basariSirasi = sira2026 → 0/480 (%0.0)
|
||||
Çekilen: 21… kayıt, benzersiz kod: 21…, kodsuz: 0, API toplamı: 21… ← üç sayı aynı
|
||||
Güncellenen: 19…, eklenen: 1…, atlanan: 0
|
||||
```
|
||||
|
||||
- **Yıl kilidi** şunu kanıtlar: servisin "bir önceki yıl" diye verdiği sıralar bizim
|
||||
2026 kolonumuzla birebir aynı **ve** "bu yıl" diye verdikleri ondan farklı. Yani
|
||||
yazacağımız veri gerçekten 2027'dir. (Servisteki `yil` alanına güvenilmez; 2026'da
|
||||
bu alan yüzünden 2025 verisi bozulmak üzereydi.)
|
||||
- "atlanan" 0 değilse satırlar altında listelenir: kaynağın puan türü vermediği yeni
|
||||
programlardır; uydurulmaz. Sayıyı not et.
|
||||
- Bu betik mevcut programların **eski yıl kolonlarına dokunmaz**; dokunursa kendi
|
||||
sağlaması bunu yakalar ve her şeyi geri alır.
|
||||
|
||||
Aynı işlemi servise gitmeden tekrarlamak için (ör. 8. adımda):
|
||||
`pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz`
|
||||
|
||||
## 4. Geçmiş boşluklarını doldur (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm gecmis-doldur --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Kılavuz kodu değişen ya da yeni koda taşınan programların eski yıllarını tamamlar.
|
||||
Yalnız **boş** alanı doldurur; dolu değeri asla ezmez, tahmin yapmaz. Kaynak sırası:
|
||||
servisin kendi geçmiş alanları → servisin bildirdiği eski kod → aynı üniversitede
|
||||
bire bir aynı adlı ve en az bir yılı kanıtla örtüşen tek satır.
|
||||
|
||||
Çıktının sonundaki "**Dolu olup kaynağın FARKLI söylediği alanlar**" listesine bak.
|
||||
Birkaç onluk fark olağandır (kaynak eski sıraları ufak düzeltmelerle yeniden yayımlıyor;
|
||||
2026'da `sira2024` farklarının %95'i on binde birin altındaydı). **Binlerce satırda büyük
|
||||
fark** görürsen dur ve veri mühendisine sor.
|
||||
|
||||
## 5. Kılavuzdan çıkan programlar: önce sadece raporla (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db --kuru
|
||||
```
|
||||
|
||||
Hiçbir şey yazmaz. Şunu söyler: bu yılın kılavuzunda **olmayan** kaç satır var ve
|
||||
bunlar siteden kalkarsa hangi `/bolum/...` ve `/universite/...` sayfaları tamamen
|
||||
kapanır (adlarıyla). Bu liste **[BİLAL] kararıdır** — sayfa kapanışı SEO'yu etkiler,
|
||||
gerekirse yönlendirme (redirect) eklenir.
|
||||
|
||||
Onaydan sonra gerçek taşıma (silmez; satırlar `programs_arsiv` tablosuna geçer):
|
||||
|
||||
```bash
|
||||
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Neden gerekli: arşivlenmezse eski kodlu satırlar listelerde bir yıl önceki sırayla
|
||||
görünmeye devam eder; öğrenci o kodu tercih ekranında bulamaz, kodu değişen program
|
||||
iki kez listelenir.
|
||||
|
||||
## 6. İndeks + dosyayı toparla (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm db:index --db /tmp/veri-2027/yeni.db --vacuum
|
||||
```
|
||||
|
||||
Son satırda `USING INDEX idx_programs_tur_onlisans_efektif` yazmalı. Yazmıyorsa betik
|
||||
zaten hata verir: sitedeki sıralama aralığı sorguları yavaşlar.
|
||||
|
||||
## 7. Kalite kapısı (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm veri-kalite --yil 2027 --db /tmp/veri-2027/yeni.db --onceki /tmp/veri-2027/once.db --arsivli
|
||||
echo "çıkış kodu: $?" # 0 olmalı
|
||||
```
|
||||
|
||||
(`--arsivli`'yi yalnız 5. adımda gerçek arşivlemeyi yaptıysan ekle.)
|
||||
|
||||
- Her satır `GEÇTİ`, `KALDI`, `UYARI` ya da `bilgi` ile başlar. **Tek bir `KALDI` varsa
|
||||
yayına çıkılmaz**; çıkış kodu 1 olur.
|
||||
- En önemli iki satır:
|
||||
- `önceki DB: geçmiş yıllar korunmuş — … değişen/silinen satır: 0`
|
||||
- `sira2027/sira2026 ∈ [0,5; 2] — … (%9x)` — programların büyük çoğunluğunun sırası
|
||||
bir yılda yarıya inmez, iki katına çıkmaz. Oran düşükse yıllar kaymıştır.
|
||||
- `UYARI`'lar bilgilendirir (ör. ili boş 110 program, yetim net satırları); yeni bir
|
||||
uyarı türü ya da sayıda sıçrama varsa veri mühendisine göster.
|
||||
|
||||
Elle 3 program seç (biri çok bilinen: ör. Boğaziçi Bilgisayar Mühendisliği), YÖK Atlas
|
||||
sitesindeki 2027 taban sırasıyla karşılaştır:
|
||||
|
||||
```bash
|
||||
sqlite3 /tmp/veri-2027/yeni.db "SELECT id, universite, isim, sira2026, sira2027, puan2027 FROM programs WHERE isim LIKE 'Bilgisayar Mühendisliği%' AND universite LIKE 'BOĞAZİÇİ%';"
|
||||
```
|
||||
|
||||
## 8. Yerine koy ve siteyi kontrol et (5 dk)
|
||||
|
||||
```bash
|
||||
cp /tmp/veri-2027/yeni.db data/yokatlas.db
|
||||
rm -f data/yokatlas.db-wal data/yokatlas.db-shm # eski dosyanın artıkları; yenisi temiz (checkpoint yapılmış)
|
||||
pnpm build # katalog sayfaları yeni veriden üretilir; hata vermemeli
|
||||
```
|
||||
|
||||
Netler (yerleşen son kişinin netleri) ayrı bir servisten gelir ve genelde daha geç
|
||||
güncellenir; hazır olduğunda:
|
||||
|
||||
```bash
|
||||
pnpm detay --yil 2027 --db data/yokatlas.db # ~127 istek, ~2 dk; yıl sağlaması tutmazsa durur
|
||||
```
|
||||
|
||||
`--kirilim` bayrağını **kullanma**: kontenjan kırılımını (okul birincisi, şehit-gazi…)
|
||||
sitede okuyan kod yok ve servis bu alanları Eylül 2026'dan beri yayımlamıyor. Bayrak
|
||||
verilse bile alanlar yoksa adım kendini durdurur, mevcut değerlerin üstüne boş yazmaz.
|
||||
|
||||
Yıl değişince koddaki metinler (`"son 6 yıl"`, başlıklardaki yıl vb.) `veri-yillari.ts`
|
||||
sabitlerinden türediği için kendiliğinden güncellenir. Elle yıl yazılmış yer kaldıysa bul:
|
||||
|
||||
```bash
|
||||
grep -rnE "sira20[0-9]{2}|20(2[1-9])[–-]20" src scripts --include='*.ts' --include='*.tsx' | grep -v veri-yillari
|
||||
```
|
||||
|
||||
Tadımlık havuzu ve kayıtlı raporlar eski sıralarla üretilmiştir → yayından sonra prod'da
|
||||
`pnpm tadimlik --force` (Bilal koşar; ayrıntı `scripts/tadimlik-uret.ts` başlığında).
|
||||
|
||||
## 9. Commit ve yayın
|
||||
|
||||
```bash
|
||||
git add src/lib/veri-yillari.ts data/yokatlas.db
|
||||
git commit -m "veri: 2027 yerleştirme verisi"
|
||||
```
|
||||
|
||||
`data/ham/` git'e girmez (yerelde sakla; bir sonraki yıl karşılaştırma için işe yarar).
|
||||
Push öncesi `AGENTS.md`'deki "Push öncesi build zorunlu" kuralı geçerli. Yayın kararı Bilal'in.
|
||||
|
||||
## Geri alma
|
||||
|
||||
| Ne zaman | Nasıl | Süre |
|
||||
|---|---|---|
|
||||
| 8. adımdan önce | Hiçbir şey yapma; `data/yokatlas.db` hiç değişmedi. `/tmp/veri-2027/` silinebilir. | 0 |
|
||||
| 8. adımdan sonra, commit'ten önce | `git checkout -- data/yokatlas.db data/yokatlas.db-wal data/yokatlas.db-shm` | 1 dk |
|
||||
| Commit/yayından sonra | `git revert <commit>` + yeniden yayın. `SON_YIL` da aynı commit'te geri döner; site bir önceki yılın verisiyle tutarlı çalışır. | 10 dk |
|
||||
| Yalnız arşivlemeyi geri almak | `INSERT INTO programs SELECT * FROM programs_arsiv; DELETE FROM programs_arsiv;` (kopyada dene, sonra `pnpm db:index`). Netler yeni kodda kalır; zararsız. | 5 dk |
|
||||
|
||||
`/tmp/veri-2027/once.db` dosyasını yayından bir hafta sonrasına kadar silme.
|
||||
|
||||
## Bir şey durursa
|
||||
|
||||
| Mesaj | Anlamı | Ne yap |
|
||||
|---|---|---|
|
||||
| `--yil … ≠ SON_YIL` | 1. adım atlanmış | `veri-yillari.ts`'i güncelle |
|
||||
| `eksiz alanları hâlâ <geçen yıl> verisi` | YÖK Atlas henüz yeni yılı yayımlamamış | Bekle, bir hafta sonra tekrar dene |
|
||||
| `"…1" alanları DB'deki … verisiyle eşleşmiyor` | Servis bir yıl daha ilerlemiş (bir yılı atladık) **ya da** servis biçim değiştirmiş **ya da** yanlış DB | Yazma. Veri mühendisine göster |
|
||||
| `Benzersiz kod sayısı … ≠ API toplamı` | Çekim sırasında servis sayfaları kaydırdı | Birkaç dakika sonra 3. adımı tekrarla (DB'ye bir şey yazılmadı) |
|
||||
| `Aynı üniversitede birden fazla unitur` | Bir kurumun türü iki farklı yazılmış | Listelenen kurumları veri mühendisine göster; `scripts/unitur-onar.ts` |
|
||||
| `Güncel satır sayısı … refresh kaydıyla uyuşmuyor` (arşivle) | refresh yarım kalmış | 3. adımı tekrarla; arşivleme bilerek çalışmıyor |
|
||||
| `veri-kalite` içinde `KALDI` | O kontrol tutmadı | Yayına çıkma; satırdaki sayıyı ve örnek kodları veri mühendisine ilet |
|
||||
|
||||
## Betikler ve dokundukları
|
||||
|
||||
| Betik | Yazar mı? | Ne yazar |
|
||||
|---|---|---|
|
||||
| `scripts/yokatlas-goc.ts` (`pnpm yokatlas:goc`) | evet | Yalnız şema: yıl kolonları, `son_kilavuz_yili`, `eski_kod`, `programs_arsiv`, `veri_meta`. refresh bunu kendisi çağırır. |
|
||||
| `scripts/refresh.ts` | evet | `sira/puan/kontenjan/yerlesen<yıl>`, `son_kilavuz_yili`, `eski_kod`; yeni programlar; ham dosya |
|
||||
| `scripts/gecmis-doldur.ts` | evet | Yalnız **NULL** geçmiş yıl alanları + `eski_kod` |
|
||||
| `scripts/eski-arsivle.ts` | `--kuru` ile hayır | Satır taşıma `programs` → `programs_arsiv`; `netler.program_id` yeni koda |
|
||||
| `scripts/db-index.ts` | evet | İndeks, `--vacuum`, WAL checkpoint |
|
||||
| `scripts/veri-kalite.ts` | **hayır** | Salt okunur; çıkış kodu 0/1 |
|
||||
| `scripts/detay.ts` | evet | `netler`; `--kirilim` ile (alanlar varsa) bir önceki yılın kırılım kolonları |
|
||||
|
||||
Kolon sözlüğü: `son_kilavuz_yili` = programın görüldüğü son tercih kılavuzu (NULL = 2024
|
||||
ve öncesi, bilinmiyor). `eski_kod` = kılavuz kodu değiştiyse bir önceki kod — kayıtlı
|
||||
listelerdeki eski kodları yeni koda çevirmek için `SELECT id FROM programs WHERE eski_kod = ?`.
|
||||
@@ -10,6 +10,11 @@
|
||||
"ingest": "tsx scripts/ingest.ts",
|
||||
"refresh": "tsx scripts/refresh.ts",
|
||||
"detay": "tsx scripts/detay.ts",
|
||||
"yokatlas:goc": "tsx scripts/yokatlas-goc.ts",
|
||||
"gecmis-doldur": "tsx scripts/gecmis-doldur.ts",
|
||||
"eski-arsivle": "tsx scripts/eski-arsivle.ts",
|
||||
"veri-kalite": "tsx scripts/veri-kalite.ts",
|
||||
"db:index": "tsx scripts/db-index.ts",
|
||||
"logolar": "tsx scripts/logo-indir.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
|
||||
@@ -1,44 +1,83 @@
|
||||
// Sorgu yolundaki ifade indexi + WAL bakımı. Bir kez çalıştırılır
|
||||
// (pnpm tsx scripts/db-index.ts); ingest/refresh sonrası da güvenle
|
||||
// tekrar çalıştırılabilir (IF NOT EXISTS).
|
||||
// Sorgu yolundaki ifade indexi + WAL bakımı. refresh sonrası (ve istenince tek
|
||||
// başına) çalıştırılır; tekrar çalıştırmak güvenlidir.
|
||||
//
|
||||
// src/lib/db.ts tüm aralık sorgularını COALESCE(sira2025, sira2024)
|
||||
// üzerinden yapar; SQLite ifade indexleri METİNSEL eşleştiği için buradaki
|
||||
// ifade oradaki EFEKTIF_SIRA sabitiyle birebir aynı olmalıdır. Bu index
|
||||
// olmadan her sorgu tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
|
||||
// src/lib/db.ts tüm aralık sorgularını EFEKTIF_SIRA_SQL üzerinden yapar;
|
||||
// SQLite ifade indexleri METİNSEL eşleştiği için indeks ifadesi
|
||||
// src/lib/veri-yillari.ts'teki sabitle birebir aynı olmalıdır — bu yüzden
|
||||
// buraya elle yazılmaz, import edilir. Bu index olmadan her sorgu
|
||||
// tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
|
||||
//
|
||||
// Kullanım: pnpm db:index --db data/yokatlas.db [--vacuum]
|
||||
// --vacuum: arşivleme gibi büyük silmelerden sonra dosyayı küçültür (site kapalıyken).
|
||||
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import { EFEKTIF_SIRA_SQL } from "../src/lib/veri-yillari";
|
||||
import { bayrakVar, calistir, dbAc, dbArgumani, dur, type Db } from "./yokatlas-goc";
|
||||
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
export const EFEKTIF_INDEKS = "idx_programs_tur_onlisans_efektif";
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
|
||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
||||
`);
|
||||
|
||||
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite detaylarını
|
||||
// ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index olmadan her
|
||||
// sayfa tam tablo taraması yapar.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
||||
`);
|
||||
|
||||
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
|
||||
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
/**
|
||||
* Efektif sıra indeksini güncel ifadeyle kurar. Eski yılın ifadesiyle kurulmuş
|
||||
* aynı adlı indeks varsa DROP edip yeniden kurar (yıl değişince ifade değişir).
|
||||
*/
|
||||
export function efektifIndeksiKur(db: Db): "kuruldu" | "yenilendi" | "guncel" {
|
||||
const mevcut = db
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
||||
.get(EFEKTIF_INDEKS) as { sql: string } | undefined;
|
||||
let sonuc: "kuruldu" | "yenilendi" | "guncel" = "kuruldu";
|
||||
if (mevcut) {
|
||||
if (mevcut.sql.includes(EFEKTIF_SIRA_SQL)) return "guncel";
|
||||
db.exec(`DROP INDEX ${EFEKTIF_INDEKS}`);
|
||||
sonuc = "yenilendi";
|
||||
}
|
||||
db.exec(
|
||||
`CREATE INDEX ${EFEKTIF_INDEKS} ON programs (tur, onlisans, ${EFEKTIF_SIRA_SQL})`
|
||||
);
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
/** Sihirbaz/liste aralık sorgusunun planı (src/lib/db.ts dilimAra ile aynı biçim). */
|
||||
export function efektifSorguPlani(db: Db): string[] {
|
||||
const plan = db
|
||||
.prepare(
|
||||
`EXPLAIN QUERY PLAN
|
||||
SELECT id FROM programs
|
||||
WHERE tur = ? AND onlisans = ?
|
||||
AND COALESCE(sira2025, sira2024) IS NOT NULL
|
||||
AND COALESCE(sira2025, sira2024) BETWEEN ? AND ?
|
||||
ORDER BY COALESCE(sira2025, sira2024) ASC LIMIT 10`,
|
||||
AND ${EFEKTIF_SIRA_SQL} IS NOT NULL
|
||||
AND ${EFEKTIF_SIRA_SQL} BETWEEN ? AND ?
|
||||
ORDER BY ${EFEKTIF_SIRA_SQL} ASC LIMIT 10`
|
||||
)
|
||||
.all("SAYISAL", 0, 1000, 50000);
|
||||
.all("SAYISAL", 0, 1000, 50000) as { detail: string }[];
|
||||
return plan.map((p) => p.detail);
|
||||
}
|
||||
|
||||
console.log("Sorgu planı:", JSON.stringify(plan, null, 2));
|
||||
if (process.argv[1]?.endsWith("db-index.ts")) {
|
||||
calistir(() => {
|
||||
const db = dbAc(dbArgumani());
|
||||
console.log(`Efektif indeks (${EFEKTIF_SIRA_SQL}): ${efektifIndeksiKur(db)}`);
|
||||
|
||||
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite
|
||||
// detaylarını ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index
|
||||
// olmadan her sayfa tam tablo taraması yapar.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
||||
`);
|
||||
|
||||
if (bayrakVar("vacuum")) {
|
||||
db.exec("VACUUM");
|
||||
console.log("VACUUM tamam.");
|
||||
}
|
||||
|
||||
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
|
||||
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
|
||||
const plan = efektifSorguPlani(db);
|
||||
console.log("Sorgu planı:");
|
||||
for (const satir of plan) console.log(` ${satir}`);
|
||||
db.close();
|
||||
if (!plan.some((s) => s.includes(EFEKTIF_INDEKS))) {
|
||||
dur(`Sorgu planı ${EFEKTIF_INDEKS} indeksini kullanmıyor.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
348
scripts/detay.ts
348
scripts/detay.ts
@@ -1,45 +1,71 @@
|
||||
/**
|
||||
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler:
|
||||
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler.
|
||||
* İki BAĞIMSIZ adım:
|
||||
*
|
||||
* 1. Kontenjan türü kırılımı (son yerleştirme yılı, ör. 2025):
|
||||
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın
|
||||
* için kontenjan + yerleşen. Kaynak: POST /api/tercih-kilavuz/search
|
||||
* (gk1/gkY1, obk1/obkY1, sgy1/sgyY1, dprm1/dprmY1, y34_1/y34Y1 —
|
||||
* "1" son yerleştirme yılı demek; kayıttaki `yil` kılavuz yılıdır,
|
||||
* veri yılı = yil - 1).
|
||||
* A) Yerleşen son kişinin netleri (varsayılan; `--netler-atla` ile kapatılır)
|
||||
* Kaynak: POST /api/netler/search → `netler` tablosu. Bu API her satırda
|
||||
* verinin ait olduğu yılı `yil` alanında verir; betik bunu körlemesine
|
||||
* kabul etmez: ilk sayfada `tabanPuan`, DB'deki puan<yil> ile ≥%95
|
||||
* eşleşmiyorsa DURUR (yıl etiketi kaymış demektir).
|
||||
*
|
||||
* 2. Yerleşen son kişinin netleri (yıl bazında, tüm puan türleri):
|
||||
* Kaynak: POST /api/netler/search → `netler` tablosu.
|
||||
* B) Kontenjan türü kırılımı — YALNIZ `--kirilim` bayrağıyla (varsayılan KAPALI)
|
||||
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın için
|
||||
* kontenjan + yerleşen; API'nin `…1` alanları → <yil-1> kolonları.
|
||||
* `--yil`'in kendi kırılım kolonları BİLEREK yok (src/ içinde okuyan kod
|
||||
* yok; KARARLAR 2026-09-21).
|
||||
* Emniyet: (1) refresh ile aynı YIL KİLİDİ; (2) beklenen on alanın HER
|
||||
* BİRİ ilk sayfada en az bir kayıtta bulunmalı — API null alanları hiç
|
||||
* göndermediği için "ilk kayıt" tek başına yanıltır; bir alan ilk sayfanın
|
||||
* tamamında `undefined` ise API o alanı artık yayımlamıyordur ve adım
|
||||
* HİÇBİR ŞEY yazmadan DURUR; (3) yazım `COALESCE(yeni, mevcut)` —
|
||||
* mevcut değerin üstüne asla NULL yazılmaz.
|
||||
* (Eylül 2026: API `gkY1/obk1/…` alanlarını yayımlamıyor → bu adım durur;
|
||||
* 2025 kırılımı DB'de Ağustos 2026 çekiminden duruyor, korunur.)
|
||||
*
|
||||
* Yıl HİÇBİR adımda kılavuz kaydındaki `yil` alanından türetilmez.
|
||||
*
|
||||
* Not: YÖK Atlas 2026'da SPA'ya geçti; eski content/lisans-dynamic/*.php
|
||||
* panelleri (ve onlarla birlikte kadın-erkek dağılımı) yayından kalktı.
|
||||
* Cinsiyet verisi yeni API'lerin hiçbirinde yok.
|
||||
*
|
||||
* Kullanım: npx tsx scripts/detay.ts
|
||||
* Kullanım:
|
||||
* pnpm detay --yil 2026 --db /tmp/deneme.db
|
||||
* pnpm detay --yil 2026 --db /tmp/deneme.db --kirilim --netler-atla
|
||||
*/
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import type Database from "better-sqlite3";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { yilKilidi, type KilavuzKaydi } from "./refresh";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
yilArgumani,
|
||||
type Db,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const KILAVUZ_API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
|
||||
const PAGE_SIZE = 500;
|
||||
const DELAY_MS = 300;
|
||||
const DELAY_MS = 500;
|
||||
|
||||
type KilavuzRecord = {
|
||||
kilavuzKodu: number;
|
||||
eskiKilavuzKodu?: number | null;
|
||||
yil: number;
|
||||
gk1?: number | null;
|
||||
gkY1?: number | null;
|
||||
obk1?: number | null;
|
||||
obkY1?: number | null;
|
||||
sgy1?: number | null;
|
||||
sgyY1?: number | null;
|
||||
dprm1?: number | null;
|
||||
dprmY1?: number | null;
|
||||
y34_1?: number | null;
|
||||
y34Y1?: number | null;
|
||||
};
|
||||
/** API alanı → DB kolon öneki. `…1` = bir önceki yerleştirme yılı. */
|
||||
const KIRILIM_ALANLARI = [
|
||||
["gk1", "gk"],
|
||||
["gkY1", "gkY"],
|
||||
["obk1", "obk"],
|
||||
["obkY1", "obkY"],
|
||||
["sgy1", "sgy"],
|
||||
["sgyY1", "sgyY"],
|
||||
["dprm1", "dprm"],
|
||||
["dprmY1", "dprmY"],
|
||||
["y34_1", "y34k"],
|
||||
["y34Y1", "y34kY"],
|
||||
] as const;
|
||||
|
||||
type KirilimKaydi = KilavuzKaydi & Record<string, unknown>;
|
||||
|
||||
type NetlerRecord = {
|
||||
kilavuzKodu: number;
|
||||
@@ -113,96 +139,9 @@ const NET_COLS = [
|
||||
"ydtYdilNet",
|
||||
] as const;
|
||||
|
||||
async function main() {
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
// ── A) Netler ────────────────────────────────────────────────────────────
|
||||
|
||||
// ── 1. Kontenjan türü kırılımı ─────────────────────────────────────────
|
||||
const first = await fetchPage<KilavuzRecord>(KILAVUZ_API, 0);
|
||||
// API kaydındaki yil kılavuz yılı; "1" ekli alanlar bir önceki
|
||||
// yerleştirme dönemine ait (2026 kılavuzu → 2025 verisi)
|
||||
const veriYili = first.content[0].yil - 1;
|
||||
|
||||
const kontCols = [
|
||||
`gk${veriYili}`,
|
||||
`gkY${veriYili}`,
|
||||
`obk${veriYili}`,
|
||||
`obkY${veriYili}`,
|
||||
`sgy${veriYili}`,
|
||||
`sgyY${veriYili}`,
|
||||
`dprm${veriYili}`,
|
||||
`dprmY${veriYili}`,
|
||||
`y34k${veriYili}`,
|
||||
`y34kY${veriYili}`,
|
||||
];
|
||||
const mevcut = new Set(
|
||||
(db.prepare("PRAGMA table_info(programs)").all() as { name: string }[]).map(
|
||||
(c) => c.name
|
||||
)
|
||||
);
|
||||
for (const col of kontCols) {
|
||||
if (!mevcut.has(col)) {
|
||||
db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
|
||||
}
|
||||
}
|
||||
|
||||
const updateKont = db.prepare(`
|
||||
UPDATE programs SET ${kontCols.map((c) => `${c} = ?`).join(", ")}
|
||||
WHERE id = ?
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
// Kılavuz kodu yıllar arasında değişebiliyor (eskiKilavuzKodu); netler
|
||||
// API'si yeni kodla döndüğü için eşleşen DB id'sini haritada tutuyoruz.
|
||||
const dbId = new Map<number, string>();
|
||||
let kontUpdated = 0;
|
||||
let kontMissing = 0;
|
||||
|
||||
const processKilavuz = db.transaction((records: KilavuzRecord[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
let id = String(r.kilavuzKodu);
|
||||
if (!exists.get(id)) {
|
||||
const eski = r.eskiKilavuzKodu ? String(r.eskiKilavuzKodu) : null;
|
||||
if (eski && exists.get(eski)) {
|
||||
id = eski;
|
||||
} else {
|
||||
kontMissing++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dbId.set(r.kilavuzKodu, id);
|
||||
updateKont.run(
|
||||
num(r.gk1),
|
||||
num(r.gkY1),
|
||||
num(r.obk1),
|
||||
num(r.obkY1),
|
||||
num(r.sgy1),
|
||||
num(r.sgyY1),
|
||||
num(r.dprm1),
|
||||
num(r.dprmY1),
|
||||
num(r.y34_1),
|
||||
num(r.y34Y1),
|
||||
id
|
||||
);
|
||||
kontUpdated++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${first.totalPages} sayfa…`
|
||||
);
|
||||
processKilavuz(first.content);
|
||||
for (let p = 1; p < first.totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
processKilavuz((await fetchPage<KilavuzRecord>(KILAVUZ_API, p)).content);
|
||||
process.stdout.write(`\rkontenjan sayfa ${p + 1}/${first.totalPages}`);
|
||||
}
|
||||
console.log(
|
||||
`\nKontenjan kırılımı: ${kontUpdated} güncellendi, ${kontMissing} DB'de yok`
|
||||
);
|
||||
|
||||
// ── 2. Yerleşen son kişinin netleri ────────────────────────────────────
|
||||
async function netlerAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS netler (
|
||||
program_id TEXT NOT NULL,
|
||||
@@ -214,54 +153,165 @@ async function main() {
|
||||
);
|
||||
`);
|
||||
|
||||
const insertNet = db.prepare(`
|
||||
INSERT OR REPLACE INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
||||
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
||||
`);
|
||||
const first = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||
|
||||
let netRows = 0;
|
||||
// Yıl etiketi sağlaması: netler.tabanPuan(yil=Y) ≈ programs.puanY
|
||||
const programKolonlari = new Set(kolonAdlari(db, "programs"));
|
||||
const puanGetir = new Map<number, Database.Statement<unknown[]>>();
|
||||
let kiyas = 0;
|
||||
let esit = 0;
|
||||
for (const r of first.content) {
|
||||
if (!r.kilavuzKodu || !r.yil || num(r.tabanPuan) == null) continue;
|
||||
if (r.yil < ILK_YIL || r.yil > yil || !programKolonlari.has(`puan${r.yil}`)) continue;
|
||||
let st = puanGetir.get(r.yil);
|
||||
if (!st) {
|
||||
st = db.prepare(`SELECT puan${r.yil} AS p FROM programs WHERE id = ?`);
|
||||
puanGetir.set(r.yil, st);
|
||||
}
|
||||
const satir = st.get(String(r.kilavuzKodu)) as { p: number | null } | undefined;
|
||||
if (!satir || satir.p == null) continue;
|
||||
kiyas++;
|
||||
if (Math.abs(satir.p - (r.tabanPuan as number)) <= 0.01) esit++;
|
||||
}
|
||||
const oran = kiyas ? esit / kiyas : 0;
|
||||
console.log(`Netler yıl sağlaması: tabanPuan = puan<yil> → ${esit}/${kiyas} (%${(oran * 100).toFixed(1)})`);
|
||||
if (kiyas < 50 || oran < 0.95) {
|
||||
dur(
|
||||
"Netler API'sindeki `yil` etiketi DB'deki yıl kolonlarıyla örtüşmüyor " +
|
||||
"(ya da karşılaştıracak veri yok). Netler yazılmadı."
|
||||
);
|
||||
}
|
||||
|
||||
// Mevcut değerin üstüne NULL yazılmaz: çakışmada COALESCE(yeni, eski).
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
||||
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
||||
ON CONFLICT(program_id, yil) DO UPDATE SET
|
||||
${NET_COLS.map((c) => `${c} = COALESCE(excluded.${c}, ${c})`).join(",\n ")}
|
||||
`);
|
||||
const programVar = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
let yazilan = 0;
|
||||
let programYok = 0;
|
||||
let yilDisi = 0;
|
||||
const processNetler = db.transaction((records: NetlerRecord[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu || !r.yil) continue;
|
||||
const id = dbId.get(r.kilavuzKodu) ?? String(r.kilavuzKodu);
|
||||
insertNet.run(
|
||||
if (r.yil < ILK_YIL || r.yil > yil) {
|
||||
yilDisi++;
|
||||
continue;
|
||||
}
|
||||
const id = String(r.kilavuzKodu);
|
||||
// Yetim satır üretme: program DB'de yoksa (refresh koşulmamış / atlanmış) sayıp geç.
|
||||
if (!programVar.get(id)) {
|
||||
programYok++;
|
||||
continue;
|
||||
}
|
||||
upsert.run(
|
||||
id,
|
||||
r.yil,
|
||||
r.puanTuru?.trim() ?? null,
|
||||
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
|
||||
);
|
||||
netRows++;
|
||||
yazilan++;
|
||||
}
|
||||
});
|
||||
|
||||
const netFirst = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||
console.log(
|
||||
`Netler: ${netFirst.totalElements} kayıt, ${netFirst.totalPages} sayfa…`
|
||||
);
|
||||
processNetler(netFirst.content);
|
||||
for (let p = 1; p < netFirst.totalPages; p++) {
|
||||
const sayfa = Math.min(first.totalPages, azamiSayfa);
|
||||
console.log(`Netler: ${first.totalElements} kayıt, ${first.totalPages} sayfa${sayfa < first.totalPages ? ` (yalnız ilk ${sayfa})` : ""}…`);
|
||||
processNetler(first.content);
|
||||
for (let p = 1; p < sayfa; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
processNetler((await fetchPage<NetlerRecord>(NETLER_API, p)).content);
|
||||
process.stdout.write(`\rnetler sayfa ${p + 1}/${netFirst.totalPages}`);
|
||||
process.stdout.write(`\rnetler sayfa ${p + 1}/${sayfa}`);
|
||||
}
|
||||
console.log(`\nNetler: ${netRows} satır yazıldı`);
|
||||
|
||||
const ozet = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT COUNT(*) FROM programs WHERE gkY${veriYili} IS NOT NULL) AS kontlu,
|
||||
(SELECT COUNT(*) FROM netler) AS net,
|
||||
(SELECT COUNT(DISTINCT program_id) FROM netler) AS netli_program`
|
||||
)
|
||||
.get() as { kontlu: number; net: number; netli_program: number };
|
||||
const dagilim = db
|
||||
.prepare("SELECT yil, COUNT(*) AS n FROM netler GROUP BY yil ORDER BY yil")
|
||||
.all() as { yil: number; n: number }[];
|
||||
console.log(
|
||||
`Özet → kontenjan kırılımı olan program: ${ozet.kontlu}, ` +
|
||||
`net satırı: ${ozet.net} (${ozet.netli_program} program)`
|
||||
`\nNetler: ${yazilan} satır yazıldı/güncellendi; programı DB'de olmayan: ${programYok}; yıl aralığı dışı: ${yilDisi}`
|
||||
);
|
||||
db.close();
|
||||
console.log(`Netler yıl dağılımı: ${dagilim.map((d) => `${d.yil}=${d.n}`).join(" ")}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
// ── B) Kontenjan kırılımı ────────────────────────────────────────────────
|
||||
|
||||
async function kirilimAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
|
||||
const veriYili = yil - 1; // `…1` alanları; yıl kilidiyle doğrulanır, r.yil'e bakılmaz
|
||||
const first = await fetchPage<KirilimKaydi>(KILAVUZ_API, 0);
|
||||
yilKilidi(db, yil, first.content);
|
||||
|
||||
const eksik = KIRILIM_ALANLARI.filter(
|
||||
([alan]) => !first.content.some((r) => r[alan] !== undefined)
|
||||
).map(([alan]) => alan);
|
||||
if (eksik.length) {
|
||||
dur(
|
||||
`Kırılım alanları API'de yok (ilk sayfanın tamamında undefined): ${eksik.join(", ")}. ` +
|
||||
`${veriYili} kırılım kolonlarına DOKUNULMADI (mevcut değerler korunuyor).`
|
||||
);
|
||||
}
|
||||
|
||||
const kontCols = KIRILIM_ALANLARI.map(([, onek]) => `${onek}${veriYili}`);
|
||||
const mevcut = new Set(kolonAdlari(db, "programs"));
|
||||
for (const col of kontCols) {
|
||||
if (!mevcut.has(col)) db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
|
||||
}
|
||||
const updateKont = db.prepare(`
|
||||
UPDATE programs SET ${kontCols.map((c) => `${c} = COALESCE(?, ${c})`).join(", ")}
|
||||
WHERE id = ?
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
let guncellenen = 0;
|
||||
let dbdeYok = 0;
|
||||
const isle = db.transaction((records: KirilimKaydi[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
const id = String(r.kilavuzKodu);
|
||||
if (!exists.get(id)) {
|
||||
dbdeYok++;
|
||||
continue;
|
||||
}
|
||||
updateKont.run(...KIRILIM_ALANLARI.map(([alan]) => num(r[alan])), id);
|
||||
guncellenen++;
|
||||
}
|
||||
});
|
||||
|
||||
const sayfa = Math.min(first.totalPages, azamiSayfa);
|
||||
console.log(`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${sayfa} sayfa…`);
|
||||
isle(first.content);
|
||||
for (let p = 1; p < sayfa; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
isle((await fetchPage<KirilimKaydi>(KILAVUZ_API, p)).content);
|
||||
process.stdout.write(`\rkırılım sayfa ${p + 1}/${sayfa}`);
|
||||
}
|
||||
console.log(`\nKontenjan kırılımı: ${guncellenen} güncellendi, ${dbdeYok} DB'de yok`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const db = dbAc(dbArgumani());
|
||||
// Yalnız deneme için: kaynağı yormadan ilk N sayfayla dur.
|
||||
const azamiSayfa = Number.parseInt(argOku("azami-sayfa") ?? "", 10) || Number.POSITIVE_INFINITY;
|
||||
|
||||
let hata: unknown = null;
|
||||
if (!bayrakVar("netler-atla")) {
|
||||
try {
|
||||
await netlerAdimi(db, yil, azamiSayfa);
|
||||
} catch (err) {
|
||||
hata = err; // adımlar bağımsız: netler dursa da kırılım (istenmişse) denenir
|
||||
console.error(`Netler adımı durdu: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
if (bayrakVar("kirilim")) {
|
||||
await kirilimAdimi(db, yil, azamiSayfa);
|
||||
} else {
|
||||
console.log("Kontenjan kırılımı atlandı (yalnız --kirilim ile koşar).");
|
||||
}
|
||||
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
if (hata) throw hata;
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("detay.ts")) calistir(main);
|
||||
|
||||
228
scripts/eski-arsivle.ts
Normal file
228
scripts/eski-arsivle.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Güncel tercih kılavuzunda OLMAYAN programları `programs` → `programs_arsiv`
|
||||
* tablosuna taşır (silmez; aynı şema, aynı değerler). Kodu değişen programların
|
||||
* çift satırı (eski kod + yeni kod) böylece sorgu koşuluyla değil DB inşasında
|
||||
* çözülür: site yalnız bu yıl tercih edilebilen programları görür.
|
||||
*
|
||||
* `netler.program_id`: arşive giden eski kodun TEK bir güncel karşılığı varsa
|
||||
* (programs.eski_kod) yeni koda çevrilir; yeni kodda o yıl zaten net satırı
|
||||
* varsa eski satır olduğu gibi bırakılır (veri silinmez).
|
||||
*
|
||||
* ÖNCE `--kuru` ile koş: hiçbir şey yazmaz; kaç satır gideceğini ve hangi bölüm
|
||||
* / üniversite sayfalarının (slug) tamamen kapanacağını adlarıyla listeler.
|
||||
* Gerçek koşu [BİLAL] onayı ister — sayfalar siteden kalkar.
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db --kuru
|
||||
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db
|
||||
*/
|
||||
import { bolumBazAdi, turkishSlugify, uniSayfaSlug } from "../src/lib/slug";
|
||||
import {
|
||||
arsivTablosunuGarantiEt,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
metaOku,
|
||||
metaYaz,
|
||||
tabloVar,
|
||||
yilArgumani,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
type Ad = { ad: string; n: number };
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const kuru = bayrakVar("kuru");
|
||||
const db = dbAc(dbYolu, { saltOkunur: kuru });
|
||||
console.log(`DB: ${dbYolu}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
|
||||
|
||||
if (!kolonAdlari(db, "programs").includes("son_kilavuz_yili")) {
|
||||
dur("son_kilavuz_yili kolonu yok: önce refresh koşulmalı.");
|
||||
}
|
||||
|
||||
// ── Emniyet: refresh bu yıl için tam koşmuş olmalı ─────────────────────
|
||||
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
|
||||
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
|
||||
const guncel = (
|
||||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||||
).n;
|
||||
if (!Number.isFinite(apiToplam) || guncel !== apiToplam - atlanan) {
|
||||
dur(
|
||||
`Güncel satır sayısı (${guncel}) refresh kaydıyla (API ${apiToplam} − atlanan ${atlanan}) uyuşmuyor. ` +
|
||||
"Yarım bir refresh'in üstüne arşivleme yapılmaz — güncel programlar da gidebilir."
|
||||
);
|
||||
}
|
||||
|
||||
// NOT: `<>` NULL'ları kaçırır; son_kilavuz_yili bilinmeyen (NULL) satırlar da eskidir.
|
||||
const ESKI = "son_kilavuz_yili IS NOT ?";
|
||||
const eskiSayi = (
|
||||
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${ESKI}`).get(yil) as { n: number }
|
||||
).n;
|
||||
console.log(`\nGüncel (${yil} kılavuzunda): ${guncel} Arşivlenecek: ${eskiSayi}`);
|
||||
if (eskiSayi === 0) {
|
||||
console.log("Arşivlenecek satır yok.");
|
||||
db.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const kirilim = db
|
||||
.prepare(
|
||||
`SELECT COALESCE(CAST(son_kilavuz_yili AS TEXT), 'bilinmiyor (≤2024)') AS y, COUNT(*) AS n,
|
||||
COUNT(sira${yil - 1}) AS onceki_sirali
|
||||
FROM programs WHERE ${ESKI} GROUP BY son_kilavuz_yili ORDER BY son_kilavuz_yili`
|
||||
)
|
||||
.all(yil) as { y: string; n: number; onceki_sirali: number }[];
|
||||
for (const k of kirilim) {
|
||||
console.log(` son kılavuz yılı ${k.y}: ${k.n} satır (${k.onceki_sirali} tanesinde sira${yil - 1} dolu)`);
|
||||
}
|
||||
|
||||
const halefli = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS n FROM programs e WHERE e.${ESKI}
|
||||
AND EXISTS (SELECT 1 FROM programs g WHERE g.eski_kod = e.id AND g.son_kilavuz_yili = ?)`
|
||||
)
|
||||
.get(yil, yil) as { n: number }
|
||||
).n;
|
||||
console.log(
|
||||
` yeni kodla devam eden (eski_kod eşleşmesi): ${halefli}; karşılığı olmayan (kapanmış/ad değişmiş): ${eskiSayi - halefli}`
|
||||
);
|
||||
|
||||
// ── SEO: tamamen kaybolan bölüm / üniversite sayfaları ─────────────────
|
||||
const eskiIsimler = db
|
||||
.prepare(`SELECT isim AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY isim`)
|
||||
.all(yil) as Ad[];
|
||||
const guncelIsimler = db
|
||||
.prepare("SELECT DISTINCT isim AS ad FROM programs WHERE son_kilavuz_yili = ?")
|
||||
.all(yil) as { ad: string }[];
|
||||
const guncelHamIsim = new Set(guncelIsimler.map((r) => r.ad));
|
||||
const guncelBolumSlug = new Set(guncelIsimler.map((r) => turkishSlugify(bolumBazAdi(r.ad))));
|
||||
const kaybolanHamIsim = eskiIsimler.filter((r) => !guncelHamIsim.has(r.ad));
|
||||
const kaybolanBolum = new Map<string, { ad: string; n: number }>();
|
||||
for (const r of eskiIsimler) {
|
||||
const baz = bolumBazAdi(r.ad);
|
||||
const slug = turkishSlugify(baz);
|
||||
if (!slug || guncelBolumSlug.has(slug)) continue;
|
||||
const k = kaybolanBolum.get(slug) ?? { ad: baz, n: 0 };
|
||||
k.n += r.n;
|
||||
kaybolanBolum.set(slug, k);
|
||||
}
|
||||
|
||||
const eskiUniler = db
|
||||
.prepare(`SELECT universite AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY universite`)
|
||||
.all(yil) as Ad[];
|
||||
const guncelUniler = db
|
||||
.prepare("SELECT DISTINCT universite AS ad FROM programs WHERE son_kilavuz_yili = ?")
|
||||
.all(yil) as { ad: string }[];
|
||||
const guncelHamUni = new Set(guncelUniler.map((r) => r.ad));
|
||||
const guncelUniSlug = new Set(guncelUniler.map((r) => uniSayfaSlug(r.ad)));
|
||||
const kaybolanHamUni = eskiUniler.filter((r) => !guncelHamUni.has(r.ad));
|
||||
const kaybolanUni = new Map<string, { ad: string; n: number }>();
|
||||
for (const r of eskiUniler) {
|
||||
const slug = uniSayfaSlug(r.ad);
|
||||
if (!slug || guncelUniSlug.has(slug)) continue;
|
||||
const k = kaybolanUni.get(slug) ?? { ad: r.ad, n: 0 };
|
||||
k.n += r.n;
|
||||
kaybolanUni.set(slug, k);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nTamamen kaybolan ham 'isim' değeri: ${kaybolanHamIsim.length} ` +
|
||||
`(çoğu burs/dil parantezi varyantı; sayfa kaybı aşağıdaki slug listesidir)`
|
||||
);
|
||||
console.log(`KAPANACAK /bolum/<slug> sayfası: ${kaybolanBolum.size}`);
|
||||
for (const [slug, k] of [...kaybolanBolum].sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||
console.log(` /bolum/${slug} — ${k.ad} (${k.n} satır)`);
|
||||
}
|
||||
console.log(`Tamamen kaybolan ham 'universite' değeri: ${kaybolanHamUni.length}`);
|
||||
for (const r of kaybolanHamUni) console.log(` ${r.ad} (${r.n} satır)`);
|
||||
console.log(`KAPANACAK /universite/<slug> sayfası: ${kaybolanUni.size}`);
|
||||
for (const [slug, k] of [...kaybolanUni].sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||
console.log(` /universite/${slug} — ${k.ad} (${k.n} satır)`);
|
||||
}
|
||||
|
||||
// ── netler ─────────────────────────────────────────────────────────────
|
||||
const netlerVar = tabloVar(db, "netler");
|
||||
if (netlerVar) {
|
||||
const net = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS satir, COUNT(DISTINCT n.program_id) AS program
|
||||
FROM netler n JOIN programs e ON e.id = n.program_id WHERE e.${ESKI}`
|
||||
)
|
||||
.get(yil) as { satir: number; program: number };
|
||||
console.log(`\nnetler: arşive gidecek programlara bağlı ${net.satir} satır (${net.program} program)`);
|
||||
}
|
||||
|
||||
if (kuru) {
|
||||
console.log("\nKURU KİP bitti — DB değişmedi.");
|
||||
db.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Gerçek taşıma: tek transaction ─────────────────────────────────────
|
||||
db.transaction(() => {
|
||||
arsivTablosunuGarantiEt(db);
|
||||
const kolonlar = kolonAdlari(db, "programs");
|
||||
const saglama = () =>
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) || '|' || TOTAL(sira${yil - 1}) || '|' || TOTAL(puan${yil - 1}) AS s
|
||||
FROM (SELECT * FROM programs UNION ALL SELECT ${kolonlar.join(", ")} FROM programs_arsiv)`
|
||||
)
|
||||
.get() as { s: string }
|
||||
).s;
|
||||
const once = saglama();
|
||||
|
||||
if (netlerVar) {
|
||||
// Tek halefli eski kodlar → yeni koda. OR IGNORE: yeni kodda o yıl varsa dokunma.
|
||||
const r = db
|
||||
.prepare(
|
||||
`UPDATE OR IGNORE netler SET program_id = (
|
||||
SELECT g.id FROM programs g WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil
|
||||
)
|
||||
WHERE program_id IN (SELECT id FROM programs WHERE son_kilavuz_yili IS NOT @yil)
|
||||
AND (SELECT COUNT(*) FROM programs g
|
||||
WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil) = 1`
|
||||
)
|
||||
.run({ yil });
|
||||
console.log(`\nnetler: ${r.changes} satır yeni koda çevrildi`);
|
||||
}
|
||||
|
||||
const tasinan = db
|
||||
.prepare(
|
||||
`INSERT INTO programs_arsiv (${kolonlar.join(", ")})
|
||||
SELECT ${kolonlar.join(", ")} FROM programs WHERE ${ESKI}`
|
||||
)
|
||||
.run(yil).changes;
|
||||
const cikan = db.prepare(`DELETE FROM programs WHERE ${ESKI}`).run(yil).changes;
|
||||
if (tasinan !== eskiSayi || cikan !== eskiSayi) {
|
||||
dur(`Taşınan (${tasinan}) / çıkarılan (${cikan}) ≠ beklenen (${eskiSayi}). Geri alındı.`);
|
||||
}
|
||||
const sonra = saglama();
|
||||
if (once !== sonra) {
|
||||
dur(`programs ∪ programs_arsiv sağlaması değişti. Geri alındı.\nönce : ${once}\nsonra: ${sonra}`);
|
||||
}
|
||||
metaYaz(db, `arsiv_${yil}_tarih`, new Date().toISOString());
|
||||
metaYaz(db, `arsiv_${yil}_satir`, tasinan);
|
||||
console.log(`Arşivlendi: ${tasinan} satır → programs_arsiv. Sağlama aynı: ${sonra}`);
|
||||
})();
|
||||
|
||||
if (netlerVar) {
|
||||
const yetim = (
|
||||
db
|
||||
.prepare("SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)")
|
||||
.get() as { n: number }
|
||||
).n;
|
||||
console.log(`netler: programs'ta karşılığı olmayan satır (arşivdeki programlara ait): ${yetim}`);
|
||||
}
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
console.log("\nSıradaki adım: pnpm db:index --db <db> --vacuum sonra pnpm veri-kalite …");
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("eski-arsivle.ts")) calistir(main);
|
||||
302
scripts/gecmis-doldur.ts
Normal file
302
scripts/gecmis-doldur.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Güncel kılavuzdaki programların GEÇMİŞ yıl kolonlarındaki boşlukları doldurur.
|
||||
*
|
||||
* Kurallar (pazarlıksız):
|
||||
* - Yalnız NULL alan doldurulur. Dolu değer ASLA ezilmez; kaynak farklı bir
|
||||
* değer söylüyorsa sayılır ve raporlanır (karar insanındır).
|
||||
* - Tahmin yok, komşu yıldan kopya yok. Kaynak yoksa alan NULL kalır.
|
||||
* - Belirsiz eşleşmede birleştirme yok (sayılır, raporlanır).
|
||||
*
|
||||
* Kaynak önceliği:
|
||||
* 1. API `…1/…2/…3` alanları (ham dosyadan; API'ye gidilmez) → yıl-1/-2/-3
|
||||
* için sıra, puan, genel kontenjan.
|
||||
* 2. `eski_kod` (API `eskiKilavuzKodu`): kodu değişen programın eski satırı
|
||||
* (programs ya da programs_arsiv) → tüm geçmiş yıllar, dört ölçü.
|
||||
* Eski kod hâlâ güncel kılavuzdaysa ya da birden çok yeni program aynı
|
||||
* eski koda işaret ediyorsa (bölünme) → belirsiz, atlanır.
|
||||
* 3. Aynı (üniversite baz adı, isim, tur, onlisans) için TAM 1 güncel + TAM 1
|
||||
* eski satır varsa; ortak yıllarda puan/kontenjan/yerleşen ÇELİŞMİYORSA ve
|
||||
* en az bir ortak değer AYNIYSA (kanıt) → eski satırdan doldurulur, güncel
|
||||
* satıra `eski_kod` yazılır. Kanıtsız ya da çelişkili eşleşme birleştirilmez.
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm gecmis-doldur --yil 2026 --db /tmp/deneme.db [--ham data/ham/2026-….jsonl.gz] [--kuru]
|
||||
*/
|
||||
import path from "node:path";
|
||||
import type Database from "better-sqlite3";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
tabloVar,
|
||||
yilArgumani,
|
||||
YIL_OLCULERI,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
type Satir = Record<string, string | number | null> & { id: string };
|
||||
|
||||
const PUAN_TOLERANS = 0.01;
|
||||
|
||||
class KuruGeriAl extends Error {}
|
||||
|
||||
function ayniMi(olcu: string, a: number, b: number): boolean {
|
||||
return olcu === "puan" ? Math.abs(a - b) <= PUAN_TOLERANS : a === b;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const kuru = bayrakVar("kuru");
|
||||
const hamArg = argOku("ham");
|
||||
const hamDosya = hamArg
|
||||
? path.resolve(process.cwd(), hamArg)
|
||||
: sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham"));
|
||||
if (!hamDosya) {
|
||||
dur(`--ham verilmedi ve data/ham altında ${yil}-*.jsonl.gz yok. Önce refresh koş.`);
|
||||
}
|
||||
|
||||
const db = dbAc(dbYolu);
|
||||
const kolonlar = new Set(kolonAdlari(db, "programs"));
|
||||
if (!kolonlar.has("son_kilavuz_yili") || !kolonlar.has(`sira${yil}`)) {
|
||||
dur("Şema eski: önce `pnpm refresh` (ya da `pnpm yokatlas:goc`) koşulmalı.");
|
||||
}
|
||||
const guncelSayisi = (
|
||||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||||
).n;
|
||||
if (guncelSayisi === 0) dur(`son_kilavuz_yili=${yil} satır yok: önce refresh koşulmalı.`);
|
||||
|
||||
const gecmisYillar: number[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
if (kolonlar.has(`sira${y}`)) gecmisYillar.push(y);
|
||||
}
|
||||
const olculer = YIL_OLCULERI.map(([o]) => o);
|
||||
|
||||
console.log(`DB: ${dbYolu}\nHam: ${hamDosya}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
|
||||
const { kayitlar } = await hamOku(hamDosya, yil);
|
||||
|
||||
const doldurulan = new Map<string, number>(); // "kaynak|kolon" → adet
|
||||
const farkli = new Map<string, { adet: number; ornek: string[] }>();
|
||||
const say = (m: Map<string, number>, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
|
||||
const farkYaz = (kaynak: string, kolon: string, id: string, db_: number, kaynakDeger: number) => {
|
||||
const k = `${kaynak}|${kolon}`;
|
||||
const f = farkli.get(k) ?? { adet: 0, ornek: [] };
|
||||
f.adet++;
|
||||
if (f.ornek.length < 3) f.ornek.push(`${id}: DB=${db_} kaynak=${kaynakDeger}`);
|
||||
farkli.set(k, f);
|
||||
};
|
||||
|
||||
const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
|
||||
const arsivGetir = tabloVar(db, "programs_arsiv")
|
||||
? db.prepare("SELECT * FROM programs_arsiv WHERE id = ?")
|
||||
: null;
|
||||
const setCache = new Map<string, Database.Statement<unknown[]>>();
|
||||
const alanYaz = (id: string, kolon: string, deger: number) => {
|
||||
let st = setCache.get(kolon);
|
||||
if (!st) {
|
||||
// Çifte emniyet: WHERE … IS NULL — dolu değerin üstüne yazılamaz.
|
||||
st = db.prepare(`UPDATE programs SET ${kolon} = ? WHERE id = ? AND ${kolon} IS NULL`);
|
||||
setCache.set(kolon, st);
|
||||
}
|
||||
st.run(deger, id);
|
||||
};
|
||||
|
||||
/** Hedefin NULL alanlarını kaynaktan doldurur; dolu+farklıyı sayar. */
|
||||
const satirdanDoldur = (kaynakAdi: string, hedef: Satir, kaynak: Satir) => {
|
||||
for (const y of gecmisYillar) {
|
||||
for (const olcu of olculer) {
|
||||
const kolon = `${olcu}${y}`;
|
||||
const k = kaynak[kolon] as number | null;
|
||||
if (k == null) continue;
|
||||
const h = hedef[kolon] as number | null;
|
||||
if (h == null) {
|
||||
alanYaz(hedef.id, kolon, k);
|
||||
hedef[kolon] = k;
|
||||
say(doldurulan, `${kaynakAdi}|${kolon}`);
|
||||
} else if (!ayniMi(olcu, h, k)) {
|
||||
farkYaz(kaynakAdi, kolon, hedef.id, h, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ozet = {
|
||||
apiEslesen: 0,
|
||||
eskiKodlu: 0,
|
||||
eskiKodBulunamadi: 0,
|
||||
eskiKodHalaGuncel: 0,
|
||||
eskiKodBolunme: 0,
|
||||
adEslesen: 0,
|
||||
adBelirsiz: 0,
|
||||
adPuanCelisik: 0,
|
||||
adKanitsiz: 0,
|
||||
};
|
||||
const ornekler: Record<string, string[]> = { bolunme: [], halaGuncel: [], adEslesen: [], adCelisik: [], adKanitsiz: [] };
|
||||
const ornekEkle = (k: string, s: string) => {
|
||||
if (ornekler[k].length < 3) ornekler[k].push(s);
|
||||
};
|
||||
|
||||
try {
|
||||
db.transaction(() => {
|
||||
// ── 1. API …1/…2/…3 ────────────────────────────────────────────────
|
||||
for (const r of kayitlar) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
const hedef = satirGetir.get(String(r.kilavuzKodu)) as Satir | undefined;
|
||||
if (!hedef || hedef.son_kilavuz_yili !== yil) continue;
|
||||
ozet.apiEslesen++;
|
||||
for (const g of gecmisAlanlari(r)) {
|
||||
const y = yil - g.fark;
|
||||
if (!gecmisYillar.includes(y)) continue;
|
||||
for (const olcu of ["sira", "puan", "kontenjan"] as const) {
|
||||
const k = g[olcu];
|
||||
if (k == null) continue;
|
||||
const kolon = `${olcu}${y}`;
|
||||
const h = hedef[kolon] as number | null;
|
||||
if (h == null) {
|
||||
alanYaz(hedef.id, kolon, k);
|
||||
hedef[kolon] = k;
|
||||
say(doldurulan, `1-api|${kolon}`);
|
||||
} else if (!ayniMi(olcu, h, k)) {
|
||||
farkYaz("1-api", kolon, hedef.id, h, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. eski_kod ────────────────────────────────────────────────────
|
||||
const eskiKodlular = db
|
||||
.prepare("SELECT * FROM programs WHERE son_kilavuz_yili = ? AND eski_kod IS NOT NULL")
|
||||
.all(yil) as Satir[];
|
||||
const eskiKullanim = new Map<string, number>();
|
||||
for (const s of eskiKodlular) say(eskiKullanim, s.eski_kod as string);
|
||||
for (const hedef of eskiKodlular) {
|
||||
const eskiId = hedef.eski_kod as string;
|
||||
if ((eskiKullanim.get(eskiId) ?? 0) > 1) {
|
||||
ozet.eskiKodBolunme++;
|
||||
ornekEkle("bolunme", `${hedef.id} ← ${eskiId} (${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
const kaynak =
|
||||
(satirGetir.get(eskiId) as Satir | undefined) ??
|
||||
(arsivGetir?.get(eskiId) as Satir | undefined);
|
||||
if (!kaynak) {
|
||||
ozet.eskiKodBulunamadi++;
|
||||
continue;
|
||||
}
|
||||
if (kaynak.son_kilavuz_yili === yil) {
|
||||
ozet.eskiKodHalaGuncel++;
|
||||
ornekEkle("halaGuncel", `${hedef.id} ← ${eskiId} (${kaynak.isim})`);
|
||||
continue;
|
||||
}
|
||||
ozet.eskiKodlu++;
|
||||
satirdanDoldur("2-eski_kod", hedef, kaynak);
|
||||
}
|
||||
|
||||
// ── 3. Benzersiz (universite, isim, tur, onlisans) ─────────────────
|
||||
const sahiplenilmis = new Set(eskiKullanim.keys());
|
||||
const gruplar = new Map<string, { guncel: Satir[]; eski: Satir[] }>();
|
||||
for (const s of db.prepare("SELECT * FROM programs").all() as Satir[]) {
|
||||
// API 2026'da üniversite adına "(İL)" eki koyuyor; eski satırlar eksiz.
|
||||
const uniBaz = String(s.universite).replace(/\s*\([^)]*\)\s*$/, "").trim();
|
||||
const anahtar = JSON.stringify([uniBaz, s.isim, s.tur, s.onlisans]);
|
||||
const g = gruplar.get(anahtar) ?? { guncel: [], eski: [] };
|
||||
if (s.son_kilavuz_yili === yil) g.guncel.push(s);
|
||||
else if (!sahiplenilmis.has(s.id)) g.eski.push(s);
|
||||
gruplar.set(anahtar, g);
|
||||
}
|
||||
const eskiKodYaz = db.prepare(
|
||||
"UPDATE programs SET eski_kod = ? WHERE id = ? AND eski_kod IS NULL"
|
||||
);
|
||||
for (const g of gruplar.values()) {
|
||||
if (g.eski.length === 0 || g.guncel.length === 0) continue;
|
||||
if (g.eski.length !== 1 || g.guncel.length !== 1) {
|
||||
ozet.adBelirsiz++;
|
||||
continue;
|
||||
}
|
||||
const [hedef] = g.guncel;
|
||||
const [kaynak] = g.eski;
|
||||
if (hedef.eski_kod != null) {
|
||||
// API başka bir eski kod söylüyor; ad eşleşmesi onun önüne geçmez.
|
||||
ozet.adBelirsiz++;
|
||||
continue;
|
||||
}
|
||||
// Aynı yılda iki satırda da veri varsa değerler AYNI olmalı (çift satır);
|
||||
// farklıysa bunlar aynı anda var olmuş iki ayrı programdır (ör. 1 kişilik
|
||||
// özel kontenjanlı adaş program). Sıra bilerek dışarıda: 2024 sırası iki
|
||||
// kaynakta farklı tanımlı. Ortak yılı hiç olmayan eşleşme KANITSIZDIR →
|
||||
// birleştirilmez, yalnız sayılır.
|
||||
let kanit = 0;
|
||||
let celisik = false;
|
||||
for (const y of gecmisYillar) {
|
||||
for (const olcu of ["puan", "kontenjan", "yerlesen"]) {
|
||||
const a = hedef[`${olcu}${y}`] as number | null;
|
||||
const b = kaynak[`${olcu}${y}`] as number | null;
|
||||
if (a == null || b == null) continue;
|
||||
if (ayniMi(olcu, a, b)) kanit++;
|
||||
else celisik = true;
|
||||
}
|
||||
}
|
||||
if (celisik) {
|
||||
ozet.adPuanCelisik++;
|
||||
ornekEkle("adCelisik", `${hedef.id} ~ ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
if (kanit === 0) {
|
||||
ozet.adKanitsiz++;
|
||||
ornekEkle("adKanitsiz", `${hedef.id} ? ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
ozet.adEslesen++;
|
||||
ornekEkle("adEslesen", `${hedef.id} ← ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
satirdanDoldur("3-ad", hedef, kaynak);
|
||||
eskiKodYaz.run(kaynak.id, hedef.id);
|
||||
}
|
||||
|
||||
if (kuru) throw new KuruGeriAl();
|
||||
})();
|
||||
} catch (err) {
|
||||
if (!(err instanceof KuruGeriAl)) throw err;
|
||||
}
|
||||
|
||||
// ── Rapor ──────────────────────────────────────────────────────────────
|
||||
console.log(`\nGüncel kılavuzdaki program: ${guncelSayisi}; API kaydıyla eşleşen: ${ozet.apiEslesen}`);
|
||||
console.log(
|
||||
`eski_kod: ${ozet.eskiKodlu} programın eski satırı bulundu ve boşlukları ondan dolduruldu; ` +
|
||||
`eski satır yok: ${ozet.eskiKodBulunamadi}; eski kod hâlâ güncel (atlandı): ${ozet.eskiKodHalaGuncel}; ` +
|
||||
`bölünme/çoklu (atlandı): ${ozet.eskiKodBolunme}`
|
||||
);
|
||||
console.log(
|
||||
`Ad eşleşmesi: ${ozet.adEslesen} birebir; belirsiz (atlandı): ${ozet.adBelirsiz}; ` +
|
||||
`aynı yılda farklı değer = ayrı program (atlandı): ${ozet.adPuanCelisik}; ` +
|
||||
`ortak yılı olmayan, kanıtsız (atlandı): ${ozet.adKanitsiz}`
|
||||
);
|
||||
for (const [k, v] of Object.entries(ornekler)) {
|
||||
if (v.length) console.log(` örnek ${k}: ${v.join(" ; ")}`);
|
||||
}
|
||||
|
||||
console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`);
|
||||
const toplamlar = new Map<string, number>();
|
||||
for (const [k, n] of [...doldurulan].sort()) {
|
||||
const [kaynak] = k.split("|");
|
||||
toplamlar.set(kaynak, (toplamlar.get(kaynak) ?? 0) + n);
|
||||
console.log(` ${k.replace("|", " ")}: ${n}`);
|
||||
}
|
||||
for (const [kaynak, n] of toplamlar) console.log(` TOPLAM ${kaynak}: ${n}`);
|
||||
if (doldurulan.size === 0) console.log(" (yok)");
|
||||
|
||||
console.log("\nDolu olup kaynağın FARKLI söylediği alanlar (EZİLMEDİ):");
|
||||
for (const [k, f] of [...farkli].sort()) {
|
||||
console.log(` ${k.replace("|", " ")}: ${f.adet} ör. ${f.ornek.join(" ; ")}`);
|
||||
}
|
||||
if (farkli.size === 0) console.log(" (yok)");
|
||||
|
||||
if (!kuru) db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("gecmis-doldur.ts")) calistir(main);
|
||||
@@ -1,24 +1,66 @@
|
||||
/**
|
||||
* Canlı YÖK Atlas API'sinden 2025 yerleştirme verisini çekip DB'yi tazeler.
|
||||
* Canlı YÖK Atlas API'sinden SON yerleştirme yılının verisini çekip DB'ye işler.
|
||||
*
|
||||
* Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search
|
||||
* (yokatlas-py'nin kullandığı resmî tercih-kılavuz JSON API'si)
|
||||
*
|
||||
* - Mevcut programlarda sira/puan/kontenjan/yerlesen 2025 kolonlarını günceller
|
||||
* - DB'de olmayan (yeni açılan) programları ekler
|
||||
* - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır
|
||||
* API'nin yıl düzeni (Eylül 2026'da doğrulandı): EKSİZ alanlar (basariSirasi,
|
||||
* minPuan, kontenjan, gkY…) en son yerleştirme yılıdır; `…1` bir önceki,
|
||||
* `…2` iki önceki, `…3` üç önceki yıldır. Kayıttaki `yil` alanı GÜVENİLMEZ
|
||||
* (kılavuz yılı / veri yılı anlamı yıl içinde değişiyor) — bu betik yılı
|
||||
* HİÇBİR ZAMAN oradan türetmez. Yıl `--yil` ile verilir ve YIL KİLİDİ ile
|
||||
* DB'ye karşı doğrulanır.
|
||||
*
|
||||
* Kullanım: npx tsx scripts/refresh.ts
|
||||
* Güvenlik sırası:
|
||||
* 1. `--yil` = SON_YIL, `--db` zorunlu (varsayılan yok).
|
||||
* 2. YIL KİLİDİ: ilk sayfadaki `basariSirasi1` değerleri DB'deki
|
||||
* sira<yil-1> ile ≥%95 eşleşmeli VE eksiz `basariSirasi` ondan farklı
|
||||
* olmalı. Değilse API henüz yeni yıla dönmemiştir (ya da bir yıl daha
|
||||
* ilerlemiştir) → HİÇBİR ŞEY yazılmadan durur.
|
||||
* 3. Tüm sayfalar önce belleğe + ham dosyaya alınır; benzersiz kod sayısı =
|
||||
* `totalElements` değilse (sayfalama kayması) durur.
|
||||
* 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction
|
||||
* içinde karşılaştırılır, fark varsa geri alınır.
|
||||
*
|
||||
* Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod.
|
||||
* Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise
|
||||
* gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından
|
||||
* önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL.
|
||||
*
|
||||
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
|
||||
* `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham
|
||||
* dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama).
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm refresh --yil 2026 --db /tmp/deneme.db
|
||||
* pnpm refresh --yil 2026 --db data/yokatlas.db --ham data/ham/2026-2026-09-21.jsonl.gz
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import readline from "node:readline";
|
||||
import zlib from "node:zlib";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { efektifIndeksiKur } from "./db-index";
|
||||
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
|
||||
import {
|
||||
argOku,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
metaYaz,
|
||||
yilArgumani,
|
||||
yilKolonlariniGarantiEt,
|
||||
yokatlasGoc,
|
||||
type Db,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||
const PAGE_SIZE = 500;
|
||||
const DELAY_MS = 300;
|
||||
const DELAY_MS = 500;
|
||||
const KILIT_ESIK = 0.95;
|
||||
const KILIT_ASGARI_ORNEK = 50;
|
||||
|
||||
const TUR_MAP: Record<string, string> = {
|
||||
export const TUR_MAP: Record<string, string> = {
|
||||
SAY: "SAYISAL",
|
||||
EA: "EŞİT AĞIRLIK",
|
||||
SÖZ: "SÖZEL",
|
||||
@@ -26,27 +68,75 @@ const TUR_MAP: Record<string, string> = {
|
||||
TYT: "TYT",
|
||||
};
|
||||
|
||||
type ApiRecord = {
|
||||
/** API kaydından betiklerin kullandığı alanlar (ham dosyada kaydın tamamı durur). */
|
||||
export type KilavuzKaydi = {
|
||||
kilavuzKodu: number;
|
||||
eskiKilavuzKodu?: number | null;
|
||||
birimAdi: string;
|
||||
universiteAdi: string;
|
||||
universiteTuru: string | null;
|
||||
ilAdi: string | null;
|
||||
fymkAdi: string | null;
|
||||
universiteTuru?: string | null;
|
||||
ilAdi?: string | null;
|
||||
fymkAdi?: string | null;
|
||||
puanTuru: string;
|
||||
ogrenimSuresi: number | null;
|
||||
ogrenimSuresi?: number | null;
|
||||
birimTuruId: number;
|
||||
basariSirasi?: number | null;
|
||||
minPuan?: number | null;
|
||||
basariSirasi?: number | string | null;
|
||||
minPuan?: number | string | null;
|
||||
kontenjan?: number | null;
|
||||
gkY?: number | null;
|
||||
sgyY?: number | null;
|
||||
dprmY?: number | null;
|
||||
obkY?: number | null;
|
||||
basariSirasi1?: number | string | null;
|
||||
basariSirasi2?: number | string | null;
|
||||
basariSirasi3?: number | string | null;
|
||||
minPuan1?: number | string | null;
|
||||
minPuan2?: number | string | null;
|
||||
minPuan3?: number | string | null;
|
||||
gk1?: number | null;
|
||||
gk2?: number | null;
|
||||
gk3?: number | null;
|
||||
};
|
||||
|
||||
const TUTULAN_ALANLAR: (keyof KilavuzKaydi)[] = [
|
||||
"kilavuzKodu",
|
||||
"eskiKilavuzKodu",
|
||||
"birimAdi",
|
||||
"universiteAdi",
|
||||
"universiteTuru",
|
||||
"ilAdi",
|
||||
"fymkAdi",
|
||||
"puanTuru",
|
||||
"ogrenimSuresi",
|
||||
"birimTuruId",
|
||||
"basariSirasi",
|
||||
"minPuan",
|
||||
"kontenjan",
|
||||
"gkY",
|
||||
"sgyY",
|
||||
"dprmY",
|
||||
"obkY",
|
||||
"basariSirasi1",
|
||||
"basariSirasi2",
|
||||
"basariSirasi3",
|
||||
"minPuan1",
|
||||
"minPuan2",
|
||||
"minPuan3",
|
||||
"gk1",
|
||||
"gk2",
|
||||
"gk3",
|
||||
];
|
||||
|
||||
function inceKayit(ham: Record<string, unknown>): KilavuzKaydi {
|
||||
const ince: Record<string, unknown> = {};
|
||||
for (const alan of TUTULAN_ALANLAR) {
|
||||
if (ham[alan] !== undefined) ince[alan] = ham[alan];
|
||||
}
|
||||
return ince as KilavuzKaydi;
|
||||
}
|
||||
|
||||
type ApiPage = {
|
||||
content: ApiRecord[];
|
||||
content: Record<string, unknown>[];
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
};
|
||||
@@ -83,140 +173,435 @@ async function fetchPage(page: number): Promise<ApiPage> {
|
||||
return (await res.json()) as ApiPage;
|
||||
}
|
||||
|
||||
function yerlesen(r: ApiRecord): number | null {
|
||||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||||
(x): x is number => typeof x === "number"
|
||||
);
|
||||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
||||
}
|
||||
|
||||
function intOrNull(v: unknown): number | null {
|
||||
/** Pozitif tam sayı; 0 / boş / sayı değil → NULL (API "veri yok"u 0 ile verir). */
|
||||
export function intOrNull(v: unknown): number | null {
|
||||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||
return typeof n === "number" && Number.isFinite(n) && n > 0
|
||||
? Math.round(n)
|
||||
: null;
|
||||
}
|
||||
|
||||
function realOrNull(v: unknown): number | null {
|
||||
export function realOrNull(v: unknown): number | null {
|
||||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(programs)").all() as {
|
||||
name: string;
|
||||
}[];
|
||||
const has2025 = cols.some((c) => c.name === "sira2025");
|
||||
if (!has2025) {
|
||||
db.exec(`
|
||||
ALTER TABLE programs ADD COLUMN sira2025 INTEGER;
|
||||
ALTER TABLE programs ADD COLUMN puan2025 REAL;
|
||||
ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER;
|
||||
ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER;
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025);
|
||||
`);
|
||||
function yerlesen(r: KilavuzKaydi): number | null {
|
||||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||||
(x): x is number => typeof x === "number"
|
||||
);
|
||||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
||||
}
|
||||
|
||||
// Sorgu yolunun kullandığı ifade indexi; ifade src/lib/db.ts EFEKTIF_SIRA
|
||||
// ile birebir aynı olmalı (bkz. scripts/db-index.ts). sira2025 kolonu
|
||||
// yukarıda garanti edildikten sonra kurulur.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
|
||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
||||
`);
|
||||
/**
|
||||
* API `eskiKilavuzKodu` iki anlamda geliyor: gerçek eski program kodu (9 hane)
|
||||
* ya da yeni açılan programda üniversite öneki (4 hane, ör. 2031). Yalnız
|
||||
* gerçek program kodu biçimindeki ve yeni koddan farklı olanı döndürür.
|
||||
*/
|
||||
export function eskiKod(r: KilavuzKaydi): string | null {
|
||||
if (!r.eskiKilavuzKodu) return null;
|
||||
const eski = String(r.eskiKilavuzKodu);
|
||||
const yeni = String(r.kilavuzKodu);
|
||||
return eski.length === yeni.length && eski !== yeni ? eski : null;
|
||||
}
|
||||
|
||||
/** Yeni programda önceki üç yılın API karşılığı: [yıl farkı, sıra, puan, genel kontenjan]. */
|
||||
export function gecmisAlanlari(
|
||||
r: KilavuzKaydi
|
||||
): { fark: number; sira: number | null; puan: number | null; kontenjan: number | null }[] {
|
||||
return [
|
||||
{ fark: 1, sira: intOrNull(r.basariSirasi1), puan: realOrNull(r.minPuan1), kontenjan: intOrNull(r.gk1) },
|
||||
{ fark: 2, sira: intOrNull(r.basariSirasi2), puan: realOrNull(r.minPuan2), kontenjan: intOrNull(r.gk2) },
|
||||
{ fark: 3, sira: intOrNull(r.basariSirasi3), puan: realOrNull(r.minPuan3), kontenjan: intOrNull(r.gk3) },
|
||||
];
|
||||
}
|
||||
|
||||
// ── Ham dosya ────────────────────────────────────────────────────────────
|
||||
|
||||
/** `--ham` verilmemişse o yılın en yeni ham dosyası (ad sırası = tarih sırası). */
|
||||
export function sonHamDosya(yil: number, dizin: string): string | null {
|
||||
if (!fs.existsSync(dizin)) return null;
|
||||
const adaylar = fs
|
||||
.readdirSync(dizin)
|
||||
.filter((f) => f.startsWith(`${yil}-`) && f.endsWith(".jsonl.gz"))
|
||||
.sort();
|
||||
return adaylar.length ? path.join(dizin, adaylar[adaylar.length - 1]) : null;
|
||||
}
|
||||
|
||||
export type HamOkuma = { toplam: number; kayitlar: KilavuzKaydi[] };
|
||||
|
||||
/** Ham .jsonl.gz → ince kayıtlar. İlk satır `_meta` (yıl doğrulanır). */
|
||||
export async function hamOku(dosya: string, yil: number): Promise<HamOkuma> {
|
||||
if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`);
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(dosya).pipe(zlib.createGunzip()),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
let toplam = -1;
|
||||
const kayitlar: KilavuzKaydi[] = [];
|
||||
for await (const satir of rl) {
|
||||
if (!satir) continue;
|
||||
const obj = JSON.parse(satir) as Record<string, unknown>;
|
||||
if (obj._meta) {
|
||||
const meta = obj._meta as { yil: number; totalElements: number };
|
||||
if (meta.yil !== yil) {
|
||||
dur(`Ham dosya ${meta.yil} yılı için çekilmiş, --yil ${yil} verildi.`);
|
||||
}
|
||||
toplam = meta.totalElements;
|
||||
continue;
|
||||
}
|
||||
kayitlar.push(inceKayit(obj));
|
||||
}
|
||||
if (toplam < 0) dur(`Ham dosyada _meta satırı yok: ${dosya}`);
|
||||
return { toplam, kayitlar };
|
||||
}
|
||||
|
||||
// ── Yıl kilidi ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* `basariSirasi1` = DB sira<yil-1> (≥%95) VE eksiz `basariSirasi` ≠ DB
|
||||
* sira<yil-1>. İkisi birlikte "eksiz alanlar gerçekten <yil> verisi" demektir.
|
||||
*/
|
||||
export function yilKilidi(db: Db, yil: number, ornek: KilavuzKaydi[]): void {
|
||||
const onceki = yil - 1;
|
||||
const bul = db.prepare(`SELECT sira${onceki} AS s FROM programs WHERE id = ?`);
|
||||
let kiyas1 = 0;
|
||||
let esit1 = 0;
|
||||
let kiyas0 = 0;
|
||||
let esit0 = 0;
|
||||
for (const r of ornek) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
let satir = bul.get(String(r.kilavuzKodu)) as { s: number | null } | undefined;
|
||||
const eski = eskiKod(r);
|
||||
if (!satir && eski) satir = bul.get(eski) as typeof satir;
|
||||
if (!satir || satir.s == null) continue;
|
||||
const s1 = intOrNull(r.basariSirasi1);
|
||||
if (s1 != null) {
|
||||
kiyas1++;
|
||||
if (s1 === satir.s) esit1++;
|
||||
}
|
||||
const s0 = intOrNull(r.basariSirasi);
|
||||
if (s0 != null) {
|
||||
kiyas0++;
|
||||
if (s0 === satir.s) esit0++;
|
||||
}
|
||||
}
|
||||
const oran1 = kiyas1 ? esit1 / kiyas1 : 0;
|
||||
const oran0 = kiyas0 ? esit0 / kiyas0 : 0;
|
||||
console.log(
|
||||
`Yıl kilidi: basariSirasi1 = sira${onceki} → ${esit1}/${kiyas1} (%${(oran1 * 100).toFixed(1)}); ` +
|
||||
`eksiz basariSirasi = sira${onceki} → ${esit0}/${kiyas0} (%${(oran0 * 100).toFixed(1)})`
|
||||
);
|
||||
if (kiyas1 < KILIT_ASGARI_ORNEK) {
|
||||
dur(
|
||||
`Yıl kilidi için yeterli karşılaştırma yok (${kiyas1} < ${KILIT_ASGARI_ORNEK}). ` +
|
||||
`DB'de sira${onceki} dolu mu, doğru DB mi?`
|
||||
);
|
||||
}
|
||||
if (oran0 >= 0.5) {
|
||||
dur(
|
||||
`API'nin eksiz alanları hâlâ ${onceki} verisi (DB ile %${(oran0 * 100).toFixed(1)} aynı). ` +
|
||||
`${yil} yerleştirme sonuçları henüz yayımlanmamış; bugün koşmak ${yil} kolonuna ${onceki} verisi yazar.`
|
||||
);
|
||||
}
|
||||
if (oran1 < KILIT_ESIK) {
|
||||
dur(
|
||||
`API'nin "…1" alanları DB'deki ${onceki} verisiyle eşleşmiyor (%${(oran1 * 100).toFixed(1)} < %95). ` +
|
||||
`API yıl düzeni değişmiş olabilir; --yil ${yil} ile yazmak yanlış kolonu doldurur.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ana akış ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function apidenCek(
|
||||
yil: number,
|
||||
hamDizin: string,
|
||||
ilkSayfaKontrol: (ornek: KilavuzKaydi[]) => void
|
||||
): Promise<HamOkuma & { hamDosya: string }> {
|
||||
fs.mkdirSync(hamDizin, { recursive: true });
|
||||
const tarih = new Date().toISOString().slice(0, 10);
|
||||
const hamDosya = path.join(hamDizin, `${yil}-${tarih}.jsonl.gz`);
|
||||
const gecici = `${hamDosya}.tmp`;
|
||||
|
||||
const first = await fetchPage(0);
|
||||
const kayitlar = first.content.map(inceKayit);
|
||||
// Kilit tutmazsa hiçbir şey yazılmadan ve kaynağa 1 istekle durulur.
|
||||
ilkSayfaKontrol(kayitlar);
|
||||
|
||||
console.log(
|
||||
`Toplam ${first.totalElements} kayıt, ${first.totalPages} sayfa çekilecek ` +
|
||||
`(~${Math.ceil((first.totalPages * (DELAY_MS + 1500)) / 1000)} sn)…`
|
||||
);
|
||||
const gzip = zlib.createGzip();
|
||||
const cikti = fs.createWriteStream(gecici);
|
||||
gzip.pipe(cikti);
|
||||
const bitti = new Promise<void>((resolve, reject) => {
|
||||
cikti.on("finish", resolve);
|
||||
cikti.on("error", reject);
|
||||
gzip.on("error", reject);
|
||||
});
|
||||
const yaz = (obj: unknown) => gzip.write(`${JSON.stringify(obj)}\n`);
|
||||
|
||||
yaz({
|
||||
_meta: {
|
||||
yil,
|
||||
cekimTarihi: new Date().toISOString(),
|
||||
kaynak: API,
|
||||
totalElements: first.totalElements,
|
||||
totalPages: first.totalPages,
|
||||
sayfaBoyu: PAGE_SIZE,
|
||||
},
|
||||
});
|
||||
for (const r of first.content) yaz(r);
|
||||
for (let p = 1; p < first.totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
const page = await fetchPage(p);
|
||||
for (const r of page.content) {
|
||||
yaz(r);
|
||||
kayitlar.push(inceKayit(r));
|
||||
}
|
||||
process.stdout.write(`\rsayfa ${p + 1}/${first.totalPages}`);
|
||||
}
|
||||
console.log();
|
||||
gzip.end();
|
||||
await bitti;
|
||||
fs.renameSync(gecici, hamDosya);
|
||||
console.log(`Ham veri: ${hamDosya}`);
|
||||
return { toplam: first.totalElements, kayitlar, hamDosya };
|
||||
}
|
||||
|
||||
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
|
||||
const kolonlar: string[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
|
||||
kolonlar.push(`COUNT(${olcu}${y})`, `TOTAL(${olcu}${y})`);
|
||||
}
|
||||
}
|
||||
const r = db
|
||||
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`)
|
||||
.raw()
|
||||
.get(azamiRowid) as number[];
|
||||
return r.join("|");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const hamArg = argOku("ham");
|
||||
const hamDizin = path.resolve(process.cwd(), argOku("ham-dizin") ?? path.join("data", "ham"));
|
||||
|
||||
const db = dbAc(dbYolu);
|
||||
console.log(`DB: ${dbYolu}\nYıl: ${yil}`);
|
||||
|
||||
// Şema (idempotent). Yeni programlarda …1/…2/…3 yazılacağı için önceki üç
|
||||
// yılın kolonları da garanti edilir (normalde zaten vardır).
|
||||
const eklenen = yokatlasGoc(db, yil);
|
||||
for (let fark = 1; fark <= 3; fark++) {
|
||||
if (yil - fark >= ILK_YIL) eklenen.push(...yilKolonlariniGarantiEt(db, yil - fark));
|
||||
}
|
||||
if (eklenen.length) console.log(`Göç: ${eklenen.join(", ")}`);
|
||||
|
||||
// ── Veri: API ya da ham dosya ──────────────────────────────────────────
|
||||
let toplam: number;
|
||||
let kayitlar: KilavuzKaydi[];
|
||||
let hamDosya: string;
|
||||
if (hamArg) {
|
||||
hamDosya = path.resolve(process.cwd(), hamArg);
|
||||
console.log(`Kaynak: ham dosya (API'ye gidilmiyor) → ${hamDosya}`);
|
||||
({ toplam, kayitlar } = await hamOku(hamDosya, yil));
|
||||
yilKilidi(db, yil, kayitlar.slice(0, PAGE_SIZE));
|
||||
} else {
|
||||
({ toplam, kayitlar, hamDosya } = await apidenCek(yil, hamDizin, (ornek) =>
|
||||
yilKilidi(db, yil, ornek)
|
||||
));
|
||||
}
|
||||
|
||||
// ── Bütünlük: benzersiz kod = totalElements ────────────────────────────
|
||||
const benzersiz = new Map<string, KilavuzKaydi>();
|
||||
let kodsuz = 0;
|
||||
for (const r of kayitlar) {
|
||||
if (!r.kilavuzKodu) {
|
||||
kodsuz++;
|
||||
continue;
|
||||
}
|
||||
benzersiz.set(String(r.kilavuzKodu), r);
|
||||
}
|
||||
console.log(
|
||||
`Çekilen: ${kayitlar.length} kayıt, benzersiz kod: ${benzersiz.size}, kodsuz: ${kodsuz}, API toplamı: ${toplam}`
|
||||
);
|
||||
if (benzersiz.size + kodsuz !== toplam || kayitlar.length !== toplam) {
|
||||
dur(
|
||||
`Benzersiz kod sayısı (${benzersiz.size}) + kodsuz (${kodsuz}) ≠ API toplamı (${toplam}). ` +
|
||||
"Sayfalama sırasında kayıt kaymış/yinelenmiş; DB'ye HİÇBİR ŞEY yazılmadı. Tekrar dene."
|
||||
);
|
||||
}
|
||||
|
||||
// ── Yazım: tek transaction ─────────────────────────────────────────────
|
||||
const update = db.prepare(`
|
||||
UPDATE programs SET sira2025 = ?, puan2025 = ?, kontenjan2025 = ?, yerlesen2025 = ?
|
||||
UPDATE programs
|
||||
SET sira${yil} = ?, puan${yil} = ?, kontenjan${yil} = ?, yerlesen${yil} = ?,
|
||||
son_kilavuz_yili = ?, eski_kod = COALESCE(?, eski_kod)
|
||||
WHERE id = ?
|
||||
`);
|
||||
const gecmisYillar = [1, 2, 3].filter((f) => yil - f >= ILK_YIL);
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
|
||||
sira2025, puan2025, kontenjan2025, yerlesen2025)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
sira${yil}, puan${yil}, kontenjan${yil}, yerlesen${yil},
|
||||
son_kilavuz_yili, eski_kod
|
||||
${gecmisYillar.map((f) => `, sira${yil - f}, puan${yil - f}, kontenjan${yil - f}`).join("")})
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${gecmisYillar.map(() => ", ?, ?, ?").join("")})
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
// Yeni satırda yazım/sözlük tutarlılığı — TAHMİN YOK, yalnız DB'de zaten var
|
||||
// olan yazımı yeniden kullanma:
|
||||
// - unitur: API sözlüğü (DEVLET/VAKIF/KKTC/YURTDIŞI) DB sözlüğünden (VAKIF MYO,
|
||||
// YURTDISI KAMU…) farklı. Aynı üniversitenin mevcut satırlarında TEK bir
|
||||
// unitur varsa o kullanılır; yoksa API değeri olduğu gibi yazılır. Aksi
|
||||
// hâlde aynı kurumda iki unitur oluşur ve uniturOnar haklı olarak durur.
|
||||
// - fakulte: API 2026'da BÜYÜK HARF veriyor; aynı üniversitede büyük harfe
|
||||
// çevrilince aynı olan mevcut yazım varsa o kullanılır.
|
||||
const bazAd = (ad: string) => ad.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
||||
const uniturlar = new Map<string, Set<string>>();
|
||||
const fakulteler = new Map<string, Map<string, string>>();
|
||||
for (const s of db
|
||||
.prepare("SELECT DISTINCT universite, unitur, fakulte FROM programs")
|
||||
.all() as { universite: string; unitur: string | null; fakulte: string | null }[]) {
|
||||
const baz = bazAd(s.universite);
|
||||
if (s.unitur) {
|
||||
for (const k of [s.universite, baz]) {
|
||||
const set = uniturlar.get(k) ?? new Set<string>();
|
||||
set.add(s.unitur);
|
||||
uniturlar.set(k, set);
|
||||
}
|
||||
}
|
||||
if (s.fakulte) {
|
||||
const m = fakulteler.get(baz) ?? new Map<string, string>();
|
||||
const anahtar = s.fakulte.toLocaleUpperCase("tr-TR");
|
||||
// Aynı anahtara iki farklı yazım düşerse belirsiz → yeniden kullanma
|
||||
m.set(anahtar, m.has(anahtar) && m.get(anahtar) !== s.fakulte ? "" : s.fakulte);
|
||||
fakulteler.set(baz, m);
|
||||
}
|
||||
}
|
||||
const uniturSec = (universite: string, api: string | null): string | null => {
|
||||
for (const k of [universite, bazAd(universite)]) {
|
||||
const set = uniturlar.get(k);
|
||||
if (set?.size === 1) return [...set][0];
|
||||
}
|
||||
return api;
|
||||
};
|
||||
const fakulteSec = (universite: string, api: string | null): string | null => {
|
||||
if (!api) return null;
|
||||
const mevcut = fakulteler.get(bazAd(universite))?.get(api.toLocaleUpperCase("tr-TR"));
|
||||
return mevcut || api;
|
||||
};
|
||||
|
||||
let updated = 0;
|
||||
let inserted = 0;
|
||||
let skipped = 0;
|
||||
const atlananlar: string[] = [];
|
||||
|
||||
const first = await fetchPage(0);
|
||||
const totalPages = first.totalPages;
|
||||
console.log(
|
||||
`Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…`
|
||||
);
|
||||
db.transaction(() => {
|
||||
const azamiRowid = (
|
||||
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
|
||||
).m;
|
||||
const once = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||
|
||||
const processPage = db.transaction((records: ApiRecord[]) => {
|
||||
for (const r of records) {
|
||||
for (const [id, r] of benzersiz) {
|
||||
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
|
||||
if (!r.kilavuzKodu || !tur) {
|
||||
skipped++;
|
||||
// Puan türü yalnız YENİ satır için gerekir; mevcut satırın türü DB'de var.
|
||||
// Kaynak türü vermiyorsa yeni satır UYDURULMAZ, atlanır ve raporlanır.
|
||||
if (!tur && !exists.get(id)) {
|
||||
atlananlar.push(`${id} ${r.universiteAdi} / ${r.birimAdi} (puanTuru="${r.puanTuru}")`);
|
||||
continue;
|
||||
}
|
||||
const id = String(r.kilavuzKodu);
|
||||
const vals = [
|
||||
intOrNull(r.basariSirasi),
|
||||
realOrNull(r.minPuan),
|
||||
intOrNull(r.kontenjan),
|
||||
yerlesen(r),
|
||||
];
|
||||
const eski = eskiKod(r);
|
||||
if (exists.get(id)) {
|
||||
update.run(...vals, id);
|
||||
update.run(...vals, yil, eski, id);
|
||||
updated++;
|
||||
} else {
|
||||
const gecmis = gecmisAlanlari(r).filter((g) => yil - g.fark >= ILK_YIL);
|
||||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||||
// duplikat kurum adı üretmesin
|
||||
const universite = uniAdiNormalize(r.universiteAdi);
|
||||
insert.run(
|
||||
id,
|
||||
r.birimAdi,
|
||||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||||
// duplikat kurum adı üretmesin
|
||||
uniAdiNormalize(r.universiteAdi),
|
||||
r.universiteTuru ?? null,
|
||||
universite,
|
||||
uniturSec(universite, r.universiteTuru ?? null),
|
||||
r.ilAdi ?? null,
|
||||
r.fymkAdi ?? null,
|
||||
fakulteSec(universite, r.fymkAdi ?? null),
|
||||
tur,
|
||||
r.ogrenimSuresi ?? null,
|
||||
r.birimTuruId === 47 ? 1 : 0,
|
||||
...vals
|
||||
...vals,
|
||||
yil,
|
||||
eski,
|
||||
...gecmis.flatMap((g) => [g.sira, g.puan, g.kontenjan])
|
||||
);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
processPage(first.content);
|
||||
for (let p = 1; p < totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
const page = await fetchPage(p);
|
||||
processPage(page.content);
|
||||
process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`);
|
||||
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||
if (once !== sonra) {
|
||||
dur(
|
||||
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
|
||||
`önce : ${once}\nsonra: ${sonra}`
|
||||
);
|
||||
}
|
||||
const gorulen = (
|
||||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||||
).n;
|
||||
const beklenen = toplam - kodsuz - atlananlar.length;
|
||||
if (gorulen !== beklenen) {
|
||||
dur(
|
||||
`son_kilavuz_yili=${yil} satır sayısı ${gorulen}, beklenen ${beklenen} ` +
|
||||
`(API ${toplam} − atlanan ${kodsuz + atlananlar.length}). Geri alındı.`
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
|
||||
const stats = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS toplam,
|
||||
SUM(CASE WHEN sira2025 IS NOT NULL THEN 1 ELSE 0 END) AS sirali
|
||||
FROM programs`
|
||||
)
|
||||
.get() as { toplam: number; sirali: number };
|
||||
metaYaz(db, `refresh_${yil}_api_toplam`, toplam);
|
||||
metaYaz(db, `refresh_${yil}_atlanan`, kodsuz + atlananlar.length);
|
||||
metaYaz(db, `refresh_${yil}_tarih`, new Date().toISOString());
|
||||
metaYaz(db, `refresh_${yil}_ham`, path.basename(hamDosya));
|
||||
})();
|
||||
|
||||
console.log(
|
||||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}`
|
||||
);
|
||||
console.log(
|
||||
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
|
||||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
|
||||
);
|
||||
for (const a of atlananlar) console.log(` atlandı: ${a}`);
|
||||
|
||||
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
||||
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
||||
db.transaction(() => uniturOnar(db))();
|
||||
|
||||
// Sorgu yolunun ifade indeksi — ifade veri-yillari.ts'ten; eski yılınki DROP.
|
||||
console.log(`Efektif indeks: ${efektifIndeksiKur(db)}`);
|
||||
|
||||
const stats = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS toplam,
|
||||
SUM(son_kilavuz_yili = ${yil}) AS guncel,
|
||||
COUNT(sira${yil}) AS sirali
|
||||
FROM programs`
|
||||
)
|
||||
.get() as { toplam: number; guncel: number; sirali: number };
|
||||
console.log(
|
||||
`DB toplam: ${stats.toplam} program; ${yil} kılavuzunda: ${stats.guncel}; ` +
|
||||
`${yil} sıralaması olan: ${stats.sirali}`
|
||||
);
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
console.log(
|
||||
`\nSıradaki adım: pnpm gecmis-doldur --yil ${yil} --db <db> --ham ${path.relative(process.cwd(), hamDosya)}`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
if (process.argv[1]?.endsWith("refresh.ts")) calistir(main);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
228
scripts/veri-kalite.ts
Normal file
228
scripts/veri-kalite.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* yokatlas.db veri kalitesi denetimi — SALT OKUNUR. Yayın öncesi kapı:
|
||||
* bir ZORUNLU kontrol başarısızsa çıkış kodu 1 (CI/runbook durur).
|
||||
* UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır.
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli]
|
||||
*
|
||||
* --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her
|
||||
* programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen)
|
||||
* birebir aynı mı (dolu değer değişmiş/silinmiş olamaz).
|
||||
* --arsivli programs içinde güncel kılavuzda olmayan satır kalmamış olmalı.
|
||||
*/
|
||||
import path from "node:path";
|
||||
import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
metaOku,
|
||||
tabloVar,
|
||||
yilArgumani,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const TURLER = ["SAYISAL", "EŞİT AĞIRLIK", "SÖZEL", "DİL", "TYT"];
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const oncekiArg = argOku("onceki");
|
||||
const arsivli = bayrakVar("arsivli");
|
||||
const db = dbAc(dbYolu, { saltOkunur: true });
|
||||
const onceki = yil - 1;
|
||||
|
||||
let basarisiz = 0;
|
||||
let uyari = 0;
|
||||
const zorunlu = (ad: string, tamam: boolean, detay: string) => {
|
||||
console.log(`${tamam ? "GEÇTİ " : "KALDI "} ${ad} — ${detay}`);
|
||||
if (!tamam) basarisiz++;
|
||||
};
|
||||
const bilgi = (ad: string, sorunlu: boolean, detay: string) => {
|
||||
console.log(`${sorunlu ? "UYARI " : "bilgi "} ${ad} — ${detay}`);
|
||||
if (sorunlu) uyari++;
|
||||
};
|
||||
const sayi = (sql: string, ...args: unknown[]) =>
|
||||
(db.prepare(sql).get(...args) as { n: number }).n;
|
||||
const ornek = (sql: string, ...args: unknown[]) =>
|
||||
(db.prepare(sql).raw().all(...args) as unknown[][]).map((r) => r.join("/")).join(" ; ");
|
||||
|
||||
console.log(`DB: ${dbYolu}\nYıl: ${yil} Efektif sıra: ${EFEKTIF_SIRA_SQL}\n`);
|
||||
|
||||
// 1. Dosya bütünlüğü
|
||||
const butunluk = (db.pragma("integrity_check") as { integrity_check: string }[])
|
||||
.map((r) => r.integrity_check)
|
||||
.join(",");
|
||||
zorunlu("integrity_check", butunluk === "ok", butunluk);
|
||||
|
||||
// 2. Şema
|
||||
const kolonlar = new Set(kolonAdlari(db, "programs"));
|
||||
const gerekli = [`sira${yil}`, `puan${yil}`, `kontenjan${yil}`, `yerlesen${yil}`, "son_kilavuz_yili", "eski_kod"];
|
||||
const eksikKolon = gerekli.filter((k) => !kolonlar.has(k));
|
||||
zorunlu("şema", eksikKolon.length === 0, eksikKolon.length ? `eksik: ${eksikKolon.join(", ")}` : "yıl kolonları + son_kilavuz_yili + eski_kod var");
|
||||
if (eksikKolon.length) {
|
||||
dur("Şema eksik; kalan kontroller koşulamaz.");
|
||||
}
|
||||
|
||||
// 3. Kapsam: güncel satır = API toplamı − atlanan
|
||||
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
|
||||
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
|
||||
const guncel = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?", yil);
|
||||
zorunlu(
|
||||
"kapsam",
|
||||
Number.isFinite(apiToplam) && guncel === apiToplam - atlanan,
|
||||
`son_kilavuz_yili=${yil}: ${guncel}; API toplamı ${apiToplam} − atlanan ${atlanan} = ${apiToplam - atlanan}`
|
||||
);
|
||||
|
||||
// 4. Doluluk
|
||||
const sirali = sayi(`SELECT COUNT(sira${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const oncekiSirali = sayi(`SELECT COUNT(sira${onceki}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const doluOran = guncel ? sirali / guncel : 0;
|
||||
zorunlu(
|
||||
`sira${yil} doluluk`,
|
||||
doluOran >= 0.7,
|
||||
`${sirali}/${guncel} (%${(doluOran * 100).toFixed(1)}; eşik %70). Aynı satırlarda sira${onceki} dolu: ${oncekiSirali}`
|
||||
);
|
||||
const puanli = sayi(`SELECT COUNT(puan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const kontenjanli = sayi(`SELECT COUNT(kontenjan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
bilgi(`${yil} diğer ölçüler`, kontenjanli < guncel * 0.95, `puan dolu ${puanli}, kontenjan dolu ${kontenjanli}, toplam ${guncel}`);
|
||||
|
||||
// 5. Yıllar arası tutarlılık: sira Y / sira Y-1 ∈ [0,5; 2]
|
||||
const ikisiDolu = sayi(`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL`);
|
||||
const aralikta = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL
|
||||
AND sira${yil} * 1.0 / sira${onceki} BETWEEN 0.5 AND 2`
|
||||
);
|
||||
const aralikOran = ikisiDolu ? aralikta / ikisiDolu : 0;
|
||||
zorunlu(
|
||||
`sira${yil}/sira${onceki} ∈ [0,5; 2]`,
|
||||
ikisiDolu > 0 && aralikOran >= 0.9,
|
||||
`${aralikta}/${ikisiDolu} (%${(aralikOran * 100).toFixed(1)}; eşik %90)`
|
||||
);
|
||||
const ayniSira = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${yil} = sira${onceki} AND puan${yil} = puan${onceki}`
|
||||
);
|
||||
zorunlu(
|
||||
"yıl kayması (kopya yıl)",
|
||||
ikisiDolu > 0 && ayniSira / ikisiDolu < 0.02,
|
||||
`sıra VE puanı ${onceki} ile birebir aynı satır: ${ayniSira}/${ikisiDolu} (çoksa ${yil} kolonuna ${onceki} verisi yazılmıştır)`
|
||||
);
|
||||
|
||||
// 6. Değer aralıkları (tüm yıllar)
|
||||
const yillar: number[] = [];
|
||||
for (let y = ILK_YIL; y <= yil; y++) if (kolonlar.has(`sira${y}`)) yillar.push(y);
|
||||
const bozukKosul = yillar
|
||||
.map((y) => `sira${y} <= 0 OR sira${y} > 5000000 OR puan${y} <= 0 OR puan${y} > 600 OR kontenjan${y} < 0 OR yerlesen${y} < 0`)
|
||||
.join(" OR ");
|
||||
const bozuk = sayi(`SELECT COUNT(*) AS n FROM programs WHERE ${bozukKosul}`);
|
||||
zorunlu("değer aralığı", bozuk === 0, `0/negatif/aşırı sıra-puan-kontenjan: ${bozuk}${bozuk ? ` ör. ${ornek(`SELECT id FROM programs WHERE ${bozukKosul} LIMIT 3`)}` : ""}`);
|
||||
|
||||
const siraPuanTutarsiz = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND puan${yil} IS NULL`
|
||||
);
|
||||
zorunlu(`sira${yil} var, puan${yil} yok`, siraPuanTutarsiz === 0, `${siraPuanTutarsiz} satır`);
|
||||
|
||||
// 7. Kategorik alanlar
|
||||
const bozukTur = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE tur NOT IN (${TURLER.map(() => "?").join(",")}) OR onlisans NOT IN (0, 1)`,
|
||||
...TURLER
|
||||
);
|
||||
zorunlu("tur/onlisans", bozukTur === 0, `geçersiz değer: ${bozukTur}`);
|
||||
const tytTutarsiz = sayi("SELECT COUNT(*) AS n FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("TYT ↔ önlisans", tytTutarsiz > 0, `tur=TYT ile onlisans=1 örtüşmeyen güncel satır: ${tytTutarsiz}${tytTutarsiz ? ` ör. ${ornek("SELECT id, tur, onlisans FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
const uniturBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("unitur boş", uniturBos > 0, `güncel satırlarda ${uniturBos} (devlet/vakıf filtresinden düşer)${uniturBos ? ` ör. ${ornek("SELECT DISTINCT universite FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
const uniturlar = ornek("SELECT COALESCE(unitur, '(NULL)'), COUNT(*) FROM programs GROUP BY unitur ORDER BY 2 DESC");
|
||||
bilgi("unitur dağılımı", false, uniturlar);
|
||||
const ilBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("il boş", ilBos > 0, `güncel satırlarda ${ilBos}${ilBos ? ` ör. ${ornek("SELECT id, universite FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
|
||||
// 8. Kontenjan–yerleşen
|
||||
const tasan = sayi(`SELECT COUNT(*) AS n FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5`);
|
||||
bilgi(`yerlesen${yil} ≫ kontenjan${yil}`, tasan > guncel * 0.02, `yerleşen > 1,5×genel kontenjan + 5: ${tasan} (yerleşen okul birincisi/şehit-gazi/deprem kontenjanlarını da içerir)${tasan ? ` ör. ${ornek(`SELECT id, kontenjan${yil}, yerlesen${yil} FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5 LIMIT 3`)}` : ""}`);
|
||||
const doluAmaSirasiz = sayi(`SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ? AND sira${yil} IS NULL AND yerlesen${yil} > 0`, yil);
|
||||
bilgi(`yerleşen var, sira${yil} yok`, false, `${doluAmaSirasiz} (API az yerleşenli programda sıra vermez; efektif sıra ${onceki}'e düşer)`);
|
||||
|
||||
// 9. Eski/çift satırlar
|
||||
const eskiSatir = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili IS NOT ?", yil);
|
||||
if (arsivli) zorunlu("arşiv", eskiSatir === 0, `programs içinde güncel kılavuzda olmayan satır: ${eskiSatir}`);
|
||||
else bilgi("arşivlenmemiş eski satır", eskiSatir > 0, `${eskiSatir} satır güncel kılavuzda yok ama sitede listelenir (efektif sıra ${onceki}'e düşer)`);
|
||||
const ciftAd = sayi(
|
||||
`SELECT COUNT(*) AS n FROM (SELECT 1 FROM programs WHERE son_kilavuz_yili = ?
|
||||
GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1)`,
|
||||
yil
|
||||
);
|
||||
bilgi("aynı adlı güncel program", false, `(universite, fakulte, isim, tur) yinelenen grup: ${ciftAd} (özel kontenjanlı adaş programlar olağandır)${ciftAd ? ` ör. ${ornek("SELECT universite, isim, COUNT(*) FROM programs WHERE son_kilavuz_yili = ? GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1 LIMIT 3", yil)}` : ""}`);
|
||||
const eskiKodCakisma = sayi(
|
||||
"SELECT COUNT(*) AS n FROM (SELECT eski_kod FROM programs WHERE eski_kod IS NOT NULL AND son_kilavuz_yili = ? GROUP BY eski_kod HAVING COUNT(*) > 1)",
|
||||
yil
|
||||
);
|
||||
bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`);
|
||||
|
||||
// 10. netler
|
||||
if (tabloVar(db, "netler")) {
|
||||
const arsivVar = tabloVar(db, "programs_arsiv");
|
||||
const yetim = sayi(
|
||||
`SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)
|
||||
${arsivVar ? "AND program_id NOT IN (SELECT id FROM programs_arsiv)" : ""}`
|
||||
);
|
||||
bilgi("yetim netler", yetim > 0, `ne programs ne arşivde karşılığı olan: ${yetim}`);
|
||||
const netliGuncel = sayi(
|
||||
"SELECT COUNT(DISTINCT n.program_id) AS n FROM netler n JOIN programs p ON p.id = n.program_id WHERE p.son_kilavuz_yili = ?",
|
||||
yil
|
||||
);
|
||||
bilgi("netler kapsamı", false, `net satırı olan güncel program: ${netliGuncel}/${guncel}; yıl dağılımı: ${ornek("SELECT yil, COUNT(*) FROM netler GROUP BY yil ORDER BY yil")}`);
|
||||
}
|
||||
|
||||
// 11. Sorgu planı
|
||||
const plan = efektifSorguPlani(db);
|
||||
zorunlu("sorgu planı", plan.some((s) => s.includes(EFEKTIF_INDEKS)), plan.join(" | "));
|
||||
|
||||
// 12. Önceki DB ile geçmiş yıl karşılaştırması
|
||||
if (oncekiArg) {
|
||||
const oncekiYol = path.resolve(process.cwd(), oncekiArg);
|
||||
db.prepare("ATTACH DATABASE ? AS eski").run(oncekiYol);
|
||||
const eskiKolonlar = new Set(
|
||||
(db.prepare("PRAGMA eski.table_info(programs)").all() as { name: string }[]).map((c) => c.name)
|
||||
);
|
||||
const ortak: string[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
for (const o of ["sira", "puan", "kontenjan", "yerlesen"]) {
|
||||
if (kolonlar.has(`${o}${y}`) && eskiKolonlar.has(`${o}${y}`)) ortak.push(`${o}${y}`);
|
||||
}
|
||||
}
|
||||
const simdi = tabloVar(db, "programs_arsiv")
|
||||
? `(SELECT id, ${ortak.join(", ")} FROM main.programs UNION ALL SELECT id, ${ortak.join(", ")} FROM main.programs_arsiv)`
|
||||
: "main.programs";
|
||||
const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`);
|
||||
zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`);
|
||||
// Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest.
|
||||
const degisen = sayi(
|
||||
`SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id
|
||||
WHERE ${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")}`
|
||||
);
|
||||
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen}`);
|
||||
const toplamlar = (tablo: string) =>
|
||||
(db
|
||||
.prepare(
|
||||
`SELECT COUNT(*), TOTAL(sira${onceki}), TOTAL(puan${onceki}), TOTAL(kontenjan${onceki}), TOTAL(yerlesen${onceki}) FROM ${tablo}`
|
||||
)
|
||||
.raw()
|
||||
.get() as number[]).join(" | ");
|
||||
console.log(` ${onceki} sağlaması (count | Σsira | Σpuan | Σkontenjan | Σyerlesen)`);
|
||||
console.log(` önceki DB : ${toplamlar("eski.programs")}`);
|
||||
console.log(` şimdi, önceki DB'deki aynı id'ler : ${toplamlar(`${simdi} WHERE id IN (SELECT id FROM eski.programs)`)}`);
|
||||
console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log(`\nSonuç: ${basarisiz} ZORUNLU kontrol kaldı, ${uyari} uyarı.`);
|
||||
if (basarisiz > 0) process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("veri-kalite.ts")) calistir(main);
|
||||
267
scripts/yokatlas-goc.ts
Normal file
267
scripts/yokatlas-goc.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* yokatlas.db şema göçü — idempotent. Yeni yerleştirme yılı için kolonları,
|
||||
* `son_kilavuz_yili` / `eski_kod` kolonlarını, `programs_arsiv` ve `veri_meta`
|
||||
* tablolarını garanti eder. Hiçbir mevcut değeri silmez/ezmez.
|
||||
*
|
||||
* Betiklerin ortak yardımcıları (argüman okuma, DB açma) da buradadır;
|
||||
* refresh / gecmis-doldur / eski-arsivle / detay / veri-kalite buradan alır.
|
||||
*
|
||||
* Kullanım: pnpm yokatlas:goc --yil 2026 --db data/yokatlas.db
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import { ILK_YIL, SON_YIL } from "../src/lib/veri-yillari";
|
||||
|
||||
export type Db = Database.Database;
|
||||
|
||||
/** Her yıl için tutulan dört ölçü ve SQLite tipi. */
|
||||
export const YIL_OLCULERI = [
|
||||
["sira", "INTEGER"],
|
||||
["puan", "REAL"],
|
||||
["kontenjan", "INTEGER"],
|
||||
["yerlesen", "INTEGER"],
|
||||
] as const;
|
||||
|
||||
// ── Komut satırı ─────────────────────────────────────────────────────────
|
||||
|
||||
/** `--ad deger` ya da `--ad=deger`. Yoksa undefined. */
|
||||
export function argOku(ad: string): string | undefined {
|
||||
const argv = process.argv.slice(2);
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (argv[i] === `--${ad}`) {
|
||||
const sonraki = argv[i + 1];
|
||||
return sonraki && !sonraki.startsWith("--") ? sonraki : undefined;
|
||||
}
|
||||
if (argv[i].startsWith(`--${ad}=`)) return argv[i].slice(ad.length + 3);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function bayrakVar(ad: string): boolean {
|
||||
return process.argv.slice(2).includes(`--${ad}`);
|
||||
}
|
||||
|
||||
export class DurHatasi extends Error {}
|
||||
|
||||
/** Güvenlik kilidi: mesajı yazıp betiği durdurmak için fırlatılır. */
|
||||
export function dur(mesaj: string): never {
|
||||
throw new DurHatasi(mesaj);
|
||||
}
|
||||
|
||||
/** main() sarmalayıcı: DurHatasi → temiz mesaj + çıkış kodu 1. */
|
||||
export function calistir(main: () => Promise<void> | void): void {
|
||||
Promise.resolve()
|
||||
.then(main)
|
||||
.catch((err) => {
|
||||
if (err instanceof DurHatasi) console.error(`\nDURDU: ${err.message}`);
|
||||
else console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `--yil` ZORUNLU ve `SON_YIL` ile aynı olmalı. Varsayılan yok: betik hangi
|
||||
* yılı yazdığını API'den ya da takvimden TAHMİN ETMEZ.
|
||||
*/
|
||||
export function yilArgumani(): number {
|
||||
const ham = argOku("yil");
|
||||
if (!ham) {
|
||||
dur(
|
||||
`--yil zorunlu (ör. --yil ${SON_YIL}). Varsayılan yok; yıl API kaydındaki ` +
|
||||
"`yil` alanından da türetilmez."
|
||||
);
|
||||
}
|
||||
const yil = Number.parseInt(ham, 10);
|
||||
if (!Number.isInteger(yil) || String(yil) !== ham.trim()) {
|
||||
dur(`--yil sayı olmalı: "${ham}"`);
|
||||
}
|
||||
if (yil !== SON_YIL) {
|
||||
dur(
|
||||
`--yil ${yil} ≠ SON_YIL ${SON_YIL} (src/lib/veri-yillari.ts). Yeni yıl ` +
|
||||
"yükleniyorsa önce SON_YIL'i güncelle; betikler ve site aynı sabiti okur."
|
||||
);
|
||||
}
|
||||
return yil;
|
||||
}
|
||||
|
||||
/** `--db` ZORUNLU; dosya var olmalı. Yanlışlıkla boş DB yaratılmaz. */
|
||||
export function dbArgumani(): string {
|
||||
const ham = argOku("db");
|
||||
if (!ham) {
|
||||
dur(
|
||||
"--db zorunlu (ör. --db data/yokatlas.db). Önce kopya üzerinde dene: " +
|
||||
"cp data/yokatlas.db /tmp/deneme.db"
|
||||
);
|
||||
}
|
||||
const yol = path.resolve(process.cwd(), ham);
|
||||
if (!fs.existsSync(yol)) dur(`DB bulunamadı: ${yol}`);
|
||||
return yol;
|
||||
}
|
||||
|
||||
export function dbAc(yol: string, secenek: { saltOkunur?: boolean } = {}): Db {
|
||||
const db = new Database(yol, {
|
||||
readonly: secenek.saltOkunur ?? false,
|
||||
fileMustExist: true,
|
||||
});
|
||||
if (!secenek.saltOkunur) db.pragma("journal_mode = WAL");
|
||||
return db;
|
||||
}
|
||||
|
||||
// ── Şema ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function tabloVar(db: Db, tablo: string): boolean {
|
||||
return (
|
||||
db
|
||||
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
||||
.get(tablo) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function kolonAdlari(db: Db, tablo: string): string[] {
|
||||
return (
|
||||
db.prepare(`PRAGMA table_info(${tablo})`).all() as { name: string }[]
|
||||
).map((c) => c.name);
|
||||
}
|
||||
|
||||
function yilDogrula(yil: number): void {
|
||||
if (!Number.isInteger(yil) || yil < ILK_YIL || yil > 2100) {
|
||||
dur(`Geçersiz yıl: ${yil}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sira/puan/kontenjan/yerlesen<yil> kolonlarını (yoksa) ekler. İdempotent.
|
||||
* Kontenjan kırılımı (gk/obk/…) ve kontenjanMeb kolonları BİLEREK eklenmez:
|
||||
* `src/` içinde okuyan kod yok (KARARLAR 2026-09-21).
|
||||
*/
|
||||
export function yilKolonlariniGarantiEt(
|
||||
db: Db,
|
||||
yil: number,
|
||||
tablo = "programs"
|
||||
): string[] {
|
||||
yilDogrula(yil);
|
||||
const mevcut = new Set(kolonAdlari(db, tablo));
|
||||
const eklenen: string[] = [];
|
||||
for (const [olcu, tip] of YIL_OLCULERI) {
|
||||
const kolon = `${olcu}${yil}`;
|
||||
if (mevcut.has(kolon)) continue;
|
||||
db.exec(`ALTER TABLE ${tablo} ADD COLUMN ${kolon} ${tip}`);
|
||||
eklenen.push(kolon);
|
||||
}
|
||||
return eklenen;
|
||||
}
|
||||
|
||||
/**
|
||||
* `son_kilavuz_yili`: programın görüldüğü son tercih kılavuzu yılı.
|
||||
* `eski_kod`: kılavuz kodu değiştiyse bir önceki kod (API `eskiKilavuzKodu`
|
||||
* ya da gecmis-doldur'un benzersiz ad eşleşmesi).
|
||||
*
|
||||
* Geri doldurma TEK SEFERLİK — yalnız kolon ilk kez eklenirken koşar:
|
||||
* kontenjan2025 dolu → 2025 kılavuzunda vardı. Gerisi NULL (bilinmiyor) kalır.
|
||||
*/
|
||||
export function kilavuzKolonlariniGarantiEt(db: Db): string[] {
|
||||
const mevcut = new Set(kolonAdlari(db, "programs"));
|
||||
const eklenen: string[] = [];
|
||||
if (!mevcut.has("son_kilavuz_yili")) {
|
||||
db.exec("ALTER TABLE programs ADD COLUMN son_kilavuz_yili INTEGER");
|
||||
eklenen.push("son_kilavuz_yili");
|
||||
if (mevcut.has("kontenjan2025")) {
|
||||
const r = db
|
||||
.prepare(
|
||||
"UPDATE programs SET son_kilavuz_yili = 2025 WHERE kontenjan2025 IS NOT NULL"
|
||||
)
|
||||
.run();
|
||||
console.log(` son_kilavuz_yili geri doldurma (2025): ${r.changes} satır`);
|
||||
}
|
||||
}
|
||||
if (!mevcut.has("eski_kod")) {
|
||||
db.exec("ALTER TABLE programs ADD COLUMN eski_kod TEXT");
|
||||
eklenen.push("eski_kod");
|
||||
}
|
||||
return eklenen;
|
||||
}
|
||||
|
||||
/**
|
||||
* `programs_arsiv`: programs ile AYNI kolonlar (ad + tip + sıra). Yoksa kurar;
|
||||
* varsa programs'a sonradan eklenmiş kolonları ona da ekler. Böylece
|
||||
* `INSERT INTO programs_arsiv SELECT * FROM programs` her zaman geçerlidir…
|
||||
* yine de taşıma kolon adlarıyla yapılır (bkz. eski-arsivle.ts).
|
||||
*/
|
||||
export function arsivTablosunuGarantiEt(db: Db): string[] {
|
||||
const kolonlar = db.prepare("PRAGMA table_info(programs)").all() as {
|
||||
name: string;
|
||||
type: string;
|
||||
}[];
|
||||
if (!tabloVar(db, "programs_arsiv")) {
|
||||
const tanim = kolonlar
|
||||
.map((c) =>
|
||||
c.name === "id"
|
||||
? "id TEXT PRIMARY KEY"
|
||||
: `${c.name} ${c.type || "TEXT"}`
|
||||
)
|
||||
.join(",\n ");
|
||||
db.exec(`CREATE TABLE programs_arsiv (\n ${tanim}\n)`);
|
||||
return ["programs_arsiv"];
|
||||
}
|
||||
const arsivde = new Set(kolonAdlari(db, "programs_arsiv"));
|
||||
const eklenen: string[] = [];
|
||||
for (const c of kolonlar) {
|
||||
if (arsivde.has(c.name)) continue;
|
||||
db.exec(
|
||||
`ALTER TABLE programs_arsiv ADD COLUMN ${c.name} ${c.type || "TEXT"}`
|
||||
);
|
||||
eklenen.push(`programs_arsiv.${c.name}`);
|
||||
}
|
||||
return eklenen;
|
||||
}
|
||||
|
||||
/** Koşu kayıtları (API toplamı, atlanan, ham dosya…): veri-kalite okur. */
|
||||
export function metaTablosunuGarantiEt(db: Db): void {
|
||||
db.exec(
|
||||
"CREATE TABLE IF NOT EXISTS veri_meta (anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL)"
|
||||
);
|
||||
}
|
||||
|
||||
export function metaYaz(db: Db, anahtar: string, deger: string | number): void {
|
||||
metaTablosunuGarantiEt(db);
|
||||
db.prepare(
|
||||
"INSERT INTO veri_meta (anahtar, deger) VALUES (?, ?) " +
|
||||
"ON CONFLICT(anahtar) DO UPDATE SET deger = excluded.deger"
|
||||
).run(anahtar, String(deger));
|
||||
}
|
||||
|
||||
export function metaOku(db: Db, anahtar: string): string | undefined {
|
||||
if (!tabloVar(db, "veri_meta")) return undefined;
|
||||
const r = db
|
||||
.prepare("SELECT deger FROM veri_meta WHERE anahtar = ?")
|
||||
.get(anahtar) as { deger: string } | undefined;
|
||||
return r?.deger;
|
||||
}
|
||||
|
||||
/** Tüm göç: yıl kolonları + kılavuz kolonları + arşiv + meta. İdempotent. */
|
||||
export function yokatlasGoc(db: Db, yil: number): string[] {
|
||||
const eklenen: string[] = [];
|
||||
db.transaction(() => {
|
||||
eklenen.push(...yilKolonlariniGarantiEt(db, yil));
|
||||
eklenen.push(...kilavuzKolonlariniGarantiEt(db));
|
||||
eklenen.push(...arsivTablosunuGarantiEt(db));
|
||||
metaTablosunuGarantiEt(db);
|
||||
})();
|
||||
return eklenen;
|
||||
}
|
||||
|
||||
// Doğrudan koşulduğunda
|
||||
if (process.argv[1]?.endsWith("yokatlas-goc.ts")) {
|
||||
calistir(() => {
|
||||
const yil = yilArgumani();
|
||||
const db = dbAc(dbArgumani());
|
||||
const eklenen = yokatlasGoc(db, yil);
|
||||
console.log(
|
||||
eklenen.length
|
||||
? `Göç tamam. Eklenen: ${eklenen.join(", ")}`
|
||||
: "Göç tamam. Şema zaten güncel (değişiklik yok)."
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
OG_CONTENT_TYPE,
|
||||
OG_SIZE,
|
||||
} from "@/lib/og-sayfa";
|
||||
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
|
||||
export const alt = "Bölüm taban puanları ve sıralamaları — KolayTercih";
|
||||
export const size = OG_SIZE;
|
||||
@@ -26,6 +27,6 @@ export default async function Image({
|
||||
return createOgSayfaImage({
|
||||
eyebrow: "Bölüm",
|
||||
title: `${ad} taban puanları`,
|
||||
description: `${sayi}2025 yerleştirme verileri · 2026 tercih rehberi`,
|
||||
description: `${sayi}${SON_YIL} yerleştirme verileri · ${TERCIH_YILI} tercih rehberi`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
OG_CONTENT_TYPE,
|
||||
OG_SIZE,
|
||||
} from "@/lib/og-sayfa";
|
||||
import { BASLIK_YILI, SON_YIL } from "@/lib/veri-yillari";
|
||||
|
||||
export const alt =
|
||||
"Tüm bölümlerin taban puanları ve sıralamaları 2026 — KolayTercih";
|
||||
export const alt = `Tüm bölümlerin taban puanları ve sıralamaları ${BASLIK_YILI} — KolayTercih`;
|
||||
export const size = OG_SIZE;
|
||||
export const contentType = OG_CONTENT_TYPE;
|
||||
|
||||
@@ -13,6 +13,6 @@ export default async function Image() {
|
||||
return createOgSayfaImage({
|
||||
eyebrow: "Katalog",
|
||||
title: "Tüm bölümlerin taban puanları ve sıralamaları",
|
||||
description: "Lisans ve önlisans programları · 2025 yerleştirme verileri",
|
||||
description: `Lisans ve önlisans programları · ${SON_YIL} yerleştirme verileri`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
|
||||
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
BolumKatalogu,
|
||||
BolumKataloguSkeleton,
|
||||
} from "@/features/katalog/components/bolum-listesi";
|
||||
|
||||
// Başlıktaki yıl tek kaynaktan (veri-yillari.ts); metadata ve h1 aynı metni okur.
|
||||
const BASLIK = `Tüm Bölümlerin Taban Puanları ve Sıralamaları ${BASLIK_YILI}`;
|
||||
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026",
|
||||
description:
|
||||
"Üniversite bölümlerinin 2025 yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, 2026 tercihleri için.",
|
||||
title: BASLIK,
|
||||
description: `Üniversite bölümlerinin ${SON_YIL} yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, ${TERCIH_YILI} tercihleri için.`,
|
||||
path: "/bolumler",
|
||||
});
|
||||
|
||||
@@ -26,7 +29,7 @@ export default function BolumlerPage() {
|
||||
/>
|
||||
|
||||
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
|
||||
Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026
|
||||
{BASLIK}
|
||||
</h1>
|
||||
|
||||
<Suspense fallback={<BolumKataloguSkeleton />}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OG_CONTENT_TYPE,
|
||||
OG_SIZE,
|
||||
} from "@/lib/og-sayfa";
|
||||
import { SON_YIL } from "@/lib/veri-yillari";
|
||||
|
||||
export const alt = "Üniversite taban puanları ve sıralamaları — KolayTercih";
|
||||
export const size = OG_SIZE;
|
||||
@@ -26,7 +27,7 @@ export default async function Image({
|
||||
if (uni?.programSayisi != null) {
|
||||
parcalar.push(`${uni.programSayisi.toLocaleString("tr-TR")} program`);
|
||||
}
|
||||
parcalar.push("2025 yerleştirme verileri");
|
||||
parcalar.push(`${SON_YIL} yerleştirme verileri`);
|
||||
|
||||
// Logo dosyası varsa OG kartına data URI olarak gömülür; okuma hatası
|
||||
// kartı düşürmesin diye sessizce logosuz karta düşülür.
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
OG_CONTENT_TYPE,
|
||||
OG_SIZE,
|
||||
} from "@/lib/og-sayfa";
|
||||
import { BASLIK_YILI, SON_YIL } from "@/lib/veri-yillari";
|
||||
|
||||
export const alt =
|
||||
"Üniversitelerin taban puanları ve sıralamaları 2026 — KolayTercih";
|
||||
export const alt = `Üniversitelerin taban puanları ve sıralamaları ${BASLIK_YILI} — KolayTercih`;
|
||||
export const size = OG_SIZE;
|
||||
export const contentType = OG_CONTENT_TYPE;
|
||||
|
||||
@@ -13,6 +13,6 @@ export default async function Image() {
|
||||
return createOgSayfaImage({
|
||||
eyebrow: "Katalog",
|
||||
title: "Üniversitelerin taban puanları ve sıralamaları",
|
||||
description: "Devlet ve vakıf üniversiteleri · 2025 yerleştirme verileri",
|
||||
description: `Devlet ve vakıf üniversiteleri · ${SON_YIL} yerleştirme verileri`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
|
||||
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
UniversiteKatalogu,
|
||||
UniversiteKataloguSkeleton,
|
||||
} from "@/features/katalog/components/universite-listesi";
|
||||
|
||||
// Başlıktaki yıl tek kaynaktan (veri-yillari.ts); metadata ve h1 aynı metni okur.
|
||||
const BASLIK = `Üniversitelerin Taban Puanları ve Sıralamaları ${BASLIK_YILI}`;
|
||||
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Üniversitelerin Taban Puanları ve Sıralamaları 2026",
|
||||
description:
|
||||
"Devlet ve vakıf üniversitelerinin 2025 yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, 2026 tercihleri için.",
|
||||
title: BASLIK,
|
||||
description: `Devlet ve vakıf üniversitelerinin ${SON_YIL} yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, ${TERCIH_YILI} tercihleri için.`,
|
||||
path: "/universiteler",
|
||||
});
|
||||
|
||||
@@ -26,7 +29,7 @@ export default function UniversitelerPage() {
|
||||
/>
|
||||
|
||||
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
|
||||
Üniversitelerin Taban Puanları ve Sıralamaları 2026
|
||||
{BASLIK}
|
||||
</h1>
|
||||
|
||||
<Suspense fallback={<UniversiteKataloguSkeleton />}>
|
||||
|
||||
@@ -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"
|
||||
>;
|
||||
|
||||
/** 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,
|
||||
/** 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 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 (
|
||||
<TrendingUp
|
||||
|
||||
@@ -25,6 +25,12 @@ import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
BASLIK_YILI,
|
||||
ONCEKI_YIL,
|
||||
SON_YIL,
|
||||
TERCIH_YILI,
|
||||
} from "@/lib/veri-yillari";
|
||||
|
||||
const sayi = (n: number) => 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 };
|
||||
}
|
||||
|
||||
/** <title> 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>
|
||||
) : (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { KATEGORILER } from "@/lib/kategoriler";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
|
||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||
|
||||
@@ -81,11 +82,11 @@ export function BolumKatalogu() {
|
||||
return (
|
||||
<>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
{sayi(hepsi.length)} bölümün 2025 YKS yerleştirme verileri: taban
|
||||
{sayi(hepsi.length)} bölümün {SON_YIL} YKS yerleştirme verileri: taban
|
||||
puanları, başarı sıralamaları, kontenjanlar ve devlet/vakıf dağılımı.
|
||||
Bölüme tıklayınca o bölümü sunan tüm üniversitelerin tablosunu görürsün.
|
||||
Veriler YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri
|
||||
için rehber niteliğindedir.
|
||||
Veriler YÖK Atlas {SON_YIL} yerleştirme sonuçlarına dayanır;{" "}
|
||||
{TERCIH_YILI} tercihleri için rehber niteliğindedir.
|
||||
</p>
|
||||
|
||||
<PagePixelDivider seed={29} className="mt-8" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
|
||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||
|
||||
@@ -49,11 +50,11 @@ export function UniversiteKatalogu() {
|
||||
return (
|
||||
<>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
{sayi(hepsi.length)} üniversitenin 2025 YKS yerleştirme verileri.
|
||||
{sayi(hepsi.length)} üniversitenin {SON_YIL} YKS yerleştirme verileri.
|
||||
Üniversiteye tıklayınca tüm programlarının taban puanlarını, başarı
|
||||
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler YÖK
|
||||
Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri için rehber
|
||||
niteliğindedir.
|
||||
Atlas {SON_YIL} yerleştirme sonuçlarına dayanır; {TERCIH_YILI} tercihleri
|
||||
için rehber niteliğindedir.
|
||||
</p>
|
||||
|
||||
<PagePixelDivider seed={63} className="mt-8" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { KATALOG_VERI_TARIHI } from "@/lib/seo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SON_YIL } from "@/lib/veri-yillari";
|
||||
|
||||
// Veri tarihinin görünür biçimi ("9 Ağustos 2026") — tek biçim kaynağı; ana
|
||||
// Veri tarihinin görünür biçimi ("21 Eylül 2026") — tek biçim kaynağı; ana
|
||||
// sayfa hero'sundaki veri satırı da bunu kullanır.
|
||||
export const KATALOG_TARIH_METNI = new Date(
|
||||
`${KATALOG_VERI_TARIHI}T00:00:00Z`,
|
||||
@@ -24,7 +25,7 @@ export function VeriKaynagi({
|
||||
return (
|
||||
<p className={cn("mt-3 text-xs leading-5 text-slate-500", className)}>
|
||||
{children}
|
||||
Kaynak: YÖK Atlas 2025 yerleştirme sonuçları. Son güncelleme:{" "}
|
||||
Kaynak: YÖK Atlas {SON_YIL} yerleştirme sonuçları. Son güncelleme:{" "}
|
||||
<time dateTime={KATALOG_VERI_TARIHI}>{KATALOG_TARIH_METNI}</time>.
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -24,8 +24,7 @@ type ProgramOzet = Pick<
|
||||
| "il"
|
||||
| "unitur"
|
||||
| "tur"
|
||||
| "sira2025"
|
||||
| "sira2024"
|
||||
| "efektifSira"
|
||||
>;
|
||||
|
||||
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
"—"
|
||||
|
||||
@@ -28,8 +28,7 @@ export type BekleyenProgram = {
|
||||
| "il"
|
||||
| "unitur"
|
||||
| "tur"
|
||||
| "sira2025"
|
||||
| "sira2024"
|
||||
| "efektifSira"
|
||||
>;
|
||||
kaynak: ManuelKaynak;
|
||||
adaySira?: number;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -158,10 +158,8 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
|
||||
return liste;
|
||||
}
|
||||
|
||||
export function efektifSiraAl(
|
||||
p: Pick<Program, "sira2025" | "sira2024">,
|
||||
): number | null {
|
||||
return p.sira2025 ?? p.sira2024 ?? null;
|
||||
export function efektifSiraAl(p: Pick<Program, "efektifSira">): 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",
|
||||
|
||||
@@ -16,7 +16,7 @@ flowchart TD
|
||||
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, yalnızca ulaşabildiklerin listelenir")
|
||||
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
||||
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar")
|
||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar<br/>kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
|
||||
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
||||
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce il, sonra alan — listede açıkça belirtilir")
|
||||
G --> H
|
||||
@@ -44,10 +44,12 @@ export function HuniSemasi() {
|
||||
return (
|
||||
<ElCizimiSema
|
||||
tanim={SEMA}
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||
figcaption={
|
||||
<>
|
||||
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
||||
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez. Her
|
||||
adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||
<span className="font-medium text-slate-700">
|
||||
dışına asla çıkmadan
|
||||
</span>{" "}
|
||||
|
||||
@@ -47,8 +47,8 @@ export function MeraklisinaIcerik() {
|
||||
öğrencinin Türkiye sıralamasıdır. Senin sıralaman bu sayıdan
|
||||
küçükse (yani daha iyiysen), geçen yılın koşullarında o bölüme
|
||||
yerleşebilirdin demektir. Sistemde her programın en güncel taban
|
||||
sıralaması kayıtlıdır; bir bölümün 2025 verisi henüz oluşmamışsa
|
||||
bir önceki yılınki kullanılır.
|
||||
sıralaması kayıtlıdır; bir bölüm için en son yılın verisi henüz
|
||||
oluşmamışsa bir önceki yılınki kullanılır.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -116,10 +116,13 @@ export function MeraklisinaIcerik() {
|
||||
tabloya da uygulanır. Adımlar bitince elde kalan gerçek
|
||||
programlar bir <strong>aday havuzu</strong>{" "}
|
||||
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
||||
verilir. Aşağıdaki şema akışın tamamını gösterir — havuz
|
||||
24'ün altına düşerse filtrelerin nasıl gevşetildiği, yine de
|
||||
yetmezse listenin eldeki programlarla, sana açıkça belirtilerek
|
||||
nasıl kurulduğu da dahil.
|
||||
verilir. Havuza yalnızca{" "}
|
||||
<strong>en son tercih kılavuzunda yer alan</strong>{" "}
|
||||
programlar girer: kapanmış ya da kodu değiştiği için eski kaydı
|
||||
kalmış bir program sana önerilmez. Aşağıdaki şema akışın tamamını
|
||||
gösterir — havuz 24'ün altına düşerse filtrelerin nasıl
|
||||
gevşetildiği, yine de yetmezse listenin eldeki programlarla, sana
|
||||
açıkça belirtilerek nasıl kurulduğu da dahil.
|
||||
</p>
|
||||
<HuniSemasi />
|
||||
</section>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DILIM_ETIKET, RISK_ETIKET, dilimdenRisk } from "@/lib/risk";
|
||||
import type { RaporParams, RaporSonuc } from "../types/rapor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { YazdirButonu } from "./yazdir-butonu";
|
||||
import { ILK_YIL, VERI_ARALIGI } from "@/lib/veri-yillari";
|
||||
|
||||
// Dilim sütunu: dilim adı + varsayılan riski. Üçüncü dilimde ikisi aynı
|
||||
// kelime ("Güvenli") olduğu için parantez düşer — "Güvenli (Güvenli)" yazmaz.
|
||||
@@ -29,6 +30,23 @@ export async function RaporYazdir() {
|
||||
|
||||
const rapor = satir.result as RaporSonuc;
|
||||
const params = satir.params as RaporParams;
|
||||
// Dipnottaki yıl aralığı raporun ÜRETİLDİĞİ veri penceresidir: eski kayıtlı
|
||||
// rapor (5 yıllık geçmiş) güncel "2021–2026"yı iddia etmesin.
|
||||
const gecmisler = Object.values(rapor.programlar).filter(
|
||||
(p) => p.siraGecmisi && p.siraGecmisi.length > 0,
|
||||
);
|
||||
const veriAraligi =
|
||||
gecmisler.length > 0
|
||||
? (() => {
|
||||
const ilk = Math.min(...gecmisler.map((p) => p.ilkYil ?? ILK_YIL));
|
||||
const son = Math.max(
|
||||
...gecmisler.map(
|
||||
(p) => (p.ilkYil ?? ILK_YIL) + p.siraGecmisi!.length - 1,
|
||||
),
|
||||
);
|
||||
return `${ilk}–${son}`;
|
||||
})()
|
||||
: VERI_ARALIGI;
|
||||
const tarih = new Date(satir.updatedAt).toLocaleDateString("tr-TR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
@@ -106,7 +124,7 @@ export async function RaporYazdir() {
|
||||
|
||||
<footer className="mt-6 border-t border-slate-300 pt-3 text-[10px] leading-relaxed text-slate-500">
|
||||
<p>
|
||||
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi (2021–2025
|
||||
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "}
|
||||
taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
|
||||
üretilmiştir. Taban sıralamaları her yıl değişir; bu rapor yerleşme
|
||||
garantisi vermez. Tercih listesinin ÖSYM sistemine girilmesi ve nihai
|
||||
|
||||
@@ -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())}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -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<Dilim, AiSecim[]> | null = null;
|
||||
|
||||
@@ -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<yıl>` 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(
|
||||
|
||||
@@ -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()
|
||||
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 })[];
|
||||
.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()
|
||||
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[];
|
||||
.all(...kayit.hamAdlar) as ProgramHamSatiri[]
|
||||
).map(programSatiri);
|
||||
|
||||
const gruplar = new Map<string, Program[]>();
|
||||
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)!;
|
||||
|
||||
@@ -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<yıl>, kontenjan<yıl>): 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,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -32,9 +32,10 @@ export const RISK_PIN_RENK: Record<RiskSeviyesi, string> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SITE_NAME = "KolayTercih";
|
||||
* Yalnızca veri GERÇEKTEN değiştiğinde (ingest/refresh sonrası) elle güncelle;
|
||||
* build zamanına bağlama — her deploy'da değişen lastmod sinyali değersizleşir.
|
||||
*/
|
||||
export const KATALOG_VERI_TARIHI = "2026-08-09";
|
||||
export const KATALOG_VERI_TARIHI = "2026-09-21";
|
||||
|
||||
type SayfaMetadataGirdi = {
|
||||
title: string;
|
||||
|
||||
@@ -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<string, number> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
38
src/lib/veri-yillari.ts
Normal file
38
src/lib/veri-yillari.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Yerleştirme verisinin yıl kapsamı — TEK KAYNAK. Saf ve import'suz: client,
|
||||
* server ve `scripts/` aynı dosyayı okur. Yeni yıl verisi yüklenince yalnız
|
||||
* `SON_YIL` değişir (+ `pnpm refresh -- --yil <yıl>` ile DB inşası).
|
||||
*/
|
||||
|
||||
/** Veri penceresinin ilk yılı. Kayıtlı rapor/tadımlık `siraGecmisi` dizileri
|
||||
* bu yıla demirlidir; pencere kaydırılmaz, uzar. */
|
||||
export const ILK_YIL = 2021;
|
||||
|
||||
/** En son yerleştirme yılı (DB'de `sira${SON_YIL}` kolonları dolu). */
|
||||
export const SON_YIL = 2026;
|
||||
|
||||
export const ONCEKI_YIL = SON_YIL - 1;
|
||||
|
||||
/** [2021, …, SON_YIL] — `siraGecmisi` dizileriyle indeks hizalı. */
|
||||
export const YILLAR: readonly number[] = Array.from(
|
||||
{ length: SON_YIL - ILK_YIL + 1 },
|
||||
(_, i) => ILK_YIL + i,
|
||||
);
|
||||
|
||||
/** Bu veriyle tercih yapacak öğrencilerin yılı. */
|
||||
export const TERCIH_YILI = SON_YIL + 1;
|
||||
|
||||
/** Sayfa başlıklarında görünen yıl ("… Taban Puanları 2026"). */
|
||||
export const BASLIK_YILI = SON_YIL;
|
||||
|
||||
export const VERI_YIL_SAYISI = YILLAR.length;
|
||||
|
||||
/** "2021–2026" */
|
||||
export const VERI_ARALIGI = `${ILK_YIL}–${SON_YIL}`;
|
||||
|
||||
/**
|
||||
* Efektif sıra: son yılın sırası, yoksa bir önceki yılınki. İNDEKS SÖZLEŞMESİ —
|
||||
* SQLite ifade indeksi metinsel eşleşmeyle çalışır; sorgular ve indeks
|
||||
* betikleri bu dizgiyi birebir (virgülden sonra tek boşluk) kullanmalıdır.
|
||||
*/
|
||||
export const EFEKTIF_SIRA_SQL = `COALESCE(sira${SON_YIL}, sira${ONCEKI_YIL})`;
|
||||
@@ -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<Program, "fakulte" | "puan2025">;
|
||||
export type ProgramListeSatiri = Omit<
|
||||
Program,
|
||||
"fakulte" | "puanSon" | "siraSon"
|
||||
>;
|
||||
|
||||
export type RankResultsListe = {
|
||||
hayal: ProgramListeSatiri[];
|
||||
|
||||
Reference in New Issue
Block a user